Skip to content

Commit

Permalink
feat(editors): add Clone functionality to Composite Editor (#236)
Browse files Browse the repository at this point in the history
* feat(editors): add Clone functionality to Composite Editor
- clone allows you to select a row and clone it, the benefit is that it opens the Composite Editor with the selected row data context and then it allows you to edit anything in the form before doing the actual clone

* fix(editors): Composite Editor input change should process only once
- in some cases we could fall into an infinite loop if a field changes another field which changes the first field and we fall into inifinite... to resolve this, we can provide an extra paramater "triggeredBy" ("user" or "system") and the developer can change his code to make sure a system change doesn't run more than once to avoid the infinite loop from happening

* refactor: add proper onSave callbacks for the Clone modal type
- add new cloned item data context to the onSave
- add applyChangesCallback to the onSave
- add post callback code to the mdal type switch/case

* fix(formatters): Complex Object Formatter rename `complexFieldLabel`
- rename from `complexFieldLabel` to `complexField` but keep previous property as an alternative to avoid breaking anyone's code

* fix(metrics): refresh metrics also when providing new data to DataView

* refactor: add onSave to all modal type and remove applyChanges callback
- the applyChanges callback is kind of unnecessary, we could remove it. If developer wants to refresh the grid, it's his own decision and won't interfere with this applyChanges anyway. This way is better because we no longer require the user to call the applyChanges callback and we didn't want to rely on that for all the other create/clone/edit which can now also benefit from this

* fix(metrics): use onRowsOrCountChanged to refresh metrics

* fix(pinning): recalculate frozen idx properly when column shown changes
- when using ColumnPicker, GridMenu or hiding a column via HeaderMenu, it has to recalculate the frozenColumn index because SlickGrid doesn't take care of that and the previous fix that I implement sometimes become out of sync. This PR simplifies the frozenColumn index position, it will simply update it when the index is different, as simple as that.
  • Loading branch information
ghiscoding authored Jan 25, 2021
1 parent 3b55972 commit df545e4
Show file tree
Hide file tree
Showing 61 changed files with 1,315 additions and 771 deletions.
5 changes: 2 additions & 3 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,7 @@
"@typescript-eslint/no-this-alias": "error",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-unused-vars-experimental": "error",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/prefer-for-of": "off",
"@typescript-eslint/prefer-function-type": "error",
"@typescript-eslint/prefer-namespace-keyword": "error",
Expand Down Expand Up @@ -184,4 +183,4 @@
"use-isnan": "error",
"valid-typeof": "off"
}
}
}
3 changes: 2 additions & 1 deletion examples/webpack-demo-vanilla-bundle/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"CLEAR_ALL_GROUPING": "Clear all Grouping",
"CLEAR_ALL_SORTING": "Clear all Sorting",
"CLEAR_FROZEN_COLUMNS": "Clear Frozen Columns",
"CLONE": "Clone",
"COLLAPSE_ALL_GROUPS": "Collapse all Groups",
"COLUMNS": "Columns",
"COMMANDS": "Commands",
Expand Down Expand Up @@ -88,4 +89,4 @@
"TASK_X": "Task {{x}}",
"TITLE": "Title",
"TRUE": "True"
}
}
3 changes: 2 additions & 1 deletion examples/webpack-demo-vanilla-bundle/assets/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"CLEAR_ALL_GROUPING": "Supprimer tous les groupes",
"CLEAR_ALL_SORTING": "Supprimer tous les tris",
"CLEAR_FROZEN_COLUMNS": "Libérer les colonnes gelées",
"CLONE": "Cloner",
"COLLAPSE_ALL_GROUPS": "Réduire tous les groupes",
"COLUMNS": "Colonnes",
"COMMANDS": "Commandes",
Expand Down Expand Up @@ -89,4 +90,4 @@
"TITLE": "Titre",
"TITLE.NAME": "Nom du Titre",
"TRUE": "Vrai"
}
}
14 changes: 7 additions & 7 deletions examples/webpack-demo-vanilla-bundle/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,24 +35,24 @@
"devDependencies": {
"@types/jquery": "^3.5.5",
"@types/moment": "^2.13.0",
"@types/node": "^14.14.20",
"@types/webpack": "^4.41.25",
"@types/node": "^14.14.22",
"@types/webpack": "^4.41.26",
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^7.0.0",
"css-loader": "^5.0.1",
"file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "^6.1.0",
"html-loader": "^1.3.2",
"html-webpack-plugin": "next",
"mini-css-extract-plugin": "^1.3.3",
"mini-css-extract-plugin": "^1.3.4",
"node-sass": "5.0.0",
"sass-loader": "^10.1.0",
"sass-loader": "^10.1.1",
"style-loader": "^2.0.0",
"ts-loader": "^8.0.14",
"ts-node": "^9.1.1",
"url-loader": "^4.1.1",
"webpack": "^5.12.3",
"webpack-cli": "^4.3.1",
"webpack-dev-server": "^3.11.1"
"webpack": "^5.16.0",
"webpack-cli": "^4.4.0",
"webpack-dev-server": "^3.11.2"
}
}
17 changes: 12 additions & 5 deletions examples/webpack-demo-vanilla-bundle/src/examples/example12.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,29 +41,36 @@ <h3 class="title is-3">
<p class="control">
<button class="button is-small" onclick.delegate="openCompositeModal('create')"
data-test="open-modal-create-btn" disabled.bind="isCompositeDisabled">
<span class="icon mdi mdi-plus"></span>
<span>Open Item Create Modal</span>
<span class="icon mdi mdi-shape-square-plus"></span>
<span>Item Create</span>
</button>
</p>
<p class="control">
<button class="button is-small" onclick.delegate="openCompositeModal('clone')"
data-test="open-modal-clone-btn" disabled.bind="isCompositeDisabled">
<span class="icon mdi mdi-content-copy"></span>
<span>Item Clone</span>
</button>
</p>
<p class="control">
<button class="button is-small" onclick.delegate="openCompositeModal('edit')"
data-test="open-modal-edit-btn" disabled.bind="isCompositeDisabled">
<span class="icon mdi mdi-square-edit-outline"></span>
<span>Open Item Edit Modal</span>
<span>Item Edit Modal</span>
</button>
</p>
<p class="control">
<button class="button is-small" onclick.delegate="openCompositeModal('mass-update')"
data-test="open-modal-mass-update-btn" disabled.bind="isCompositeDisabled">
<span class="icon mdi mdi-pencil-box-multiple-outline"></span>
<span>Open Mass Update Modal</span>
<span>Mass Update</span>
</button>
</p>
<p class="control">
<button class="button is-small" onclick.delegate="openCompositeModal('mass-selection')"
data-test="open-modal-mass-selection-btn" disabled.bind="isMassSelectionDisabled">
<span class="icon mdi mdi-order-bool-ascending-variant"></span>
<span>Open Mass Selection Change</span>
<span>Update Selected</span>
</button>
</p>
</div>
Expand Down
77 changes: 57 additions & 20 deletions examples/webpack-demo-vanilla-bundle/src/examples/example12.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
Formatters,
GridOption,
LongTextEditorOption,
OnCompositeEditorChangeEventArgs,
SlickNamespace,
SortComparers,

