diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example12.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example12.ts index c7fa25ee0..7dd228df1 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example12.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example12.ts @@ -141,7 +141,7 @@ export class Example12 { initializeGrid() { this.columnDefinitions = [ { - id: 'title', name: 'Title', field: 'title', sortable: true, type: FieldType.string, minWidth: 75, + id: 'title', name: ' Title', field: 'title', sortable: true, type: FieldType.string, minWidth: 75, filterable: true, columnGroup: 'Common Factor', filter: { model: Filters.compoundInputText }, formatter: Formatters.multiple, params: { formatters: [Formatters.uppercase, Formatters.bold] }, @@ -943,29 +943,39 @@ export class Example12 { // backdrop: null, // viewColumnLayout: 2, // responsive layout, choose from 'auto', 1, 2, or 3 (defaults to 'auto') showFormResetButton: true, + + // you can validate each row item dataContext before apply Mass Update/Selection changes via this validation callback (returning false would skip the change) + // validateMassUpdateChange: (fieldName, dataContext, formValues) => { + // const levelComplex = this.complexityLevelList.find(level => level.label === 'Complex'); + // if (fieldName === 'duration' && (dataContext.complexity === levelComplex?.value || formValues.complexity === levelComplex?.value) && formValues.duration < 5) { + // // not good, do not apply the change because when it's "Complex", we assume the user has to be choose at least 5 days of work (duration) + // return false; + // } + // return true; + // }, + // showResetButtonOnEachEditor: true, 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, dataContext) => { + onSave: (formValues, _selection, dataContextOrUpdatedDatasetPreview) => { const serverResponseDelay = 50; - // 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') { + console.log(`${modalType} dataset preview`, dataContextOrUpdatedDatasetPreview); + + // simulate a backend server call which will reject if the "% Complete" is below 50% return new Promise((resolve, reject) => { - setTimeout(() => { - if (formValues.percentComplete >= 50) { - resolve(true); - } else { - reject('Unfortunately we only accept a minimum of 50% Completion...'); - } - }, serverResponseDelay); + setTimeout( + () => (formValues.percentComplete >= 50) ? resolve(true) : 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(`${modalType} item data context`, dataContext); + console.log(`${modalType} item data context`, dataContextOrUpdatedDatasetPreview); return new Promise(resolve => setTimeout(() => resolve(true), serverResponseDelay)); } } diff --git a/packages/common/src/interfaces/compositeEditorOpenDetailOption.interface.ts b/packages/common/src/interfaces/compositeEditorOpenDetailOption.interface.ts index 4bcbb7d0f..e33ec7866 100644 --- a/packages/common/src/interfaces/compositeEditorOpenDetailOption.interface.ts +++ b/packages/common/src/interfaces/compositeEditorOpenDetailOption.interface.ts @@ -65,6 +65,13 @@ export interface CompositeEditorOpenDetailOption { /** Optionally provide a CSS class by the form reset button */ resetFormButtonIconCssClass?: string; + /** + * Defaults to false, do we want to provide a preview of what the dataset with the applied Mass changes (works for both Mass Update and/or Mass Selection)? + * If set to true, then it would provide a 4th argument to the `onSave` callback even before sending the data to the server. + * This could be useful to actually use this dataset preview to send directly to the backend server. + */ + shouldPreviewMassChangeDataset?: boolean; + /** Defaults to true, do we want the close button outside the modal (true) or inside the header modal (false)? */ showCloseButtonOutside?: boolean; @@ -84,6 +91,10 @@ export interface CompositeEditorOpenDetailOption { */ viewColumnLayout?: 1 | 2 | 3 | 'auto'; + // --------- + // Methods + // --------- + /** onBeforeOpen callback allows the user to optionally execute something before opening the modal (for example cancel any batch edits, or change/reset some validations in column definitions) */ onBeforeOpen?: () => void; @@ -107,7 +118,23 @@ export interface CompositeEditorOpenDetailOption { /** current selection of row indexes & data context Ids */ selection: CompositeEditorSelection, - /** optional item data context that is returned, this is only provided when the modal type is (clone, create or edit) */ - dataContext?: any + /** + * optional item data context when the modal type is (clone, create or edit) + * OR a preview of the updated dataset when modal type is (mass-update or mass-selection). + * NOTE: the later requires `shouldPreviewMassChangeDataset` to be enabled since it could be resource heavy with large dataset. + */ + dataContextOrUpdatedDatasetPreview?: any | any[], ) => Promise; + + /** + * Optional callback that the user can add before applying the change to all item rows, + * if this callback returns False then the change will NOT be applied to the given field, + * or if on the other end it returns True or `undefined` then it assumes that it is valid and it should apply the change to the item dataContext. + * This callback works for both Mass Selection & Mass Update. + * @param {String} fieldName - field property name being validated + * @param {*} dataContext - item object data context + * @param {*} formValues - all form input and values that were changed + * @returns {Boolean} - returning False means we can't apply the change, else we go ahead and apply the change + */ + validateMassUpdateChange?: (fieldName: string, dataContext: any, formValues: any) => boolean; } diff --git a/packages/composite-editor-component/src/slick-composite-editor.component.spec.ts b/packages/composite-editor-component/src/slick-composite-editor.component.spec.ts index 4f4064703..6280f8bb4 100644 --- a/packages/composite-editor-component/src/slick-composite-editor.component.spec.ts +++ b/packages/composite-editor-component/src/slick-composite-editor.component.spec.ts @@ -1,3 +1,4 @@ +import 'jest-extended'; import { Column, CompositeEditorOpenDetailOption, @@ -336,7 +337,8 @@ describe('CompositeEditorService', () => { expect(compositeContainerElm).toBeFalsy(); }); - it('should make sure Slick-Composite-Editor is being created and rendered with 1 column layout', () => { + it('should make sure Slick-Composite-Editor is being created and rendered with 1 column layout & also expect column name html to be rendered as well', () => { + columnsMock[2].name = ' Field 3'; // add tooltip const mockProduct = { id: 222, address: { zip: 123456 }, productName: 'Product ABC', price: 12.55 }; jest.spyOn(gridStub, 'getDataItem').mockReturnValue(mockProduct); @@ -361,7 +363,7 @@ describe('CompositeEditorService', () => { expect(compositeContainerElm).toBeTruthy(); expect(compositeHeaderElm).toBeTruthy(); expect(productNameLabelElm.textContent).toBe('Product'); // regular, without column group - expect(field3LabelElm.textContent).toBe('Group Name - Field 3'); // with column group + expect(field3LabelElm.innerHTML).toBe('Group Name - Field 3'); // with column group expect(compositeTitleElm).toBeTruthy(); expect(compositeTitleElm.textContent).toBe('Details'); expect(compositeBodyElm).toBeTruthy(); @@ -369,6 +371,9 @@ describe('CompositeEditorService', () => { expect(compositeFooterCancelBtnElm).toBeTruthy(); expect(compositeFooterSaveBtnElm).toBeTruthy(); expect(productNameDetailContainerElm).toBeTruthy(); + + // reset Field 3 column name + columnsMock[2].name = 'Field 3'; }); it('should make sure Slick-Composite-Editor is being created and expect form inputs to be in specific order when user provides column def "compositeEditorFormOrder"', () => { @@ -546,7 +551,7 @@ describe('CompositeEditorService', () => { }); it('should execute "onClose" callback when user confirms the closing of the modal when "onClose" callback is defined', (done) => { - const mockProduct = { id: 222, address: { zip: 123456 }, productName: 'Product ABC', price: 12.55 }; + const mockProduct = { id: 222, address: { zip: 123456 }, productName: 'Product ABC', price: 12.55, field2: 'Test' }; jest.spyOn(gridStub, 'getDataItem').mockReturnValue(mockProduct); const getEditSpy = jest.spyOn(gridStub, 'getEditController'); const cancelSpy = jest.spyOn(gridStub.getEditController(), 'cancelCurrentEdit'); @@ -1183,6 +1188,11 @@ describe('CompositeEditorService', () => { }); describe('Form Logics', () => { + beforeEach(() => { + // reset Field 3 column name + columnsMock[2].name = 'Field 3'; + }); + it('should make sure Slick-Composite-Editor is being created and then call "changeFormInputValue" to change dynamically any of the form input value', () => { const mockEditor = { setValue: jest.fn(), disable: jest.fn(), } as unknown as Editor; const mockProduct = { id: 222, address: { zip: 123456 }, productName: 'Product ABC', price: 12.55 }; @@ -1782,6 +1792,64 @@ describe('CompositeEditorService', () => { }); }); + it('should handle saving and expect a dataset preview of the change when "shouldPreviewMassChangeDataset" is enabled and grid changes when "Mass Update" save button is clicked and user provides a custom "onSave" async function', (done) => { + const mockProduct1 = { id: 222, field3: 'something', address: { zip: 123456 }, product: { name: 'Product ABC', price: 12.55 } }; + const mockProduct2 = { id: 333, field3: 'else', address: { zip: 789123 }, product: { name: 'Product XYZ', price: 33.44 } }; + const currentEditorMock = { validate: jest.fn() }; + jest.spyOn(dataViewStub, 'getItems').mockReturnValue([mockProduct1, mockProduct2]); + jest.spyOn(gridStub, 'getCellEditor').mockReturnValue(currentEditorMock as any); + jest.spyOn(currentEditorMock, 'validate').mockReturnValue({ valid: true, msg: null }); + jest.spyOn(gridStateServiceStub, 'getCurrentRowSelections').mockReturnValue({ gridRowIndexes: [0], dataContextIds: [222] }); + const getEditSpy = jest.spyOn(gridStub, 'getEditController'); + const cancelCommitSpy = jest.spyOn(gridStub.getEditController(), 'cancelCurrentEdit'); + const setActiveCellSpy = jest.spyOn(gridStub, 'setActiveCell'); + const clearSelectionSpy = jest.spyOn(gridStub, 'setSelectedRows'); + const setItemsSpy = jest.spyOn(dataViewStub, 'setItems'); + + const mockOnSave = jest.fn(); + mockOnSave.mockResolvedValue(Promise.resolve(true)); + const mockModalOptions = { headerTitle: 'Details', modalType: 'mass-update', onSave: mockOnSave, shouldClearRowSelectionAfterMassAction: false, shouldPreviewMassChangeDataset: true } as CompositeEditorOpenDetailOption; + component = new SlickCompositeEditorComponent(); + component.init(gridStub, container); + component.openDetails(mockModalOptions); + + const compositeContainerElm = document.querySelector('div.slick-editor-modal.slickgrid_123456') as HTMLSelectElement; + const compositeFooterSaveBtnElm = compositeContainerElm.querySelector('.btn-save') as HTMLSelectElement; + const compositeHeaderElm = document.querySelector('.slick-editor-modal-header') as HTMLSelectElement; + const compositeTitleElm = compositeHeaderElm.querySelector('.slick-editor-modal-title') as HTMLSelectElement; + const compositeBodyElm = document.querySelector('.slick-editor-modal-body') as HTMLSelectElement; + const field3DetailContainerElm = compositeBodyElm.querySelector('.item-details-container.editor-field3.slick-col-medium-12') as HTMLSelectElement; + const field3LabelElm = field3DetailContainerElm.querySelector('.item-details-label.editor-field3') as HTMLSelectElement; + const validationSummaryElm = compositeContainerElm.querySelector('.validation-summary') as HTMLSelectElement; + + gridStub.onCompositeEditorChange.notify({ row: 0, cell: 0, column: columnsMock[0], item: mockProduct1, formValues: { field3: 'test' }, editors: {}, grid: gridStub }); + + compositeFooterSaveBtnElm.click(); + + setTimeout(() => { + expect(component).toBeTruthy(); + expect(component.constructor).toBeDefined(); + expect(compositeContainerElm).toBeTruthy(); + expect(compositeHeaderElm).toBeTruthy(); + expect(compositeTitleElm).toBeTruthy(); + expect(compositeTitleElm.textContent).toBe('Details'); + expect(field3LabelElm.textContent).toBe('Group Name - Field 3'); + expect(getEditSpy).toHaveBeenCalledTimes(2); + expect(mockOnSave).toHaveBeenCalledWith( + { field3: 'test' }, + { gridRowIndexes: [0], dataContextIds: [222] }, + [{ address: { zip: 123456 }, field3: 'test', id: 222, product: { name: 'Product ABC', price: 12.55 } }, { address: { zip: 789123 }, field3: 'test', id: 333, product: { name: 'Product XYZ', price: 33.44 } }] + ); + expect(setItemsSpy).toHaveBeenCalled(); + expect(cancelCommitSpy).toHaveBeenCalled(); + expect(setActiveCellSpy).toHaveBeenCalledWith(0, 0, false); + expect(validationSummaryElm.style.display).toBe('none'); + expect(validationSummaryElm.textContent).toBe(''); + expect(clearSelectionSpy).not.toHaveBeenCalled(); // shouldClearRowSelectionAfterMassAction is false + done(); + }); + }); + it('should show a validation summary when clicking "Mass Update" save button and the custom "onSave" async function throws an error', (done) => { const mockProduct1 = { id: 222, field3: 'something', address: { zip: 123456 }, product: { name: 'Product ABC', price: 12.55 } }; const mockProduct2 = { id: 333, field3: 'else', address: { zip: 789123 }, product: { name: 'Product XYZ', price: 33.44 } }; diff --git a/packages/composite-editor-component/src/slick-composite-editor.component.ts b/packages/composite-editor-component/src/slick-composite-editor.component.ts index a31975555..86a0d114e 100644 --- a/packages/composite-editor-component/src/slick-composite-editor.component.ts +++ b/packages/composite-editor-component/src/slick-composite-editor.component.ts @@ -44,8 +44,14 @@ const DEFAULT_ON_ERROR = (error: OnErrorOption) => console.log(error.message); type ApplyChangesCallbackFn = ( formValues: { [columnId: string]: any; } | null, - selection: { gridRowIndexes: number[]; dataContextIds: Array; } -) => void; + selection: { gridRowIndexes: number[]; dataContextIds: Array; }, + applyToDataview?: boolean, +) => any[] | void | undefined; + +type DataSelection = { + gridRowIndexes: number[]; + dataContextIds: Array; +}; export class SlickCompositeEditorComponent implements ExternalResource { protected _bindEventService: BindingEventService; @@ -454,7 +460,7 @@ export class SlickCompositeEditorComponent implements ExternalResource { const templateItemLabelElm = createDomElement('div', { className: `item-details-label editor-${columnDef.id}`, - textContent: this.getColumnLabel(columnDef) || 'n/a' + innerHTML: sanitizeTextByAvailableSanitizer(this.gridOptions, this.getColumnLabel(columnDef) || 'n/a') }); const templateItemEditorElm = createDomElement('div', { className: 'item-details-editor-container slick-cell', @@ -565,15 +571,16 @@ export class SlickCompositeEditorComponent implements ExternalResource { // ---------------- /** Apply Mass Update Changes (form values) to the entire dataset */ - protected applySaveMassUpdateChanges(formValues: any) { - const data = this.dataView.getItems(); + protected applySaveMassUpdateChanges(formValues: any, _selection: DataSelection, applyToDataview = true): any[] { + // not applying to dataView means that we're doing a preview of dataset and we should use a deep copy of it instead of applying changes directly to it + const data = applyToDataview ? this.dataView.getItems() : deepCopy(this.dataView.getItems()); // from the "lastCompositeEditor" object that we kept as reference, it contains all the changes inside the "formValues" property // we can loop through these changes and apply them on the selected row indexes for (const itemProp in formValues) { if (itemProp in formValues) { - data.forEach(dataContext => { - if (itemProp in formValues) { + data.forEach((dataContext: any) => { + if (itemProp in formValues && (this._options?.validateMassUpdateChange === undefined || this._options.validateMassUpdateChange(itemProp, dataContext, formValues) !== false)) { dataContext[itemProp] = formValues[itemProp]; } }); @@ -581,21 +588,27 @@ export class SlickCompositeEditorComponent implements ExternalResource { } // change the entire dataset with our updated dataset - this.dataView.setItems(data, this.gridOptions.datasetIdPropertyName); - this.grid.invalidate(); + if (applyToDataview) { + this.dataView.setItems(data, this.gridOptions.datasetIdPropertyName); + this.grid.invalidate(); + } + return data; } /** Apply Mass Changes to the Selected rows in the grid (form values) */ - protected applySaveMassSelectionChanges(formValues: any, selection: { gridRowIndexes: number[]; dataContextIds: Array; }) { + protected applySaveMassSelectionChanges(formValues: any, selection: DataSelection, applyToDataview = true): any[] { const selectedItemIds = selection?.dataContextIds ?? []; - const selectedItems = selectedItemIds.map(itemId => this.dataView.getItemById(itemId)); + const selectedTmpItems = selectedItemIds.map(itemId => this.dataView.getItemById(itemId)); + + // not applying to dataView means that we're doing a preview of dataset and we should use a deep copy of it instead of applying changes directly to it + const selectedItems = applyToDataview ? selectedTmpItems : deepCopy(selectedTmpItems); // from the "lastCompositeEditor" object that we kept as reference, it contains all the changes inside the "formValues" property // we can loop through these changes and apply them on the selected row indexes for (const itemProp in formValues) { if (itemProp in formValues) { - selectedItems.forEach(dataContext => { - if (itemProp in formValues) { + selectedItems.forEach((dataContext: any) => { + if (itemProp in formValues && (this._options?.validateMassUpdateChange === undefined || this._options.validateMassUpdateChange(itemProp, dataContext, formValues) !== false)) { dataContext[itemProp] = formValues[itemProp]; } }); @@ -603,7 +616,10 @@ export class SlickCompositeEditorComponent implements ExternalResource { } // update all items in the grid with the grid service - this.gridService?.updateItems(selectedItems); + if (applyToDataview) { + this.gridService?.updateItems(selectedItems); + } + return selectedItems; } /** @@ -675,7 +691,7 @@ export class SlickCompositeEditorComponent implements ExternalResource { * @param {Function} applyChangesCallback - first callback to apply the changes into the grid (this could be a user custom callback) * @param {Function} executePostCallback - second callback to execute right after the "onSave" * @param {Function} beforeClosingCallback - third and last callback to execute after Saving but just before closing the modal window - * @param {Object} itemDataContext - item data context, only provided for modal type (create/clone/edit) + * @param {Object} itemDataContext - item data context when modal type is (create/clone/edit) */ protected async executeOnSave(applyChangesCallback: ApplyChangesCallbackFn, executePostCallback: PlainFunc, beforeClosingCallback?: PlainFunc, itemDataContext?: any) { try { @@ -687,11 +703,19 @@ export class SlickCompositeEditorComponent implements ExternalResource { this._modalSaveButtonElm.disabled = true; if (typeof this._options?.onSave === 'function') { + const isMassChange = (this._options.modalType === 'mass-update' || this._options.modalType === 'mass-selection'); + + // apply the changes in the grid early when that option is enabled (that is before the await of `onSave`) + let updatedDataset; + if (isMassChange && this._options?.shouldPreviewMassChangeDataset) { + updatedDataset = applyChangesCallback(this.formValues, this.getCurrentRowSelections(), false) as any[]; + } // call the custon onSave callback when defined and note that the item data context will only be filled for create/clone/edit - const successful = await this._options?.onSave(this.formValues, this.getCurrentRowSelections(), itemDataContext); + const dataContextOrUpdatedDatasetPreview = isMassChange ? updatedDataset : itemDataContext; + const successful = await this._options?.onSave(this.formValues, this.getCurrentRowSelections(), dataContextOrUpdatedDatasetPreview); if (successful) { - // apply the changes in the grid + // apply the changes in the grid (if it's not yet applied) applyChangesCallback(this.formValues, this.getCurrentRowSelections()); // once we're done doing the mass update, we can cancel the current editor since we don't want to add any new row diff --git a/packages/vanilla-force-bundle/dist-grid-bundle-zip/slickgrid-vanilla-bundle.zip b/packages/vanilla-force-bundle/dist-grid-bundle-zip/slickgrid-vanilla-bundle.zip index 7038c204b..dc4bcfc9a 100644 Binary files a/packages/vanilla-force-bundle/dist-grid-bundle-zip/slickgrid-vanilla-bundle.zip and b/packages/vanilla-force-bundle/dist-grid-bundle-zip/slickgrid-vanilla-bundle.zip differ diff --git a/packages/vanilla-force-bundle/src/salesforce-global-grid-options.ts b/packages/vanilla-force-bundle/src/salesforce-global-grid-options.ts index 8df42d9fb..42ec48f26 100644 --- a/packages/vanilla-force-bundle/src/salesforce-global-grid-options.ts +++ b/packages/vanilla-force-bundle/src/salesforce-global-grid-options.ts @@ -10,12 +10,9 @@ export const SalesforceGlobalGridOptions = { cellValueCouldBeUndefined: true, eventNamingStyle: EventNamingStyle.lowerCaseWithoutOnPrefix, compositeEditorOptions: { - labels: { - massSelectionButton: 'Apply to Selected & Save', - massUpdateButton: 'Apply to All & Save' - }, resetEditorButtonCssClass: 'mdi mdi-refresh mdi-15px mdi-v-align-text-top', - resetFormButtonIconCssClass: 'mdi mdi-refresh mdi-16px mdi-flip-h mdi-v-align-text-top' + resetFormButtonIconCssClass: 'mdi mdi-refresh mdi-16px mdi-flip-h mdi-v-align-text-top', + shouldPreviewMassChangeDataset: true, }, datasetIdPropertyName: 'Id', emptyDataWarning: { diff --git a/test/cypress/integration/example12.spec.js b/test/cypress/integration/example12.spec.js index fed65e344..730f0ee50 100644 --- a/test/cypress/integration/example12.spec.js +++ b/test/cypress/integration/example12.spec.js @@ -3,7 +3,7 @@ import { changeTimezone, zeroPadding } from '../plugins/utilities'; describe('Example 12 - Composite Editor Modal', { retries: 1 }, () => { const fullPreTitles = ['', 'Common Factor', 'Analysis', 'Period', 'Item', '']; - const fullTitles = ['', 'Title', 'Duration', 'Cost', '% Complete', 'Complexity', 'Start', 'Completed', 'Finish', 'Product', 'Country of Origin', 'Action']; + const fullTitles = ['', ' Title', 'Duration', 'Cost', '% Complete', 'Complexity', 'Start', 'Completed', 'Finish', 'Product', 'Country of Origin', 'Action']; const GRID_ROW_HEIGHT = 33; const UNSAVED_RGB_COLOR = 'rgb(221, 219, 218)'; @@ -336,14 +336,14 @@ describe('Example 12 - Composite Editor Modal', { retries: 1 }, () => { cy.get('.item-details-container.editor-origin .modified').should('have.length', 1); cy.get('.item-details-container.editor-origin .autocomplete').invoke('val').then(text => expect(text).to.eq('Belgium')); - cy.get('.btn-save').contains('Apply to All & Save').click(); + cy.get('.btn-save').contains('Apply Mass Update').click(); cy.get('.validation-summary').contains('Unfortunately we only accept a minimum of 50% Completion...'); cy.get('.item-details-editor-container .slider-editor-input.editor-percentComplete').as('range').invoke('val', 5).trigger('change'); cy.get('.item-details-editor-container .slider-editor-input.editor-percentComplete').as('range').invoke('val', 51).trigger('change'); cy.get('.item-details-editor-container .input-group-text').contains('51'); - cy.get('.btn-save').contains('Apply to All & Save').click(); + cy.get('.btn-save').contains('Apply Mass Update').click(); cy.get('.slick-editor-modal').should('not.exist'); }); @@ -448,12 +448,12 @@ describe('Example 12 - Composite Editor Modal', { retries: 1 }, () => { cy.get('.item-details-container.editor-origin .modified').should('have.length', 1); cy.get('.item-details-container.editor-origin .autocomplete').invoke('val').then(text => expect(text).to.eq('Belize')); - cy.get('.btn-save').contains('Apply to Selected & Save').click(); + cy.get('.btn-save').contains('Update Selection').click(); cy.get('.validation-summary').contains('Unfortunately we only accept a minimum of 50% Completion...'); cy.get('.item-details-editor-container .slider-editor-input.editor-percentComplete').as('range').invoke('val', 77).trigger('change'); cy.get('.item-details-editor-container .input-group-text').contains('77'); - cy.get('.btn-save').contains('Apply to Selected & Save').click(); + cy.get('.btn-save').contains('Update Selection').click(); cy.get('.slick-editor-modal').should('not.exist'); });