diff --git a/packages/common/src/editors/__tests__/selectEditor.spec.ts b/packages/common/src/editors/__tests__/selectEditor.spec.ts index 3beba6d6f..f8bfbfdb5 100644 --- a/packages/common/src/editors/__tests__/selectEditor.spec.ts +++ b/packages/common/src/editors/__tests__/selectEditor.spec.ts @@ -211,7 +211,7 @@ describe('SelectEditor', () => { expect(editorElm[0].value).toEqual('male'); }); - it('should create the multi-select filter with a blank entry at the beginning of the collection when "addBlankEntry" is set in the "collectionOptions" property', () => { + it('should create the multi-select editor with a blank entry at the beginning of the collection when "addBlankEntry" is set in the "collectionOptions" property', () => { mockColumn.internalColumnEditor.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; mockColumn.internalColumnEditor.collectionOptions = { addBlankEntry: true }; @@ -223,6 +223,39 @@ describe('SelectEditor', () => { editorOkElm.click(); expect(editorListElm.length).toBe(3); + expect(editorListElm[0].value).toBe(''); + expect(editorListElm[1].textContent).toBe(''); + }); + + it('should create the multi-select editor with a custom entry at the beginning of the collection when "addCustomFirstEntry" is provided in the "collectionOptions" property', () => { + mockColumn.internalColumnEditor.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + mockColumn.internalColumnEditor.collectionOptions = { addCustomFirstEntry: { value: null, label: '' } }; + + editor = new SelectEditor(editorArguments, true); + const editorBtnElm = divContainer.querySelector('.ms-parent.ms-filter.editor-gender button.ms-choice'); + const editorListElm = divContainer.querySelectorAll(`[name=editor-gender].ms-drop ul>li input[type=checkbox]`); + const editorOkElm = divContainer.querySelector(`[name=editor-gender].ms-drop .ms-ok-button`); + editorBtnElm.click(); + editorOkElm.click(); + + expect(editorListElm.length).toBe(3); + expect(editorListElm[0].value).toBe(''); + expect(editorListElm[1].textContent).toBe(''); + }); + + it('should create the multi-select editor with a custom entry at the end of the collection when "addCustomFirstEntry" is provided in the "collectionOptions" property', () => { + mockColumn.internalColumnEditor.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + mockColumn.internalColumnEditor.collectionOptions = { addCustomLastEntry: { value: null, label: '' } }; + + editor = new SelectEditor(editorArguments, true); + const editorBtnElm = divContainer.querySelector('.ms-parent.ms-filter.editor-gender button.ms-choice'); + const editorListElm = divContainer.querySelectorAll(`[name=editor-gender].ms-drop ul>li input[type=checkbox]`); + const editorOkElm = divContainer.querySelector(`[name=editor-gender].ms-drop .ms-ok-button`); + editorBtnElm.click(); + editorOkElm.click(); + + expect(editorListElm.length).toBe(3); + expect(editorListElm[2].value).toBe(''); expect(editorListElm[1].textContent).toBe(''); }); @@ -450,7 +483,7 @@ describe('SelectEditor', () => { }); describe('initialize with collection', () => { - it('should create the multi-select filter with a default search term when passed as a filter argument even with collection an array of strings', () => { + it('should create the multi-select editor with a default search term when passed as a filter argument even with collection an array of strings', () => { mockColumn.internalColumnEditor.collection = ['male', 'female']; editor = new SelectEditor(editorArguments, true); @@ -467,7 +500,7 @@ describe('SelectEditor', () => { }); describe('collectionSortBy setting', () => { - it('should create the multi-select filter and sort the string collection when "collectionSortBy" is set', () => { + it('should create the multi-select editor and sort the string collection when "collectionSortBy" is set', () => { mockColumn.internalColumnEditor = { collection: ['other', 'male', 'female'], collectionSortBy: { @@ -487,7 +520,7 @@ describe('SelectEditor', () => { expect(editorListElm[2].value).toBe('female'); }); - it('should create the multi-select filter and sort the value/label pair collection when "collectionSortBy" is set', () => { + it('should create the multi-select editor and sort the value/label pair collection when "collectionSortBy" is set', () => { mockColumn.internalColumnEditor = { collection: [{ value: 'other', description: 'other' }, { value: 'male', description: 'male' }, { value: 'female', description: 'female' }], collectionSortBy: { @@ -514,7 +547,7 @@ describe('SelectEditor', () => { }); describe('collectionFilterBy setting', () => { - it('should create the multi-select filter and filter the string collection when "collectionFilterBy" is set', () => { + it('should create the multi-select editor and filter the string collection when "collectionFilterBy" is set', () => { mockColumn.internalColumnEditor = { collection: ['other', 'male', 'female'], collectionFilterBy: { @@ -532,7 +565,7 @@ describe('SelectEditor', () => { expect(editorListElm[0].value).toBe('other'); }); - it('should create the multi-select filter and filter the value/label pair collection when "collectionFilterBy" is set', () => { + it('should create the multi-select editor and filter the value/label pair collection when "collectionFilterBy" is set', () => { mockColumn.internalColumnEditor = { collection: [{ value: 'other', description: 'other' }, { value: 'male', description: 'male' }, { value: 'female', description: 'female' }], collectionFilterBy: [ @@ -554,7 +587,7 @@ describe('SelectEditor', () => { expect(editorListElm[0].value).toBe('female'); }); - it('should create the multi-select filter and filter the value/label pair collection when "collectionFilterBy" is set and "filterResultAfterEachPass" is set to "merge"', () => { + it('should create the multi-select editor and filter the value/label pair collection when "collectionFilterBy" is set and "filterResultAfterEachPass" is set to "merge"', () => { mockColumn.internalColumnEditor = { collection: [{ value: 'other', description: 'other' }, { value: 'male', description: 'male' }, { value: 'female', description: 'female' }], collectionFilterBy: [ @@ -608,7 +641,7 @@ describe('SelectEditor', () => { }); describe('enableRenderHtml property', () => { - it('should create the multi-select filter with a default search term and have the HTML rendered when "enableRenderHtml" is set', () => { + it('should create the multi-select editor with a default search term and have the HTML rendered when "enableRenderHtml" is set', () => { mockColumn.internalColumnEditor = { enableRenderHtml: true, collection: [{ value: true, label: 'True', labelPrefix: ` ` }, { value: false, label: 'False' }], @@ -628,7 +661,7 @@ describe('SelectEditor', () => { expect(editorListElm[0].innerHTML).toBe(' True'); }); - it('should create the multi-select filter with a default search term and have the HTML rendered and sanitized when "enableRenderHtml" is set and has ` }, { isEffort: false, label: 'False' }], @@ -656,7 +689,7 @@ describe('SelectEditor', () => { expect(editorListElm[0].innerHTML).toBe(' : True'); }); - it('should create the multi-select filter with a default search term and have the HTML rendered and sanitized when using a custom "sanitizer" and "enableRenderHtml" flag is set and has ` }, { isEffort: false, label: 'False' }], diff --git a/packages/common/src/editors/selectEditor.ts b/packages/common/src/editors/selectEditor.ts index fb5764cdc..bf6475663 100644 --- a/packages/common/src/editors/selectEditor.ts +++ b/packages/common/src/editors/selectEditor.ts @@ -560,7 +560,7 @@ export class SelectEditor implements Editor { } renderDomElement(collection: any[]) { - if (!Array.isArray(collection) && this.collectionOptions && (this.collectionOptions.collectionInsideObjectProperty)) { + if (!Array.isArray(collection) && this.collectionOptions?.collectionInsideObjectProperty) { const collectionInsideObjectProperty = this.collectionOptions.collectionInsideObjectProperty; collection = getDescendantProperty(collection, collectionInsideObjectProperty); } @@ -569,10 +569,24 @@ export class SelectEditor implements Editor { } // user can optionally add a blank entry at the beginning of the collection - if (this.collectionOptions && this.collectionOptions.addBlankEntry) { + // make sure however that it wasn't added more than once + if (this.collectionOptions?.addBlankEntry && Array.isArray(collection) && collection.length > 0 && collection[0][this.valueName] !== '') { collection.unshift(this.createBlankEntry()); } + // user can optionally add his own custom entry at the beginning of the collection + if (this.collectionOptions?.addCustomFirstEntry && Array.isArray(collection) && collection.length > 0 && collection[0][this.valueName] !== this.collectionOptions.addCustomFirstEntry[this.valueName]) { + collection.unshift(this.collectionOptions.addCustomFirstEntry); + } + + // user can optionally add his own custom entry at the end of the collection + if (this.collectionOptions?.addCustomLastEntry && Array.isArray(collection) && collection.length > 0) { + const lastCollectionIndex = collection.length - 1; + if (collection[lastCollectionIndex][this.valueName] !== this.collectionOptions.addCustomLastEntry[this.valueName]) { + collection.push(this.collectionOptions.addCustomLastEntry); + } + } + // assign the collection to a temp variable before filtering/sorting the collection let newCollection = collection; @@ -614,7 +628,9 @@ export class SelectEditor implements Editor { let prefixText = option[this.labelPrefixName] || ''; let suffixText = option[this.labelSuffixName] || ''; let optionLabel = option[this.optionLabel] || ''; - optionLabel = optionLabel.toString().replace(/\"/g, '\''); // replace double quotes by single quotes to avoid interfering with regular html + if (optionLabel?.toString) { + optionLabel = optionLabel.toString().replace(/\"/g, '\''); // replace double quotes by single quotes to avoid interfering with regular html + } // also translate prefix/suffix if enableTranslateLabel is true and text is a string prefixText = (this.enableTranslateLabel && prefixText && typeof prefixText === 'string') ? this._translaterService.translate(prefixText || ' ') : prefixText; @@ -634,7 +650,12 @@ export class SelectEditor implements Editor { optionText = htmlEncode(sanitizedText); } - options += ``; + // html text of each select option + let optionValue = option[this.valueName]; + if (optionValue === undefined || optionValue === null) { + optionValue = ''; + } + options += ``; }); } return ``; diff --git a/packages/common/src/filters/__tests__/selectFilter.spec.ts b/packages/common/src/filters/__tests__/selectFilter.spec.ts index ba89df6f4..a3f0fc502 100644 --- a/packages/common/src/filters/__tests__/selectFilter.spec.ts +++ b/packages/common/src/filters/__tests__/selectFilter.spec.ts @@ -485,10 +485,53 @@ describe('SelectFilter', () => { expect(filterListElm.length).toBe(3); expect(filterFilledElms.length).toBe(1); + expect(filterListElm[0].value).toBe(''); expect(filterListElm[2].checked).toBe(true); expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, operator: 'IN', searchTerms: ['female'], shouldTriggerQuery: true }); }); + it('should create the multi-select filter with a custom entry at the beginning of the collection when "addCustomFirstEntry" is provided in the "collectionOptions" property', () => { + filterArguments.searchTerms = ['female']; + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + mockColumn.filter.collectionOptions = { addCustomFirstEntry: { value: null, label: '' } }; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filter.init(filterArguments); + const filterBtnElm = divContainer.querySelector('.ms-parent.ms-filter.search-filter.filter-gender button.ms-choice'); + const filterListElm = divContainer.querySelectorAll(`[name=filter-gender].ms-drop ul>li input[type=checkbox]`); + const filterFilledElms = divContainer.querySelectorAll('.ms-parent.ms-filter.search-filter.filter-gender.filled'); + const filterOkElm = divContainer.querySelector(`[name=filter-gender].ms-drop .ms-ok-button`); + filterBtnElm.click(); + filterOkElm.click(); + + expect(filterListElm.length).toBe(3); + expect(filterFilledElms.length).toBe(1); + expect(filterListElm[0].value).toBe(''); + expect(filterListElm[2].checked).toBe(true); + expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, operator: 'IN', searchTerms: ['female'], shouldTriggerQuery: true }); + }); + + it('should create the multi-select filter with a custom entry at the end of the collection when "addCustomFirstEntry" is provided in the "collectionOptions" property', () => { + filterArguments.searchTerms = ['female']; + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + mockColumn.filter.collectionOptions = { addCustomLastEntry: { value: null, label: '' } }; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filter.init(filterArguments); + const filterBtnElm = divContainer.querySelector('.ms-parent.ms-filter.search-filter.filter-gender button.ms-choice'); + const filterListElm = divContainer.querySelectorAll(`[name=filter-gender].ms-drop ul>li input[type=checkbox]`); + const filterFilledElms = divContainer.querySelectorAll('.ms-parent.ms-filter.search-filter.filter-gender.filled'); + const filterOkElm = divContainer.querySelector(`[name=filter-gender].ms-drop .ms-ok-button`); + filterBtnElm.click(); + filterOkElm.click(); + + expect(filterListElm.length).toBe(3); + expect(filterFilledElms.length).toBe(1); + expect(filterListElm[2].value).toBe(''); + expect(filterListElm[1].checked).toBe(true); + expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, operator: 'IN', searchTerms: ['female'], shouldTriggerQuery: true }); + }); + it('should trigger a callback with the clear filter set when calling the "clear" method', () => { filterArguments.searchTerms = ['female']; mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; diff --git a/packages/common/src/filters/selectFilter.ts b/packages/common/src/filters/selectFilter.ts index f37fe2462..2e70eefee 100644 --- a/packages/common/src/filters/selectFilter.ts +++ b/packages/common/src/filters/selectFilter.ts @@ -254,10 +254,23 @@ export class SelectFilter implements Filter { // user can optionally add a blank entry at the beginning of the collection // make sure however that it wasn't added more than once - if (this.collectionOptions && this.collectionOptions.addBlankEntry && Array.isArray(collection) && collection.length > 0 && collection[0][this.labelName] !== '') { + if (this.collectionOptions?.addBlankEntry && Array.isArray(collection) && collection.length > 0 && collection[0][this.valueName] !== '') { collection.unshift(this.createBlankEntry()); } + // user can optionally add his own custom entry at the beginning of the collection + if (this.collectionOptions?.addCustomFirstEntry && Array.isArray(collection) && collection.length > 0 && collection[0][this.valueName] !== this.collectionOptions.addCustomFirstEntry[this.valueName]) { + collection.unshift(this.collectionOptions.addCustomFirstEntry); + } + + // user can optionally add his own custom entry at the end of the collection + if (this.collectionOptions?.addCustomLastEntry && Array.isArray(collection) && collection.length > 0) { + const lastCollectionIndex = collection.length - 1; + if (collection[lastCollectionIndex][this.valueName] !== this.collectionOptions.addCustomLastEntry[this.valueName]) { + collection.push(this.collectionOptions.addCustomLastEntry); + } + } + // assign the collection to a temp variable before filtering/sorting the collection let newCollection = collection; @@ -312,7 +325,9 @@ export class SelectFilter implements Filter { let prefixText = option[this.labelPrefixName] || ''; let suffixText = option[this.labelSuffixName] || ''; let optionLabel = option.hasOwnProperty(this.optionLabel) ? option[this.optionLabel] : ''; - optionLabel = optionLabel.toString().replace(/\"/g, '\''); // replace double quotes by single quotes to avoid interfering with regular html + if (optionLabel?.toString) { + optionLabel = optionLabel.toString().replace(/\"/g, '\''); // replace double quotes by single quotes to avoid interfering with regular html + } // also translate prefix/suffix if enableTranslateLabel is true and text is a string prefixText = (this.enableTranslateLabel && isTranslateEnabled && prefixText && typeof prefixText === 'string') ? this.translaterService?.getCurrentLanguage && this.translaterService.getCurrentLanguage() && this.translaterService.translate(prefixText || ' ') : prefixText; @@ -332,7 +347,11 @@ export class SelectFilter implements Filter { } // html text of each select option - options += ``; + let optionValue = option[this.valueName]; + if (optionValue === undefined || optionValue === null) { + optionValue = ''; + } + options += ``; // if there's a search term, we will add the "filled" class for styling purposes // on a single select, we'll also make sure the single value is not an empty string to consider this being filled diff --git a/packages/common/src/interfaces/collectionOption.interface.ts b/packages/common/src/interfaces/collectionOption.interface.ts index 61fa811ed..f0844886e 100644 --- a/packages/common/src/interfaces/collectionOption.interface.ts +++ b/packages/common/src/interfaces/collectionOption.interface.ts @@ -3,11 +3,17 @@ import { FilterMultiplePassTypeString } from '../enums/filterMultiplePassTypeStr export interface CollectionOption { /** - * Optionally add a blank entry to the beginning of the collection. + * Optionally add a blank entry to the beginning of the collection (only used by the SingleSelect/MultipleSelect Editor or Filter). * Useful when we want to return all data by setting an empty filter that might not exist in the original collection */ addBlankEntry?: boolean; + /** Optionally add a custom entry at the beginning of the collection (only used by the SingleSelect/MultipleSelect Editor or Filter). */ + addCustomFirstEntry?: any; + + /** Optionally add a custom entry at the end of the collection (only used by the SingleSelect/MultipleSelect Editor or Filter). */ + addCustomLastEntry?: any; + /** * When the collection is inside an object descendant property * we can optionally pass a dot (.) notation string to pull the collection from an object property. diff --git a/packages/common/src/styles/_variables-theme-salesforce.scss b/packages/common/src/styles/_variables-theme-salesforce.scss index d3290f242..52022db3a 100644 --- a/packages/common/src/styles/_variables-theme-salesforce.scss +++ b/packages/common/src/styles/_variables-theme-salesforce.scss @@ -121,6 +121,7 @@ $autocomplete-loading-icon-margin-left: -26px !default; $autocomplete-loading-icon-line-height: 0px !default; $autocomplete-loading-icon-vertical-align: sub !default; $compound-filter-operator-select-border: 1px solid #6cb6ff !default; +$compound-filter-text-color: $button-primary-color !default; $multiselect-icon-arrow-font-size: calc(#{$icon-font-size} - 4px) !default; $multiselect-icon-checked-color: $highlight-color !default; $multiselect-icon-border: 1px solid #d6d4d4 !default; diff --git a/packages/vanilla-bundle/dist-grid-bundle-zip/slickgrid-vanilla-bundle.zip b/packages/vanilla-bundle/dist-grid-bundle-zip/slickgrid-vanilla-bundle.zip index 31091e4c6..4b04f5153 100644 Binary files a/packages/vanilla-bundle/dist-grid-bundle-zip/slickgrid-vanilla-bundle.zip and b/packages/vanilla-bundle/dist-grid-bundle-zip/slickgrid-vanilla-bundle.zip differ