Skip to content

Commit

Permalink
fix(editors): Select Editor option to return flat data w/complex obje…
Browse files Browse the repository at this point in the history
…ct (#189)

* fix(editors): Select Editor option to return flat data w/complex object
  • Loading branch information
ghiscoding authored Dec 9, 2020
1 parent 6a96c9f commit 4695cd3
Show file tree
Hide file tree
Showing 10 changed files with 93 additions and 51 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Note however that this project also has a Vanilla Implementation (not associated
and it is also used to test with [Cypress](https://www.cypress.io/) the UI portion. The Vanilla bundle is also used in our SalesForce (with Lightning Web Component) hence the creation of this monorepo.

### Fully Tested with [Jest](https://jestjs.io/) (Unit Tests) - [Cypress](https://www.cypress.io/) (E2E Tests)
Slickgrid-Universal has **100%** Unit Test Coverage, we are talking about +12,000 lines of code (+2,800 unit tests) that are now fully tested with [Jest](https://jestjs.io/). There are also +200 Cypress E2E tests to cover all [Examples](https://ghiscoding.github.io/slickgrid-universal/) and most UI functionalities (there's also an additional +360 tests in Aurelia-Slickgrid)
Slickgrid-Universal has **100%** Unit Test Coverage, we are talking about +12,000 lines of code (+3,000 unit tests) that are now fully tested with [Jest](https://jestjs.io/). There are also +200 Cypress E2E tests to cover all [Examples](https://ghiscoding.github.io/slickgrid-universal/) and most UI functionalities (there's also an additional +360 tests in Aurelia-Slickgrid)

### Available Public Packages

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,8 +302,8 @@ export class Example11 {
this.sgb.slickGrid.invalidate();
editCommand.execute();

const hash = { [editCommand.row]: { [column.field]: 'unsaved-editable-field' } };
this.sgb.slickGrid.setCellCssStyles(`unsaved_highlight_${[column.field]}${editCommand.row}`, hash);
const hash = { [editCommand.row]: { [column.id]: 'unsaved-editable-field' } };
this.sgb.slickGrid.setCellCssStyles(`unsaved_highlight_${[column.id]}${editCommand.row}`, hash);
}
},
enableContextMenu: true,
Expand Down
41 changes: 25 additions & 16 deletions examples/webpack-demo-vanilla-bundle/src/examples/example12.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,23 +166,28 @@ export class Example12 {
type: FieldType.number,
sortable: true, filterable: true, columnGroup: 'Analysis',
filter: { model: Filters.compoundSlider, operator: '>=' },
// formatter: Formatters.collectionEditor,
editor: {
model: Editors.slider,
// model: Editors.multipleSelect,
// enableRenderHtml: true,
// collection: Array.from(Array(101).keys()).map(k => ({ value: k, label: k, symbol: ' <i class="mdi mdi-calendar-check color-primary"></i>' })),
// collectionOptions: {
// addCustomFirstEntry: { value: '', label: '--none--' }
// },
// customStructure: {
// value: 'value',
// label: 'label',
// labelSuffix: 'symbol'
// },
massUpdate: true, minValue: 0, maxValue: 100,
},
},
// {
// id: 'percentComplete2', name: '% Complete', field: 'analysis.percentComplete', minWidth: 100,
// type: FieldType.number,
// sortable: true, filterable: true, columnGroup: 'Analysis',
// filter: { model: Filters.compoundSlider, operator: '>=' },
// formatter: Formatters.complex,
// exportCustomFormatter: Formatters.complex, // without the Editing cell Formatter
// editor: {
// model: Editors.singleSelect,
// serializeComplexValueFormat: 'flat', // if we keep "object" as the default it will apply { value: 2, label: 2 } which is not what we want in this case
// collection: Array.from(Array(101).keys()).map(k => ({ value: k, label: k })),
// collectionOptions: {
// addCustomFirstEntry: { value: '', label: '--none--' }
// },
// massUpdate: true, minValue: 0, maxValue: 100,
// },
// },
{
id: 'start', name: 'Start', field: 'start', sortable: true, minWidth: 100,
formatter: Formatters.dateUs, columnGroup: 'Period',
Expand Down Expand Up @@ -229,6 +234,7 @@ export class Example12 {
dataKey: 'id',
labelKey: 'itemName',
formatter: Formatters.complexObject,
exportCustomFormatter: Formatters.complex, // without the Editing cell Formatter
type: FieldType.object,
sortComparer: SortComparers.objectString,
editor: {
Expand Down Expand Up @@ -263,7 +269,7 @@ export class Example12 {
{
id: 'origin', name: 'Country of Origin', field: 'origin',
formatter: Formatters.complexObject, columnGroup: 'Item',
exportWithFormatter: true,
exportCustomFormatter: Formatters.complex, // without the Editing cell Formatter
dataKey: 'code',
labelKey: 'name',
type: FieldType.object,
Expand Down Expand Up @@ -355,7 +361,7 @@ export class Example12 {
},
enableExcelExport: true,
excelExportOptions: {
exportWithFormatter: true
exportWithFormatter: false
},
registerExternalServices: [new ExcelExportService()],
enableFiltering: true,
Expand Down Expand Up @@ -426,6 +432,9 @@ export class Example12 {
title: 'Task ' + i,
duration: Math.floor(Math.random() * 100) + 10,
percentComplete: randomPercentComplete > 100 ? 100 : randomPercentComplete,
analysis: {
percentComplete: randomPercentComplete > 100 ? 100 : randomPercentComplete,
},
start: new Date(randomYear, randomMonth, randomDay, randomDay, randomTime, randomTime, randomTime),
finish: (i % 3 === 0 && (randomFinish > new Date() && i > 3)) ? randomFinish : '', // make sure the random date is earlier than today and it's index is bigger than 3
cost: (i % 33 === 0) ? null : Math.round(Math.random() * 10000) / 100,
Expand Down Expand Up @@ -613,8 +622,8 @@ export class Example12 {
if (editCommand && item && column) {
const row = this.sgb.dataView.getRowByItem(item);
if (row >= 0) {
const hash = { [row]: { [column.field]: 'unsaved-editable-field' } };
this.sgb.slickGrid.setCellCssStyles(`unsaved_highlight_${[column.field]}${row}`, hash);
const hash = { [row]: { [column.id]: 'unsaved-editable-field' } };
this.sgb.slickGrid.setCellCssStyles(`unsaved_highlight_${[column.id]}${row}`, hash);
}
}
}
Expand Down
13 changes: 13 additions & 0 deletions packages/common/src/editors/__tests__/selectEditor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,19 @@ describe('SelectEditor', () => {
expect(currentValue).toEqual({});
});

it('should return flat value when using a dot (.) notation for complex object with a collection of option/label pair and using "serializeComplexValueFormat" as "flat"', () => {
mockColumn.field = 'employee.gender';
mockItemData = { id: 1, employee: { id: 24, gender: ['male', 'other'] }, isActive: true };
(mockColumn.internalColumnEditor as ColumnEditor).serializeComplexValueFormat = 'flat';
editor = new SelectEditor(editorArguments, true);
editor.loadValue(mockItemData);
const output = editor.serializeValue();
const currentValue = editor.currentValue;

expect(output).toEqual(['male', 'other']);
expect(currentValue).toEqual('');
});

it('should return object value when using a dot (.) notation and we override the object path using "complexObjectPath" to find correct values', () => {
mockColumn.field = 'employee.bio';
mockItemData = { id: 1, employee: { id: 24, bio: { gender: ['male', 'other'] } }, isActive: true };
Expand Down
59 changes: 31 additions & 28 deletions packages/common/src/editors/selectEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export class SelectEditor implements Editor {
single: true,
textTemplate: ($elm) => {
// render HTML code or not, by default it is sanitized and won't be rendered
const isRenderHtmlEnabled = this.columnEditor && this.columnEditor.enableRenderHtml || false;
const isRenderHtmlEnabled = this.columnEditor?.enableRenderHtml ?? false;
return isRenderHtmlEnabled ? $elm.text() : $elm.html();
},
onClose: () => {
Expand Down Expand Up @@ -150,12 +150,12 @@ export class SelectEditor implements Editor {

/** Get the Collection */
get collection(): SelectOption[] {
return this.columnEditor && this.columnEditor.collection || [];
return this.columnEditor?.collection ?? [];
}

/** Getter for the Collection Options */
get collectionOptions(): CollectionOption | undefined {
return this.columnEditor && this.columnEditor.collectionOptions;
return this.columnEditor?.collectionOptions;
}

/** Get Column Definition object */
Expand All @@ -165,7 +165,7 @@ export class SelectEditor implements Editor {

/** Get Column Editor object */
get columnEditor(): ColumnEditor | undefined {
return this.columnDef && this.columnDef.internalColumnEditor || {};
return this.columnDef?.internalColumnEditor ?? {};
}

/** Getter for the Editor DOM Element */
Expand All @@ -175,7 +175,7 @@ export class SelectEditor implements Editor {

/** Getter for the Custom Structure if exist */
protected get customStructure(): CollectionCustomStructure | undefined {
return this.columnDef && this.columnDef.internalColumnEditor && this.columnDef.internalColumnEditor.customStructure;
return this.columnDef?.internalColumnEditor?.customStructure;
}

get hasAutoCommitEdit() {
Expand All @@ -194,8 +194,8 @@ export class SelectEditor implements Editor {
}

// collection of label/value pair
const separatorBetweenLabels = this.collectionOptions && this.collectionOptions.separatorBetweenTextLabels || '';
const isIncludingPrefixSuffix = this.collectionOptions && this.collectionOptions.includePrefixSuffixToSelectedValues || false;
const separatorBetweenLabels = this.collectionOptions?.separatorBetweenTextLabels ?? '';
const isIncludingPrefixSuffix = this.collectionOptions?.includePrefixSuffixToSelectedValues ?? false;

return this.collection
.filter(c => elmValue.indexOf(c.hasOwnProperty(this.valueName) && c[this.valueName]?.toString()) !== -1)
Expand All @@ -205,11 +205,13 @@ export class SelectEditor implements Editor {
let suffixText = c[this.labelSuffixName] || '';

// when it's a complex object, then pull the object name only, e.g.: "user.firstName" => "user"
const fieldName = this.columnDef && this.columnDef.field || '';
const fieldName = this.columnDef?.field ?? '';

// is the field a complex object, "address.streetNumber"
const isComplexObject = fieldName?.indexOf('.') > 0;
if (isComplexObject && typeof c === 'object') {
const serializeComplexValueFormat = this.columnEditor?.serializeComplexValueFormat ?? 'object';

if (isComplexObject && typeof c === 'object' && serializeComplexValueFormat === 'object') {
return c;
}

Expand Down Expand Up @@ -239,14 +241,15 @@ export class SelectEditor implements Editor {
}

// collection of label/value pair
const separatorBetweenLabels = this.collectionOptions && this.collectionOptions.separatorBetweenTextLabels || '';
const isIncludingPrefixSuffix = this.collectionOptions && this.collectionOptions.includePrefixSuffixToSelectedValues || false;
const separatorBetweenLabels = this.collectionOptions?.separatorBetweenTextLabels ?? '';
const isIncludingPrefixSuffix = this.collectionOptions?.includePrefixSuffixToSelectedValues ?? false;
const itemFound = findOrDefault(this.collection, (c: any) => c.hasOwnProperty(this.valueName) && c[this.valueName]?.toString() === elmValue);

// is the field a complex object, "address.streetNumber"
const isComplexObject = fieldName?.indexOf('.') > 0;
const serializeComplexValueFormat = this.columnEditor?.serializeComplexValueFormat ?? 'object';

if (isComplexObject && typeof itemFound === 'object') {
if (isComplexObject && typeof itemFound === 'object' && serializeComplexValueFormat === 'object') {
return itemFound;
} else if (itemFound && itemFound.hasOwnProperty(this.valueName)) {
const labelText = itemFound[this.valueName];
Expand Down Expand Up @@ -282,12 +285,12 @@ export class SelectEditor implements Editor {
}

this._collectionService = new CollectionService(this._translaterService);
this.enableTranslateLabel = this.columnEditor && this.columnEditor.enableTranslateLabel || false;
this.labelName = this.customStructure && this.customStructure.label || 'label';
this.labelPrefixName = this.customStructure && this.customStructure.labelPrefix || 'labelPrefix';
this.labelSuffixName = this.customStructure && this.customStructure.labelSuffix || 'labelSuffix';
this.optionLabel = this.customStructure && this.customStructure.optionLabel || 'value';
this.valueName = this.customStructure && this.customStructure.value || 'value';
this.enableTranslateLabel = this.columnEditor?.enableTranslateLabel ?? false;
this.labelName = this.customStructure?.label ?? 'label';
this.labelPrefixName = this.customStructure?.labelPrefix ?? 'labelPrefix';
this.labelSuffixName = this.customStructure?.labelSuffix ?? 'labelSuffix';
this.optionLabel = this.customStructure?.optionLabel ?? 'value';
this.valueName = this.customStructure?.value ?? 'value';

if (this.enableTranslateLabel && (!this._translaterService || typeof this._translaterService.translate !== 'function')) {
throw new Error('[Slickgrid-Universal] requires a Translate Service to be installed and configured when the grid option "enableTranslate" is enabled.');
Expand Down Expand Up @@ -365,13 +368,13 @@ export class SelectEditor implements Editor {

// validate the value before applying it (if not valid we'll set an empty string)
const validation = this.validate(null, newValue);
newValue = (validation && validation.valid) ? newValue : '';
newValue = (validation?.valid) ? newValue : '';

// set the new value to the item datacontext
if (isComplexObject) {
// when it's a complex object, user could override the object path (where the editable object is located)
// else we use the path provided in the Field Column Definition
const objectPath = this.columnEditor && this.columnEditor.complexObjectPath || fieldName || '';
const objectPath = this.columnEditor?.complexObjectPath ?? fieldName ?? '';
setDeepValue(item, objectPath, newValue);
} else {
item[fieldName] = newValue;
Expand Down Expand Up @@ -400,7 +403,7 @@ export class SelectEditor implements Editor {
if (item && fieldName !== undefined) {
// when it's a complex object, user could override the object path (where the editable object is located)
// else we use the path provided in the Field Column Definition
const objectPath = this.columnEditor && this.columnEditor.complexObjectPath || fieldName;
const objectPath = this.columnEditor?.complexObjectPath ?? fieldName;
const currentValue = (isComplexObject) ? getDescendantProperty(item, objectPath as string) : (item.hasOwnProperty(fieldName) && item[fieldName]);
const value = (isComplexObject && currentValue?.hasOwnProperty(this.valueName)) ? currentValue[this.valueName] : currentValue;

Expand Down Expand Up @@ -437,7 +440,7 @@ export class SelectEditor implements Editor {

save() {
const validation = this.validate();
const isValid = (validation && validation.valid) || false;
const isValid = validation?.valid ?? false;

if (!this._destroying && this.hasAutoCommitEdit && isValid) {
// do not use args.commitChanges() as this sets the focus to the next row.
Expand Down Expand Up @@ -558,7 +561,7 @@ export class SelectEditor implements Editor {
// user might want to filter certain items of the collection
if (this.columnEditor && this.columnEditor.collectionFilterBy) {
const filterBy = this.columnEditor.collectionFilterBy;
const filterCollectionBy = this.columnEditor.collectionOptions && this.columnEditor.collectionOptions.filterResultAfterEachPass || null;
const filterCollectionBy = this.columnEditor.collectionOptions?.filterResultAfterEachPass ?? null;
outputCollection = this._collectionService.filterCollection(outputCollection, filterBy, filterCollectionBy);
}

Expand Down Expand Up @@ -634,10 +637,10 @@ export class SelectEditor implements Editor {
/** Build the template HTML string */
protected buildTemplateHtmlString(collection: any[]): string {
let options = '';
const columnId = this.columnDef && this.columnDef.id || '';
const separatorBetweenLabels = this.collectionOptions && this.collectionOptions.separatorBetweenTextLabels || '';
const isRenderHtmlEnabled = this.columnEditor && this.columnEditor.enableRenderHtml || false;
const sanitizedOptions = this.gridOptions && this.gridOptions.sanitizeHtmlOptions || {};
const columnId = this.columnDef?.id ?? '';
const separatorBetweenLabels = this.collectionOptions?.separatorBetweenTextLabels ?? '';
const isRenderHtmlEnabled = this.columnEditor?.enableRenderHtml ?? false;
const sanitizedOptions = this.gridOptions?.sanitizeHtmlOptions ?? {};

// collection could be an Array of Strings OR Objects
if (collection.every((x: any) => typeof x === 'string')) {
Expand Down Expand Up @@ -714,7 +717,7 @@ export class SelectEditor implements Editor {
}

// add placeholder when found
const placeholder = this.columnEditor && this.columnEditor.placeholder || '';
const placeholder = this.columnEditor?.placeholder ?? '';
this.defaultOptions.placeholder = placeholder || '';

if (typeof this.$editorElm.multipleSelect === 'function') {
Expand Down
9 changes: 9 additions & 0 deletions packages/common/src/interfaces/columnEditor.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,15 @@ export interface ColumnEditor {
*/
required?: boolean;

/**
* defaults to 'object', how do we want to serialize the editor value to the resulting dataContext object when using a complex object?
* Currently only applies to Single/Multiple Select Editor.
*
* For example, if keep default "object" format and the selected value is { value: 2, label: 'Two' } then the end value will remain as an object, so { value: 2, label: 'Two' }.
* On the other end, if we set "flat" format and the selected value is { value: 2, label: 'Two' } then the end value will be 2.
*/
serializeComplexValueFormat?: 'flat' | 'object';

/**
* Title attribute that can be used in some Editors as tooltip (usually the "input" editors).
*
Expand Down
2 changes: 2 additions & 0 deletions packages/vanilla-bundle/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dist-grid-bundle-zip/*.zip
external-libs/*.zip
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,7 @@ describe('CompositeEditorService', () => {
const spyOnClose = jest.spyOn(mockModalOptions, 'onClose').mockReturnValue(Promise.resolve(true));
component = new SlickCompositeEditorComponent(gridStub, gridServiceStub, gridStateServiceStub);
component.openDetails(mockModalOptions);
component.editors = { field1: { setValue: jest.fn(), isValueChanged: () => true } as unknown as Editor }; // return True for value changed
gridStub.onCompositeEditorChange.notify({ row: 0, cell: 0, column: columnsMock[0], item: mockProduct, formValues: { field1: 'test' }, editors: {}, grid: gridStub });

const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement;
Expand Down Expand Up @@ -645,6 +646,7 @@ describe('CompositeEditorService', () => {
const mockModalOptions = { headerTitle: 'Details', modalType: 'create' } as CompositeEditorOpenDetailOption;
component = new SlickCompositeEditorComponent(gridStub, gridServiceStub, gridStateServiceStub);
component.openDetails(mockModalOptions);
component.editors = { field1: { setValue: jest.fn(), isValueChanged: () => true } as unknown as Editor }; // return True for value changed

const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement;
const compositeFooterElm = compositeContainerElm.querySelector('.slick-editor-modal-footer') as HTMLSelectElement;
Expand Down
Loading

0 comments on commit 4695cd3

Please sign in to comment.