diff --git a/src/app/modules/angular-slickgrid/editors/__tests__/selectEditor.spec.ts b/src/app/modules/angular-slickgrid/editors/__tests__/selectEditor.spec.ts index b64c6ae2b..70323a205 100644 --- a/src/app/modules/angular-slickgrid/editors/__tests__/selectEditor.spec.ts +++ b/src/app/modules/angular-slickgrid/editors/__tests__/selectEditor.spec.ts @@ -205,7 +205,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 }; @@ -217,6 +217,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(''); }); @@ -457,7 +490,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); @@ -474,7 +507,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: { @@ -494,7 +527,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: { @@ -521,7 +554,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: { @@ -539,7 +572,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: [ @@ -561,7 +594,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: [ @@ -615,7 +648,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' }], @@ -635,7 +668,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' }], diff --git a/src/app/modules/angular-slickgrid/editors/selectEditor.ts b/src/app/modules/angular-slickgrid/editors/selectEditor.ts index 0e777f9f6..0bbf65602 100644 --- a/src/app/modules/angular-slickgrid/editors/selectEditor.ts +++ b/src/app/modules/angular-slickgrid/editors/selectEditor.ts @@ -519,10 +519,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 && Array.isArray(collection) && collection.length > 0 && collection[0][this.labelName] !== '') { + // 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.valueName] !== '') { collection.unshift(this.createBlankEntry()); } + // user can optionally add his own custom entry at the beginning of the collection + if (this.collectionOptions && this.collectionOptions.addCustomFirstEntry && Array.isArray(collection) && collection.length > 0 && collection[0][this.valueName] !== this.collectionOptions.addCustomFirstEntry[this.valueName]) { + collection.unshift(this.collectionOptions && this.collectionOptions.addCustomFirstEntry); + } + + // user can optionally add his own custom entry at the end of the collection + if (this.collectionOptions && 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 && this.collectionOptions.addCustomLastEntry); + } + } + let newCollection = collection || []; // user might want to filter and/or sort certain items of the collection @@ -560,7 +574,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 && 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._translate.instant(prefixText || ' ') : prefixText; @@ -580,7 +596,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 += ``; }); } diff --git a/src/app/modules/angular-slickgrid/filters/__tests__/selectFilter.spec.ts b/src/app/modules/angular-slickgrid/filters/__tests__/selectFilter.spec.ts index 7e7417c1d..cb2191e7a 100644 --- a/src/app/modules/angular-slickgrid/filters/__tests__/selectFilter.spec.ts +++ b/src/app/modules/angular-slickgrid/filters/__tests__/selectFilter.spec.ts @@ -89,13 +89,13 @@ describe('SelectFilter', () => { }); it('should throw an error when trying to call init without any arguments', () => { - expect(() => filter.init(null, true)).toThrowError('[Angular-SlickGrid] A filter must always have an "init()" with valid arguments.'); + expect(() => filter.init(null)).toThrowError('[Angular-SlickGrid] A filter must always have an "init()" with valid arguments.'); }); it('should throw an error when there is no collection provided in the filter property', (done) => { try { mockColumn.filter.collection = undefined; - filter.init(filterArguments, true); + filter.init(filterArguments); } catch (e) { expect(e.toString()).toContain(`[Angular-SlickGrid] You need to pass a "collection" (or "collectionAsync") for the MultipleSelect/SingleSelect Filter to work correctly.`); done(); @@ -106,7 +106,7 @@ describe('SelectFilter', () => { try { // @ts-ignore mockColumn.filter.collection = { hello: 'world' }; - filter.init(filterArguments, true); + filter.init(filterArguments); } catch (e) { expect(e.toString()).toContain(`The "collection" passed to the Select Filter is not a valid array.`); done(); @@ -116,7 +116,7 @@ describe('SelectFilter', () => { it('should throw an error when collection is not a valid value/label pair array', (done) => { try { mockColumn.filter.collection = [{ hello: 'world' }]; - filter.init(filterArguments, true); + filter.init(filterArguments); } catch (e) { expect(e.toString()).toContain(`[select-filter] A collection with value/label (or value/labelKey when using Locale) is required to populate the Select list`); done(); @@ -130,7 +130,7 @@ describe('SelectFilter', () => { mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; filter = new SelectFilter(translate, collectionService); - filter.init(filterArguments, true); + filter.init(filterArguments); } catch (e) { expect(e.toString()).toContain(`[select-filter] The ngx-translate TranslateService is required for the Select Filter to work correctly when "enableTranslateLabel" is set.`); done(); @@ -139,7 +139,7 @@ describe('SelectFilter', () => { it('should initialize the filter', () => { mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; - filter.init(filterArguments, true); + filter.init(filterArguments); const filterCount = divContainer.querySelectorAll('select.ms-filter.search-filter.filter-gender').length; expect(spyGetHeaderRow).toHaveBeenCalled(); @@ -149,7 +149,7 @@ describe('SelectFilter', () => { it('should be a multiple-select filter by default when it is not specified in the constructor', () => { mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; filter = new SelectFilter(translate, collectionService); - filter.init(filterArguments, true); + filter.init(filterArguments); const filterCount = divContainer.querySelectorAll('select.ms-filter.search-filter.filter-gender').length; expect(spyGetHeaderRow).toHaveBeenCalled(); @@ -162,7 +162,7 @@ describe('SelectFilter', () => { mockColumn.filter.placeholder = testValue; mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; - filter.init(filterArguments, true); + filter.init(filterArguments); const filterElm = divContainer.querySelector('.ms-filter.search-filter.filter-gender .placeholder'); expect(filterElm.innerHTML).toBe(testValue); @@ -172,7 +172,7 @@ describe('SelectFilter', () => { const spyCallback = jest.spyOn(filterArguments, 'callback'); mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; - filter.init(filterArguments, true); + 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 filterOkElm = divContainer.querySelector(`[name=filter-gender].ms-drop .ms-ok-button`); @@ -192,7 +192,7 @@ describe('SelectFilter', () => { const spyCallback = jest.spyOn(filterArguments, 'callback'); mockColumn.filter.collection = ['male', 'female']; - filter.init(filterArguments, true); + 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 filterOkElm = divContainer.querySelector(`[name=filter-gender].ms-drop .ms-ok-button`); @@ -213,7 +213,7 @@ describe('SelectFilter', () => { mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; const spyCallback = jest.spyOn(filterArguments, 'callback'); - filter.init(filterArguments, true); + 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 filterOkElm = divContainer.querySelector(`[name=filter-gender].ms-drop .ms-ok-button`); @@ -230,7 +230,7 @@ describe('SelectFilter', () => { it('should have same value in "getValues" after being set in "setValues"', () => { mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; - filter.init(filterArguments, true); + filter.init(filterArguments); filter.setValues('female'); const values = filter.getValues(); @@ -240,7 +240,7 @@ describe('SelectFilter', () => { it('should have empty array returned from "getValues" when nothing is set', () => { mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; - filter.init(filterArguments, true); + filter.init(filterArguments); const values = filter.getValues(); expect(values).toEqual([]); @@ -259,7 +259,7 @@ describe('SelectFilter', () => { const spyCallback = jest.spyOn(filterArguments, 'callback'); filterArguments.searchTerms = ['female']; - filter.init(filterArguments, true); + 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'); @@ -278,7 +278,7 @@ describe('SelectFilter', () => { const spyCallback = jest.spyOn(filterArguments, 'callback'); filterArguments.searchTerms = [false]; - filter.init(filterArguments, true); + 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'); @@ -297,7 +297,7 @@ describe('SelectFilter', () => { const spyCallback = jest.spyOn(filterArguments, 'callback'); filterArguments.searchTerms = [2]; - filter.init(filterArguments, true); + 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'); @@ -316,7 +316,7 @@ describe('SelectFilter', () => { mockColumn.filter.collection = ['male', 'female']; filterArguments.searchTerms = ['female']; - filter.init(filterArguments, true); + 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'); @@ -339,7 +339,7 @@ describe('SelectFilter', () => { } }; - filter.init(filterArguments, true); + 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 span`); filterBtnElm.click(); @@ -364,7 +364,7 @@ describe('SelectFilter', () => { }, }; - filter.init(filterArguments, true); + 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 span`); filterBtnElm.click(); @@ -384,7 +384,7 @@ describe('SelectFilter', () => { } }; - filter.init(filterArguments, true); + 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 span`); filterBtnElm.click(); @@ -406,7 +406,7 @@ describe('SelectFilter', () => { }, }; - filter.init(filterArguments, true); + 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 span`); filterBtnElm.click(); @@ -431,7 +431,7 @@ describe('SelectFilter', () => { }, }; - filter.init(filterArguments, true); + 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 span`); filterBtnElm.click(); @@ -454,7 +454,7 @@ describe('SelectFilter', () => { }, }; - filter.init(filterArguments, true); + 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 span`); filterBtnElm.click(); @@ -477,7 +477,7 @@ describe('SelectFilter', () => { }, }; - filter.init(filterArguments, true); + filter.init(filterArguments); setTimeout(() => { const filterBtnElm = divContainer.querySelector('.ms-parent.ms-filter.search-filter.filter-gender button.ms-choice'); @@ -497,7 +497,7 @@ describe('SelectFilter', () => { mockColumn.filter.collectionAsync = of(['male', 'female']); filterArguments.searchTerms = ['female']; - filter.init(filterArguments, true); + filter.init(filterArguments); setTimeout(() => { const filterBtnElm = divContainer.querySelector('.ms-parent.ms-filter.search-filter.filter-gender button.ms-choice'); @@ -520,7 +520,7 @@ describe('SelectFilter', () => { mockColumn.filter.collectionAsync = of(mockCollection); filterArguments.searchTerms = ['female']; - filter.init(filterArguments, true); + filter.init(filterArguments); setTimeout(() => { const filterBtnElm = divContainer.querySelector('.ms-parent.ms-filter.search-filter.filter-gender button.ms-choice'); @@ -542,7 +542,7 @@ describe('SelectFilter', () => { it('should throw an error when "collectionAsync" Observable does not return a valid array', (done) => { mockColumn.filter.collectionAsync = of({ hello: 'world' }); - filter.init(filterArguments, true).catch((e) => { + filter.init(filterArguments).catch((e) => { expect(e.toString()).toContain(`Something went wrong while trying to pull the collection from the "collectionAsync" call in the Select Filter, the collection is not a valid array.`); done(); }); @@ -559,7 +559,7 @@ describe('SelectFilter', () => { }, }; - filter.init(filterArguments, true); + 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 span`); filterBtnElm.click(); @@ -579,7 +579,7 @@ describe('SelectFilter', () => { }, }; - filter.init(filterArguments, true); + 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 span`); filterBtnElm.click(); @@ -594,7 +594,7 @@ describe('SelectFilter', () => { mockColumn.filter.collectionOptions = { addBlankEntry: true }; const spyCallback = jest.spyOn(filterArguments, 'callback'); - filter.init(filterArguments, true); + 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'); @@ -604,16 +604,59 @@ 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' }]; const spyCallback = jest.spyOn(filterArguments, 'callback'); - filter.init(filterArguments, true); + filter.init(filterArguments); filter.clear(); const filterFilledElms = divContainer.querySelectorAll('.ms-parent.ms-filter.search-filter.filter-gender.filled'); @@ -627,7 +670,7 @@ describe('SelectFilter', () => { const spyCallback = jest.spyOn(filterArguments, 'callback'); filterArguments.searchTerms = ['female']; - filter.init(filterArguments, true); + filter.init(filterArguments); filter.clear(false); const filterFilledElms = divContainer.querySelectorAll('.ms-parent.ms-filter.search-filter.filter-gender.filled'); @@ -650,7 +693,7 @@ describe('SelectFilter', () => { }; filterArguments.searchTerms = ['male', 'female']; - filter.init(filterArguments, true); + filter.init(filterArguments); setTimeout(() => { const filterSelectAllElm = divContainer.querySelector('.filter-gender .ms-select-all label span'); @@ -685,7 +728,7 @@ describe('SelectFilter', () => { }; filterArguments.searchTerms = ['male', 'female']; - filter.init(filterArguments, true); + filter.init(filterArguments); setTimeout(() => { const filterSelectAllElm = divContainer.querySelector('.filter-gender .ms-select-all label span'); const filterBtnElm = divContainer.querySelector('.ms-parent.ms-filter.search-filter.filter-gender button.ms-choice'); diff --git a/src/app/modules/angular-slickgrid/filters/selectFilter.ts b/src/app/modules/angular-slickgrid/filters/selectFilter.ts index e2a01e39b..a3ce0fe51 100644 --- a/src/app/modules/angular-slickgrid/filters/selectFilter.ts +++ b/src/app/modules/angular-slickgrid/filters/selectFilter.ts @@ -307,11 +307,25 @@ export class SelectFilter implements Filter { throw new Error('The "collection" passed to the Select Filter is not a valid array.'); } + // 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 && 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 && 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 && 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); + } + } + let newCollection = collection; // user might want to filter and/or sort certain items of the collection @@ -360,7 +374,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 && 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 && isEnableTranslate && prefixText && typeof prefixText === 'string') ? this.translate && this.translate.currentLang && this.translate.instant(prefixText || ' ') : prefixText; @@ -381,7 +397,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 at least 1 search term found, 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/src/app/modules/angular-slickgrid/models/collectionOption.interface.ts b/src/app/modules/angular-slickgrid/models/collectionOption.interface.ts index de9f36b34..51a426a53 100644 --- a/src/app/modules/angular-slickgrid/models/collectionOption.interface.ts +++ b/src/app/modules/angular-slickgrid/models/collectionOption.interface.ts @@ -3,11 +3,17 @@ import { FilterMultiplePassTypeString } from './filterMultiplePassTypeString'; 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; + /** @deprecated please use "collectionInsideObjectProperty" instead */ collectionInObjectProperty?: string;