Skip to content

Commit

Permalink
feat: ✨ #2 Vertical/Horizontal/Wrapping control
Browse files Browse the repository at this point in the history
  • Loading branch information
scottdurow committed Nov 25, 2022
1 parent 3b7b795 commit efb040f
Show file tree
Hide file tree
Showing 17 changed files with 13,437 additions and 6,811 deletions.
2 changes: 1 addition & 1 deletion code-component/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
"prettier",
"prettier/react"
"plugin:sonarjs/recommended"
],
"parserOptions": {
"project": "./tsconfig.json"
Expand Down
5 changes: 4 additions & 1 deletion code-component/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,7 @@

# msbuild output directories
/bin
/obj
/obj

# Jest coverage results
**/coverage
16 changes: 16 additions & 0 deletions code-component/.vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"configurations": [
{
"type": "node",
"name": "vscode-jest-tests",
"request": "launch",
"args": ["${fileBasename}", "--runInBand", "--code-coverage=false" ],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"smartStep": true,
"program": "${workspaceFolder}/node_modules/jest/bin/jest"
}
]
}

3 changes: 2 additions & 1 deletion code-component/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"cSpell.words": [
"powerdnd",
"Picklist",
"powerdnd",
"sonarjs",
"sortablejs",
"Unchoose"
]
Expand Down
8 changes: 7 additions & 1 deletion code-component/PowerDragDrop/ControlManifest.Input.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>
<control namespace="CustomControl" constructor="PowerDragDrop" version="1.0.19" display-name-key="PowerDragDrop" description-key="PowerDragDrop_Desc" control-type="standard">
<control namespace="CustomControl" constructor="PowerDragDrop" version="1.0.21" display-name-key="PowerDragDrop" description-key="PowerDragDrop_Desc" control-type="standard">
<!--external-service-usage node declares whether this 3rd party PCF control is using external service or not, if yes, this control will be considered as premium and please also add the external domain it is using.-->
<external-service-usage enabled="false"></external-service-usage>
<property name="DropZoneID" description-key="DropZoneID_Desc" display-name-key="DropZoneID" required="true" usage="input" of-type="SingleLine.Text"/>
Expand Down Expand Up @@ -37,6 +37,12 @@
<value name="AnitcockwiseLarge" display-name-key="AnitclockwiseLarge">4</value>
</property>
<property name="PreserveSort" description-key="PreserveSort_Desc" display-name-key="PreserveSort" required="true" usage="bound" of-type="TwoOptions"/>
<property name="Direction" description-key="Direction_Desc" display-name-key="Direction" required="false" usage="bound" of-type="Enum" default-value="0">
<value name="Auto" display-name-key="Auto">0</value>
<value name="Vertical" display-name-key="Vertical">1</value>
<value name="Horizontal" display-name-key="Horizontal">2</value>
</property>
<property name="Wrap" description-key="Wrap_Desc" display-name-key="Wrap" required="false" usage="bound" of-type="TwoOptions" default-value="false"/>
<property name="Scroll" description-key="Scroll_Desc" display-name-key="Scroll" required="true" usage="bound" of-type="TwoOptions" default-value="true"/>
<property name="DelaySelect" description-key="DelaySelect_Desc" display-name-key="DelaySelect" of-type="Enum" usage="bound" required="false" default-value="0">
<value name="No" display-name-key="No">0</value>
Expand Down
27 changes: 24 additions & 3 deletions code-component/PowerDragDrop/ItemRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import sanitize from 'sanitize-html';
import { CurrentItem } from './CurrentItemSchema';
import { GetOutputObjectRecord } from './DynamicSchema';
import { IInputs } from './generated/ManifestTypes';
import { ItemProperties } from './ManifestConstants';
import { DirectionEnum, ItemProperties } from './ManifestConstants';
import { SanitizeHtmlOptions } from './SanitizeHtmlOptions';
import { CSS_STYLE_CLASSES, ORIGINAL_POSITION_ATTRIBUTE, ORIGINAL_ZONE_ATTRIBUTE, RECORD_ID_ATTRIBUTE } from './Styles';

Expand Down Expand Up @@ -46,6 +46,7 @@ export class ItemRenderer {
return true;
}