Expand Down Expand Up @@ -187,13 +188,21 @@ export class Example12 {
// collectionOptions: {
// addCustomFirstEntry: { value: '', label: '--none--' }
// },
// collectionOverride: (_collectionInput, args) => {
// const originalCollection = args.originalCollections || [];
// const duration = args?.dataContext?.duration ?? args?.compositeEditorOptions?.formValues?.duration;
// if (duration === 10) {
// return originalCollection.filter(itemCollection => +itemCollection.value !== 1);
// }
// return originalCollection;
// },
// massUpdate: true, minValue: 0, maxValue: 100,
// },
// },
{
id: 'start', name: 'Start', field: 'start', sortable: true, minWidth: 100,
formatter: Formatters.dateUs, columnGroup: 'Period',
type: FieldType.dateIso, outputType: FieldType.dateUs,
type: FieldType.dateUs, outputType: FieldType.dateUs,
filterable: true, filter: { model: Filters.compoundDate },
editor: { model: Editors.date, massUpdate: true, params: { hideClearButton: false } },
},
Expand All @@ -213,7 +222,7 @@ export class Example12 {
{
id: 'finish', name: 'Finish', field: 'finish', sortable: true, minWidth: 100,
formatter: Formatters.dateUs, columnGroup: 'Period',
type: FieldType.dateIso, outputType: FieldType.dateUs,
type: FieldType.dateUs, outputType: FieldType.dateUs,
filterable: true, filter: { model: Filters.compoundDate },
editor: {
model: Editors.date,
Expand Down Expand Up @@ -428,19 +437,21 @@ export class Example12 {
const randomTime = Math.floor((Math.random() * 59));
const randomFinish = new Date(randomFinishYear, (randomMonth + 1), randomDay, randomTime, randomTime, randomTime);
const randomPercentComplete = Math.floor(Math.random() * 100) + 15; // make it over 15 for E2E testing purposes
const percentCompletion = randomPercentComplete > 100 ? (i > 5 ? 100 : 88) : randomPercentComplete; // don't use 100 unless it's over index 5, for E2E testing purposes
const isCompleted = percentCompletion === 100;

tmpArray[i] = {
id: i,
title: 'Task ' + i,
duration: Math.floor(Math.random() * 100) + 10,
percentComplete: randomPercentComplete > 100 ? 100 : randomPercentComplete,
percentComplete: percentCompletion,
analysis: {
percentComplete: randomPercentComplete > 100 ? 100 : randomPercentComplete,
percentComplete: percentCompletion,
},
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
finish: (isCompleted || (i % 3 === 0 && (randomFinish > new Date() && i > 3)) ? (isCompleted ? new Date() : 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,
completed: (i % 3 === 0 && (randomFinish > new Date() && i > 3)),
completed: (isCompleted || (i % 3 === 0 && (randomFinish > new Date() && i > 3))),
product: { id: this.mockProducts()[randomItemId]?.id, itemName: this.mockProducts()[randomItemId]?.itemName, },
origin: (i % 2) ? { code: 'CA', name: 'Canada' } : { code: 'US', name: 'United States' },
};
Expand Down Expand Up @@ -516,15 +527,27 @@ export class Example12 {
}

handleOnCompositeEditorChange(event) {
const args = event && event.detail && event.detail.args;
const columnDef = args?.column;
const formValues = args?.formValues;
const args = event.detail.args as OnCompositeEditorChangeEventArgs;
const columnDef = args.column as Column;
const formValues = args.formValues;

// you can dynamically change a select dropdown collection,
// if you need to re-render the editor for the list to be reflected
// if (columnDef.id === 'duration') {
// const editor = this.compositeEditorInstance.editors['percentComplete2'] as SelectEditor;
// const newCollection = editor.finalCollection;
// editor.renderDomElement(newCollection);
// }

// you can change any other form input values when certain conditions are met
if (columnDef.id === 'percentComplete' && formValues.percentComplete === 100) {
this.compositeEditorInstance.changeFormInputValue('completed', true);
this.compositeEditorInstance.changeFormInputValue('finish', new Date());
// this.compositeEditorInstance.changeFormInputValue('product', { id: 0, itemName: 'Sleek Metal Computer' });

// you can even change a value that is not part of the form (but is part of the grid)
// but you will have to bypass the error thrown by providing `true` as the 3rd argument
// this.compositeEditorInstance.changeFormInputValue('cost', 9999.99, true);
}

// you can also change some editor options (not all Editors supports this functionality, so far only these Editors AutoComplete, Date MultipleSelect & SingleSelect)
Expand Down Expand Up @@ -874,6 +897,9 @@ export class Example12 {
case 'create':
modalTitle = 'Inserting New Task';
break;
case 'clone':
modalTitle = 'Clone - {{title}}';
break;
case 'edit':
modalTitle = 'Editing - {{title}} (<span class="color-muted">id:</span> <span class="color-primary">{{id}}</span>)'; // 'Editing - {{title}} ({{product.itemName}})'
break;
Expand All @@ -894,17 +920,28 @@ export class Example12 {
// viewColumnLayout: 2, // choose from 'auto', 1, 2, or 3 (defaults to 'auto')
onClose: () => Promise.resolve(confirm('You have unsaved changes, are you sure you want to close this window?')),
onError: (error) => alert(error.message),
onSave: (formValues, selection, applyChangesCallback) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (formValues.percentComplete > 50) {
applyChangesCallback(formValues, selection);
resolve(true);
} else {
reject('Unfortunately we only accept a minimum of 50% Completion...');
}
}, 250);
});
onSave: (formValues, _selection, dataContext) => {
const serverResponseDelay = 250;

// simulate a backend server call which will reject if the "% Complete" is below 50%
// when processing a mass update or mass selection
if (modalType === 'mass-update' || modalType === 'mass-selection') {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (formValues.percentComplete >= 50) {
resolve(true);
} else {
reject('Unfortunately we only accept a minimum of 50% Completion...');
}
}, serverResponseDelay);
});
} else {
// also simulate a server cal for any other modal type (create/clone/edit)
// we'll just apply the change without any rejection from the server and
// note that we also have access to the "dataContext" which is only available for these modal
console.log(`new ${modalType}d item`, dataContext);
return new Promise(resolve => setTimeout(() => resolve(true), serverResponseDelay));
}
}
});
}, openDelay);
Expand Down
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@
},
"devDependencies": {
"@types/jest": "^26.0.20",
"@types/node": "^14.14.20",
"@typescript-eslint/eslint-plugin": "^4.12.0",
"@typescript-eslint/parser": "^4.12.0",
"cypress": "^6.2.1",
"eslint": "^7.17.0",
"@types/node": "^14.14.22",
"@typescript-eslint/eslint-plugin": "^4.14.0",
"@typescript-eslint/parser": "^4.14.0",
"cypress": "^6.3.0",
"eslint": "^7.18.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-prefer-arrow": "^1.2.2",
"http-server": "^0.12.3",
Expand Down
6 changes: 3 additions & 3 deletions packages/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,13 @@
"slickgrid": "^2.4.32"
},
"devDependencies": {
"@types/dompurify": "^2.2.0",
"@types/dompurify": "^2.2.1",
"@types/jquery": "^3.5.5",
"@types/moment": "^2.13.0",
"autoprefixer": "^10.2.1",
"autoprefixer": "^10.2.3",
"copyfiles": "^2.4.1",
"cross-env": "^7.0.3",
"mini-css-extract-plugin": "^1.3.3",
"mini-css-extract-plugin": "^1.3.4",
"node-sass": "5.0.0",
"nodemon": "^2.0.7",
"npm-run-all": "^4.1.5",
Expand Down
3 changes: 2 additions & 1 deletion packages/common/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export class Constants {
TEXT_CLEAR_ALL_GROUPING: 'Clear all Grouping',
TEXT_CLEAR_ALL_SORTING: 'Clear all Sorting',
TEXT_CLEAR_FROZEN_COLUMNS: 'Clear Frozen Columns',
TEXT_CLONE: 'Clone',
TEXT_COLLAPSE_ALL_GROUPS: 'Collapse all Groups',
TEXT_CONTAINS: 'Contains',
TEXT_COLUMNS: 'Columns',
Expand All @@ -23,7 +24,7 @@ export class Constants {
TEXT_ERROR_ENABLE_CELL_NAVIGATION_REQUIRED: 'Composite Editor requires the flag "enableCellNavigation" to be set to True in your Grid Options.',
TEXT_ERROR_NO_CHANGES_DETECTED: 'Sorry we could not detect any changes.',
TEXT_ERROR_NO_EDITOR_FOUND: 'We could not find any Editor in your Column Definition.',
TEXT_ERROR_NO_RECORD_FOUND: 'No records selected for edit operation.',
TEXT_ERROR_NO_RECORD_FOUND: 'No records selected for edit or clone operation.',
TEXT_ERROR_ROW_NOT_EDITABLE: 'Current row is not editable.',
TEXT_ERROR_ROW_SELECTION_REQUIRED: 'You must select some rows before trying to apply new value(s).',
TEXT_EXPAND_ALL_GROUPS: 'Expand all Groups',
Expand Down
28 changes: 24 additions & 4 deletions packages/common/src/editors/__tests__/autoCompleteEditor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -801,7 +801,7 @@ describe('AutoCompleteEditor', () => {
expect(editor.getValue()).toBe('Male');
expect(onCompositeEditorSpy).toHaveBeenCalledWith({
...activeCellMock, column: mockColumn, item: mockItemData, grid: gridStub,
formValues: { gender: 'male' }, editors: {},
formValues: { gender: 'male' }, editors: {}, triggeredBy: 'system',
}, expect.anything());
});

Expand Down Expand Up @@ -834,7 +834,7 @@ describe('AutoCompleteEditor', () => {
expect(onBeforeEditSpy).toHaveBeenCalledWith({ ...activeCellMock, column: mockColumn, item: mockItemData, grid: gridStub });
expect(onCompositeEditorSpy).toHaveBeenCalledWith({
...activeCellMock, column: mockColumn, item: mockItemData, grid: gridStub,
formValues: { gender: '' }, editors: {},
formValues: { gender: '' }, editors: {}, triggeredBy: 'user',
}, expect.anything());
expect(disableSpy).toHaveBeenCalledWith(true);
expect(editor.editorDomElement.attr('disabled')).toEqual('disabled');
Expand Down Expand Up @@ -879,7 +879,7 @@ describe('AutoCompleteEditor', () => {
expect(getCellSpy).toHaveBeenCalled();
expect(onCompositeEditorSpy).toHaveBeenCalledWith({
...activeCellMock, column: mockColumn, item: mockItemData, grid: gridStub,
formValues: {}, editors: {},
formValues: {}, editors: {}, triggeredBy: 'user',
}, expect.anything());
expect(editor.editorDomElement.attr('disabled')).toEqual('disabled');
expect(editor.editorDomElement.val()).toEqual('');
Expand All @@ -904,8 +904,28 @@ describe('AutoCompleteEditor', () => {
expect(onBeforeEditSpy).toHaveBeenCalledWith({ ...activeCellMock, column: mockColumn, item: mockItemData, grid: gridStub });
expect(onCompositeEditorSpy).toHaveBeenCalledWith({
...activeCellMock, column: mockColumn, item: mockItemData, grid: gridStub,
formValues: { gender: 'female' }, editors: {},
formValues: { gender: 'female' }, editors: {}, triggeredBy: 'user',
}, expect.anything());
});

describe('collectionOverride callback option', () => {
it('should create the editor and expect a different collection outputed when using the override', () => {
const activeCellMock = { row: 0, cell: 0 };
jest.spyOn(gridStub, 'getActiveCell').mockReturnValue(activeCellMock);
const onCompositeEditorSpy = jest.spyOn(gridStub.onCompositeEditorChange, 'notify').mockReturnValue(false);
mockColumn.internalColumnEditor = {
collection: ['Other', 'Male', 'Female'],
collectionOverride: (inputCollection) => inputCollection.filter(item => item !== 'other')
};
editor = new AutoCompleteEditor(editorArguments);
editor.setValue('Male', true);

expect(editor.getValue()).toBe('Male');
expect(onCompositeEditorSpy).toHaveBeenCalledWith({
...activeCellMock, column: mockColumn, item: mockItemData, grid: gridStub,
formValues: { gender: 'Male' }, editors: {}, triggeredBy: 'system',
}, expect.anything());
});
});
});
});
Loading

0 comments on commit df545e4

Please sign in to comment.