// eslint-disable-next-line sonarjs/cognitive-complexity
public renderItems(
context: ComponentFramework.Context<IInputs>,
): { itemsRendered?: CurrentItem[]; sortOrder?: string[] } {
Expand Down Expand Up @@ -96,6 +97,7 @@ export class ItemRenderer {

// Style the list container
this.updateContainerStyles(parameters);
this.updateContainerFlex(parameters);

// Items for this drop zone (or all items if the drop zone is not set)
const thisDropZoneId = parameters.DropZoneID.raw ?? '';
Expand Down Expand Up @@ -160,8 +162,7 @@ export class ItemRenderer {
if (!context.parameters.items.columns) {
return [];
}
const columns = context.parameters.items.columns.filter((c) => c.order !== -1);
return columns;
return context.parameters.items.columns.filter((c) => c.order !== -1);
}

private updateContainerStyles(parameters: IInputs) {
Expand Down Expand Up @@ -197,6 +198,26 @@ export class ItemRenderer {
}
}

private updateContainerFlex(parameters: IInputs) {
const listContainer = this.listContainer;
if (parameters.Direction?.raw !== null || parameters.Wrap?.raw !== null) {
// Set the list direction and wrap
// Auto for standard ordered list behavior
const direction = parameters.Direction?.raw;
const wrap = parameters.Wrap?.raw;

if (direction === DirectionEnum.Auto && wrap !== true) {
listContainer.style.flexDirection = '';
listContainer.style.flexWrap = '';
listContainer.style.display = '';
} else {
listContainer.style.flexDirection = direction === DirectionEnum.Vertical ? 'column' : 'row';
listContainer.style.flexWrap = wrap === true ? 'wrap' : 'nowrap';
listContainer.style.display = 'flex';
}
}
}

private styleItemElement(itemRow: HTMLElement, parameters: IInputs) {
if (parameters.ItemBackgroundColor.raw) {
itemRow.style.backgroundColor = parameters.ItemBackgroundColor.raw;
Expand Down
10 changes: 10 additions & 0 deletions code-component/PowerDragDrop/ManifestConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export enum ManifestConstants {
ItemFont = 'ItemFont',
DelaySelect = 'DelaySelect',
ItemGap = 'ItemGap',
Direction = 'Direction',
Wrap = 'Wrap',
}

export enum InputEvents {
Expand Down Expand Up @@ -60,6 +62,8 @@ export const RENDER_TRIGGER_PROPERTIES: string[] = [
ManifestConstants.PaddingTop,
ManifestConstants.PaddingBottom,
ManifestConstants.Scroll,
ManifestConstants.Direction,
ManifestConstants.Wrap,
];

export const ZONE_REGISTRATION_PROPERTIES: string[] = [
Expand All @@ -74,3 +78,9 @@ export const ZONE_OPTIONS_PROPERTIES: string[] = [
ManifestConstants.Scroll,
ManifestConstants.DelaySelect,
];

export enum DirectionEnum {
Auto = '0',
Vertical = '1',
Horizontal = '2',
}
106 changes: 106 additions & 0 deletions code-component/PowerDragDrop/__mocks__/mock-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/* istanbul ignore file */

export class MockContext<T> implements ComponentFramework.Context<T> {
constructor(parameters: T) {
this.parameters = parameters;
this.mode = {
allocatedHeight: -1,
allocatedWidth: -1,
isControlDisabled: false,
isVisible: true,
label: '',
setControlState: jest.fn(),
setFullScreen: jest.fn(),
trackContainerResize: jest.fn(),
};
this.client = {
disableScroll: false,
getClient: jest.fn(),
getFormFactor: jest.fn(),
isOffline: jest.fn(),
};

// Canvas apps currently assigns a positive tab-index
// so we must use this property to assign a positive tab-index also
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this as any).accessibility = { assignedTabIndex: 0 };
}
client: ComponentFramework.Client;
device: ComponentFramework.Device;
factory: ComponentFramework.Factory;
formatting: ComponentFramework.Formatting;
mode: ComponentFramework.Mode;
navigation: ComponentFramework.Navigation;
resources: ComponentFramework.Resources;
userSettings: ComponentFramework.UserSettings;
utils: ComponentFramework.Utility;
webAPI: ComponentFramework.WebApi;
parameters: T;
updatedProperties: string[] = [];
}

export class MockState implements ComponentFramework.Dictionary {}

export class MockStringProperty implements ComponentFramework.PropertyTypes.StringProperty {
constructor(raw?: string | null, formatted?: string | undefined) {
this.raw = raw ?? null;
this.formatted = formatted;
}
raw: string | null;
attributes?: ComponentFramework.PropertyHelper.FieldPropertyMetadata.StringMetadata | undefined;
error: boolean;
errorMessage: string;
formatted?: string | undefined;
security?: ComponentFramework.PropertyHelper.SecurityValues | undefined;
type: string;
}

export class MockWholeNumberProperty implements ComponentFramework.PropertyTypes.WholeNumberProperty {
constructor(raw?: number | null, formatted?: string | undefined) {
this.raw = raw ?? null;
this.formatted = formatted;
}
attributes?: ComponentFramework.PropertyHelper.FieldPropertyMetadata.WholeNumberMetadata | undefined;
raw: number | null;
error: boolean;
errorMessage: string;
formatted?: string | undefined;
security?: ComponentFramework.PropertyHelper.SecurityValues | undefined;
type: string;
}

export class MockDecimalProperty implements ComponentFramework.PropertyTypes.DecimalNumberProperty {
constructor(raw?: number | null, formatted?: string | undefined) {
this.raw = raw ?? null;
this.formatted = formatted;
}
attributes?: ComponentFramework.PropertyHelper.FieldPropertyMetadata.DecimalNumberMetadata | undefined;
raw: number | null;
error: boolean;
errorMessage: string;
formatted?: string | undefined;
security?: ComponentFramework.PropertyHelper.SecurityValues | undefined;
type: string;
}

export class MockEnumProperty<T> implements ComponentFramework.PropertyTypes.EnumProperty<T> {
constructor(raw?: T, type?: string) {
if (raw) this.raw = raw;
if (type) this.type = type;
}
type: string;
raw: T;
}

export class MockTwoOptionsProperty implements ComponentFramework.PropertyTypes.TwoOptionsProperty {
constructor(raw?: boolean) {
if (raw) this.raw = raw;
}
raw: boolean;
attributes?: ComponentFramework.PropertyHelper.FieldPropertyMetadata.TwoOptionMetadata | undefined;
error: boolean;
errorMessage: string;
formatted?: string | undefined;
security?: ComponentFramework.PropertyHelper.SecurityValues | undefined;
type: string;
}
108 changes: 108 additions & 0 deletions code-component/PowerDragDrop/__mocks__/mock-datasets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/* istanbul ignore file */

export class MockDataSet implements ComponentFramework.PropertyTypes.DataSet {
private rows: MockEntityRecord[] = [];
constructor(rows: MockEntityRecord[]) {
this.rows = rows;
this.records = {};
rows.forEach((r) => (this.records[r.id] = r));
this.sortedRecordIds = rows.map((r) => r.id);
this.paging = {
setPageSize: jest.fn(),
totalResultCount: 0,
firstPageNumber: 0,
hasNextPage: false,
hasPreviousPage: false,
lastPageNumber: 0,
loadExactPage: jest.fn(),
loadNextPage: jest.fn(),
loadPreviousPage: jest.fn(),
pageSize: 0,
reset: jest.fn(),
};
}
addColumn = jest.fn();
columns: ComponentFramework.PropertyHelper.DataSetApi.Column[] = [];
error: boolean;
errorMessage: string;
filtering: ComponentFramework.PropertyHelper.DataSetApi.Filtering;
linking: ComponentFramework.PropertyHelper.DataSetApi.Linking;
loading: boolean;
paging: ComponentFramework.PropertyHelper.DataSetApi.Paging;
records: { [id: string]: ComponentFramework.PropertyHelper.DataSetApi.EntityRecord };
sortedRecordIds: string[];
sorting: ComponentFramework.PropertyHelper.DataSetApi.SortStatus[];
clearSelectedRecordIds = jest.fn();
getSelectedRecordIds = jest.fn();
getTargetEntityType = jest.fn();
getTitle = jest.fn();
getViewId = jest.fn();
openDatasetItem = jest.fn();
refresh = jest.fn();
setSelectedRecordIds = jest.fn();
}

export class MockColumn implements ComponentFramework.PropertyHelper.DataSetApi.Column {
name: string;
displayName: string;
dataType!: string;
alias!: string;
order!: number;
visualSizeFactor!: number;
isHidden?: boolean | undefined;
isPrimary?: boolean | undefined;
disableSorting?: boolean | undefined;
constructor(name: string, displayName: string) {
this.name = name;
this.displayName = displayName;
}
}

type valueType =
| string
| number
| boolean
| Date
| number[]
| ComponentFramework.EntityReference
| ComponentFramework.EntityReference[]
| ComponentFramework.LookupValue
| ComponentFramework.LookupValue[];

export class MockEntityRecord implements ComponentFramework.PropertyHelper.DataSetApi.EntityRecord {
values: Record<string, valueType>;
id: string;
constructor(id: string, values: Record<string, valueType>) {
this.values = values;
this.id = id;
}
getFormattedValue(columnName: string): string {
return this.values[columnName] as string;
}
getRecordId(): string {
return this.id;
}
getValue(columnName: string): valueType {
return this.values[columnName];
}
getNamedReference = jest.fn();
}

export function getData(
records: ComponentFramework.PropertyHelper.DataSetApi.EntityRecord[],
): {
sortedRecordIds: string[];
records: Record<string, ComponentFramework.PropertyHelper.DataSetApi.EntityRecord>;
} {
const sortedRecordIds: string[] = [];
const recordsOut: Record<string, ComponentFramework.PropertyHelper.DataSetApi.EntityRecord> = {};

for (const r of records) {
sortedRecordIds.push(r.getRecordId());
recordsOut[r.getRecordId()] = r;
}
return {
sortedRecordIds: sortedRecordIds,
records: recordsOut,
};
}
Loading

0 comments on commit efb040f

Please sign in to comment.