diff --git a/README.md b/README.md index b80844b6e..bc49a055d 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ You like and use this great library `Angular-Slickgrid`? Please upvote :star: an - version `2.x.x` for Angular 7+ #### Angular 8 -When running `ng update` to upgrade to Angular 8, one of the biggest change that is noticeable is that they change the target to `ES2015`, which does not play well with SlickGrid core library (which is all written in plain ES5 javascript). So for that reason, you need to switch back the `target` to `ES5` in your `tsconfig.json` file (`"target": "es5"`). This might be fixeable in the future, but for now that is the quick fix to get Angular 8 to work. +When running `ng update` to upgrade to Angular 8, one of the biggest change that is noticeable is that they change the target to `ES2015`, which does not play well with SlickGrid core library (which is all written in plain ES5 javascript). So for that reason, you need to switch back the `target` to `ES5` in your `tsconfig.json` file (`"target": "es5"`). This might be fixable in the future, but for now that is the quick fix to get Angular 8 to work. ### Installation Refer to the [Wiki - HOWTO Step by Step](https://github.com/ghiscoding/angular-slickgrid/wiki/HOWTO---Step-by-Step) diff --git a/src/app/modules/angular-slickgrid/editors/selectEditor.ts b/src/app/modules/angular-slickgrid/editors/selectEditor.ts index ffa1d6ca2..6e23b49fe 100644 --- a/src/app/modules/angular-slickgrid/editors/selectEditor.ts +++ b/src/app/modules/angular-slickgrid/editors/selectEditor.ts @@ -432,8 +432,9 @@ export class SelectEditor implements Editor { } protected renderDomElement(collection: any[]) { - if (!Array.isArray(collection) && this.collectionOptions && this.collectionOptions.collectionInObjectProperty) { - collection = getDescendantProperty(collection, this.collectionOptions.collectionInObjectProperty); + if (!Array.isArray(collection) && this.collectionOptions && (this.collectionOptions.collectionInsideObjectProperty || this.collectionOptions.collectionInObjectProperty)) { + const collectionInsideObjectProperty = this.collectionOptions.collectionInsideObjectProperty || this.collectionOptions.collectionInObjectProperty; + collection = getDescendantProperty(collection, collectionInsideObjectProperty); } if (!Array.isArray(collection)) { throw new Error('The "collection" passed to the Select Editor is not a valid array'); diff --git a/src/app/modules/angular-slickgrid/filters/__tests__/multipleSelectFilter.spec.ts b/src/app/modules/angular-slickgrid/filters/__tests__/multipleSelectFilter.spec.ts new file mode 100644 index 000000000..b85bb84df --- /dev/null +++ b/src/app/modules/angular-slickgrid/filters/__tests__/multipleSelectFilter.spec.ts @@ -0,0 +1,79 @@ +// import 3rd party lib multiple-select for the tests +import '../../../../../assets/lib/multiple-select/multiple-select'; + +import { TestBed } from '@angular/core/testing'; +import { TranslateService, TranslateModule } from '@ngx-translate/core'; +import { Column, FilterArguments, GridOption } from '../../models'; +import { CollectionService } from '../../services/collection.service'; +import { Filters } from '..'; +import { MultipleSelectFilter } from '../multipleSelectFilter'; +import { of, Subject } from 'rxjs'; + +const containerId = 'demo-container'; + +// define a
container to simulate the grid container +const template = `
`; + +const gridOptionMock = { + enableFiltering: true, + enableFilterTrimWhiteSpace: true, +} as GridOption; + +const gridStub = { + getOptions: () => gridOptionMock, + getColumns: jest.fn(), + getHeaderRowColumn: jest.fn(), + render: jest.fn(), +}; + +describe('MultipleSelectFilter', () => { + let divContainer: HTMLDivElement; + let filter: MultipleSelectFilter; + let filterArguments: FilterArguments; + let spyGetHeaderRow; + let mockColumn: Column; + let collectionService: CollectionService; + let translate: TranslateService; + + beforeEach(() => { + divContainer = document.createElement('div'); + divContainer.innerHTML = template; + document.body.appendChild(divContainer); + spyGetHeaderRow = jest.spyOn(gridStub, 'getHeaderRowColumn').mockReturnValue(divContainer); + + mockColumn = { + id: 'gender', field: 'gender', filterable: true, + filter: { + model: Filters.multipleSelect, + } + }; + filterArguments = { + grid: gridStub, + columnDef: mockColumn, + callback: jest.fn() + }; + + TestBed.configureTestingModule({ + providers: [CollectionService], + imports: [TranslateModule.forRoot()] + }); + collectionService = TestBed.get(CollectionService); + translate = TestBed.get(TranslateService); + filter = new MultipleSelectFilter(translate, collectionService); + }); + + afterEach(() => { + filter.destroy(); + }); + + it('should be a multiple-select filter', () => { + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + filter = new MultipleSelectFilter(translate, collectionService); + filter.init(filterArguments, true); + const filterCount = divContainer.querySelectorAll('select.ms-filter.search-filter.filter-gender').length; + + expect(spyGetHeaderRow).toHaveBeenCalled(); + expect(filterCount).toBe(1); + expect(filter.isMultipleSelect).toBe(true); + }); +}); diff --git a/src/app/modules/angular-slickgrid/filters/__tests__/selectFilter.spec.ts b/src/app/modules/angular-slickgrid/filters/__tests__/selectFilter.spec.ts new file mode 100644 index 000000000..33bed652e --- /dev/null +++ b/src/app/modules/angular-slickgrid/filters/__tests__/selectFilter.spec.ts @@ -0,0 +1,670 @@ +// import 3rd party lib multiple-select for the tests +import '../../../../../assets/lib/multiple-select/multiple-select'; + +import { TestBed } from '@angular/core/testing'; +import { TranslateService, TranslateModule } from '@ngx-translate/core'; +import { Column, FilterArguments, GridOption, FieldType, OperatorType } from '../../models'; +import { CollectionService } from './../../services/collection.service'; +import { Filters } from '..'; +import { SelectFilter } from '../selectFilter'; +import { of, Subject } from 'rxjs'; + +const containerId = 'demo-container'; + +// define a
container to simulate the grid container +const template = `
`; + +const gridOptionMock = { + enableFiltering: true, + enableFilterTrimWhiteSpace: true, +} as GridOption; + +const gridStub = { + getOptions: () => gridOptionMock, + getColumns: jest.fn(), + getHeaderRowColumn: jest.fn(), + render: jest.fn(), +}; + +describe('SelectFilter', () => { + let divContainer: HTMLDivElement; + let filter: SelectFilter; + let filterArguments: FilterArguments; + let spyGetHeaderRow; + let mockColumn: Column; + let collectionService: CollectionService; + let translate: TranslateService; + + beforeEach(() => { + divContainer = document.createElement('div'); + divContainer.innerHTML = template; + document.body.appendChild(divContainer); + spyGetHeaderRow = jest.spyOn(gridStub, 'getHeaderRowColumn').mockReturnValue(divContainer); + + mockColumn = { + id: 'gender', field: 'gender', filterable: true, + filter: { + model: Filters.multipleSelect, + } + }; + filterArguments = { + grid: gridStub, + columnDef: mockColumn, + callback: jest.fn() + }; + + TestBed.configureTestingModule({ + providers: [CollectionService], + imports: [TranslateModule.forRoot()] + }); + collectionService = TestBed.get(CollectionService); + translate = TestBed.get(TranslateService); + + translate.setTranslation('en', { + ALL_SELECTED: 'All Selected', + FEMALE: 'Female', + MALE: 'Male', + OK: 'OK', + OTHER: 'Other', + SELECT_ALL: 'Select All', + X_OF_Y_SELECTED: '# of % selected', + }); + translate.setTranslation('fr', { + ALL_SELECTED: 'Tout sélectionnés', + FEMALE: 'Femme', + MALE: 'Mâle', + OK: 'Terminé', + OTHER: 'Autre', + SELECT_ALL: 'Sélectionner tout', + X_OF_Y_SELECTED: '# de % sélectionnés', + }); + translate.setDefaultLang('en'); + + filter = new SelectFilter(translate, collectionService); + }); + + afterEach(() => { + filter.destroy(); + }); + + 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.'); + }); + + 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); + } 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(); + } + }); + + it('should throw an error when collection is not a valid array', (done) => { + try { + // @ts-ignore + mockColumn.filter.collection = { hello: 'world' }; + filter.init(filterArguments, true); + } catch (e) { + expect(e.toString()).toContain(`The "collection" passed to the Select Filter is not a valid array.`); + done(); + } + }); + + 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); + } 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(); + } + }); + + it('should throw an error when "enableTranslateLabel" is set without a valid TranslateService', (done) => { + try { + translate = undefined; + mockColumn.filter.enableTranslateLabel = true; + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + filter = new SelectFilter(translate, collectionService); + + filter.init(filterArguments, true); + } 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(); + } + }); + + it('should initialize the filter', () => { + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + filter.init(filterArguments, true); + const filterCount = divContainer.querySelectorAll('select.ms-filter.search-filter.filter-gender').length; + + expect(spyGetHeaderRow).toHaveBeenCalled(); + expect(filterCount).toBe(1); + }); + + 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); + const filterCount = divContainer.querySelectorAll('select.ms-filter.search-filter.filter-gender').length; + + expect(spyGetHeaderRow).toHaveBeenCalled(); + expect(filterCount).toBe(1); + expect(filter.isMultipleSelect).toBe(true); + }); + + it('should have a placeholder when defined in its column definition', () => { + const testValue = 'test placeholder'; + mockColumn.filter.placeholder = testValue; + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + + filter.init(filterArguments, true); + const filterElm = divContainer.querySelector('.ms-filter.search-filter.filter-gender .placeholder'); + + expect(filterElm.innerHTML).toBe(testValue); + }); + + it('should trigger multiple select change event and expect the callback to be called with the search terms we select from dropdown list', () => { + const spyCallback = jest.spyOn(filterArguments, 'callback'); + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + + filter.init(filterArguments, true); + 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`); + filterBtnElm.click(); + + // we can use property "checked" or dispatch an event + filterListElm[0].dispatchEvent(new CustomEvent('click')); + filterOkElm.click(); + + const filterFilledElms = divContainer.querySelectorAll('.ms-parent.ms-filter.search-filter.filter-gender.filled'); + expect(filterListElm.length).toBe(2); + expect(filterFilledElms.length).toBe(1); + expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, operator: 'IN', searchTerms: ['male'], shouldTriggerQuery: true }); + }); + + it('should trigger multiple select change event and expect this to work with a regular array of strings', () => { + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + mockColumn.filter.collection = ['male', 'female']; + filter.init(filterArguments, true); + 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`); + filterBtnElm.click(); + + // here we use "checked" property instead of dispatching an event + filterListElm[0].checked = true; + filterOkElm.click(); + + const filterFilledElms = divContainer.querySelectorAll('.ms-parent.ms-filter.search-filter.filter-gender.filled'); + expect(filterListElm.length).toBe(2); + expect(filterFilledElms.length).toBe(1); + expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, operator: 'IN', searchTerms: ['male'], shouldTriggerQuery: true }); + }); + + it('should pass a different operator then trigger an input change event and expect the callback to be called with the search terms we select from dropdown list', () => { + mockColumn.filter.operator = 'NIN'; + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filter.init(filterArguments, true); + 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`); + filterBtnElm.click(); + + filterListElm[0].checked = true; + filterOkElm.click(); + + const filterFilledElms = divContainer.querySelectorAll('.ms-parent.ms-filter.search-filter.filter-gender.filled'); + expect(filterListElm.length).toBe(2); + expect(filterFilledElms.length).toBe(1); + expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, operator: 'NIN', searchTerms: ['male'], shouldTriggerQuery: true }); + }); + + 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.setValues('female'); + const values = filter.getValues(); + + expect(values).toEqual(['female']); + expect(values.length).toBe(1); + }); + + 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); + const values = filter.getValues(); + + expect(values).toEqual([]); + expect(values.length).toBe(0); + }); + + it('should have empty array returned from "getValues" even when filter is not yet created', () => { + const values = filter.getValues(); + + expect(values).toEqual([]); + expect(values.length).toBe(0); + }); + + it('should create the multi-select filter with a default search term when passed as a filter argument', () => { + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filterArguments.searchTerms = ['female']; + filter.init(filterArguments, true); + 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(2); + expect(filterFilledElms.length).toBe(1); + expect(filterListElm[1].checked).toBe(true); + expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, operator: 'IN', searchTerms: ['female'], shouldTriggerQuery: true }); + }); + + 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', () => { + const spyCallback = jest.spyOn(filterArguments, 'callback'); + mockColumn.filter.collection = ['male', 'female']; + + filterArguments.searchTerms = ['female']; + filter.init(filterArguments, true); + 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(2); + expect(filterFilledElms.length).toBe(1); + expect(filterListElm[1].checked).toBe(true); + expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, operator: 'IN', searchTerms: ['female'], shouldTriggerQuery: true }); + }); + + it('should create the multi-select filter and sort the string collection when "collectionSortBy" is set', () => { + mockColumn.filter = { + collection: ['other', 'male', 'female'], + collectionSortBy: { + sortDesc: true, + fieldType: FieldType.string + } + }; + + filter.init(filterArguments, true); + 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(); + + expect(filterListElm.length).toBe(3); + expect(filterListElm[0].textContent).toBe('other'); + expect(filterListElm[1].textContent).toBe('male'); + expect(filterListElm[2].textContent).toBe('female'); + }); + + it('should create the multi-select filter and sort the value/label pair collection when "collectionSortBy" is set', () => { + mockColumn.filter = { + collection: [{ value: 'other', description: 'other' }, { value: 'male', description: 'male' }, { value: 'female', description: 'female' }], + collectionSortBy: { + property: 'value', + sortDesc: false, + fieldType: FieldType.string + }, + customStructure: { + value: 'value', + label: 'description', + }, + }; + + filter.init(filterArguments, true); + 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(); + + expect(filterListElm.length).toBe(3); + expect(filterListElm[0].textContent).toBe('female'); + expect(filterListElm[1].textContent).toBe('male'); + expect(filterListElm[2].textContent).toBe('other'); + }); + + it('should create the multi-select filter and filter the string collection when "collectionFilterBy" is set', () => { + mockColumn.filter = { + collection: ['other', 'male', 'female'], + collectionFilterBy: { + operator: OperatorType.equal, + value: 'other' + } + }; + + filter.init(filterArguments, true); + 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(); + + expect(filterListElm.length).toBe(1); + expect(filterListElm[0].textContent).toBe('other'); + }); + + it('should create the multi-select filter and filter the value/label pair collection when "collectionFilterBy" is set', () => { + mockColumn.filter = { + collection: [{ value: 'other', description: 'other' }, { value: 'male', description: 'male' }, { value: 'female', description: 'female' }], + collectionFilterBy: [ + { property: 'value', operator: OperatorType.notEqual, value: 'other' }, + { property: 'value', operator: OperatorType.notEqual, value: 'male' } + ], + customStructure: { + value: 'value', + label: 'description', + }, + }; + + filter.init(filterArguments, true); + 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(); + + expect(filterListElm.length).toBe(1); + expect(filterListElm[0].textContent).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"', () => { + mockColumn.filter = { + collection: [{ value: 'other', description: 'other' }, { value: 'male', description: 'male' }, { value: 'female', description: 'female' }], + collectionFilterBy: [ + { property: 'value', operator: OperatorType.equal, value: 'other' }, + { property: 'value', operator: OperatorType.equal, value: 'male' } + ], + collectionOptions: { + filterResultAfterEachPass: 'merge' + }, + customStructure: { + value: 'value', + label: 'description', + }, + }; + + filter.init(filterArguments, true); + 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(); + + expect(filterListElm.length).toBe(2); + expect(filterListElm[0].textContent).toBe('other'); + expect(filterListElm[1].textContent).toBe('male'); + }); + + it('should create the multi-select filter with a value/label pair collection that is inside an object when "collectionInObjectProperty" is defined with a dot notation', () => { + mockColumn.filter = { + // @ts-ignore + collection: { deep: { myCollection: [{ value: 'other', description: 'other' }, { value: 'male', description: 'male' }, { value: 'female', description: 'female' }] } }, + collectionOptions: { + collectionInObjectProperty: 'deep.myCollection' + }, + customStructure: { + value: 'value', + label: 'description', + }, + }; + + filter.init(filterArguments, true); + 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(); + + expect(filterListElm.length).toBe(3); + expect(filterListElm[0].textContent).toBe('other'); + expect(filterListElm[1].textContent).toBe('male'); + expect(filterListElm[2].textContent).toBe('female'); + }); + + it('should create the multi-select filter with a value/label pair collectionAsync that is inside an object when "collectionInObjectProperty" is defined with a dot notation', (done) => { + mockColumn.filter = { + collectionAsync: of({ deep: { myCollection: [{ value: 'other', description: 'other' }, { value: 'male', description: 'male' }, { value: 'female', description: 'female' }] } }), + collectionOptions: { + collectionInObjectProperty: 'deep.myCollection' + }, + customStructure: { + value: 'value', + label: 'description', + }, + }; + + filter.init(filterArguments, true); + + setTimeout(() => { + 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(); + + expect(filterListElm.length).toBe(3); + expect(filterListElm[0].textContent).toBe('other'); + expect(filterListElm[1].textContent).toBe('male'); + expect(filterListElm[2].textContent).toBe('female'); + done(); + }); + }); + + it('should create the multi-select filter with a default search term when using "collectionAsync" as an Observable', (done) => { + const spyCallback = jest.spyOn(filterArguments, 'callback'); + mockColumn.filter.collectionAsync = of(['male', 'female']); + + filterArguments.searchTerms = ['female']; + filter.init(filterArguments, true); + + setTimeout(() => { + 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(2); + expect(filterFilledElms.length).toBe(1); + expect(filterListElm[1].checked).toBe(true); + expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, operator: 'IN', searchTerms: ['female'], shouldTriggerQuery: true }); + done(); + }); + }); + + it('should create the multi-select filter with a "collectionAsync" as an Observable and be able to call next on it', (done) => { + const mockCollection = ['male', 'female']; + mockColumn.filter.collectionAsync = of(mockCollection); + + filterArguments.searchTerms = ['female']; + filter.init(filterArguments, true); + + setTimeout(() => { + 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]`); + filterBtnElm.click(); + + expect(filterListElm.length).toBe(2); + expect(filterListElm[1].checked).toBe(true); + + // after await (or timeout delay) we'll get the Subject Observable + mockCollection.push('other'); + (mockColumn.filter.collectionAsync as Subject).next(mockCollection); + + const filterUpdatedListElm = divContainer.querySelectorAll(`[name=filter-gender].ms-drop ul>li input[type=checkbox]`); + expect(filterUpdatedListElm.length).toBe(3); + done(); + }); + }); + + xit('should throw an error when "collectionAsync" Observable does not return a valid array', (done) => { + try { + mockColumn.filter.collectionAsync = of({ hello: 'world' }); + filter.init(filterArguments, true); + } 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(); + } + }); + + it('should create the multi-select filter with a default search term and have the HTML rendered when "enableRenderHtml" is set', () => { + mockColumn.filter = { + enableRenderHtml: true, + collection: [{ value: true, label: 'True', labelPrefix: ` ` }, { value: false, label: 'False' }], + customStructure: { + value: 'isEffort', + label: 'label', + labelPrefix: 'labelPrefix', + }, + }; + + filter.init(filterArguments, true); + 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(); + + expect(filterListElm.length).toBe(2); + expect(filterListElm[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 ` }, { value: false, label: 'False' }], + customStructure: { + value: 'isEffort', + label: 'label', + labelPrefix: 'labelPrefix', + }, + }; + + filter.init(filterArguments, true); + 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(); + + expect(filterListElm.length).toBe(2); + expect(filterListElm[0].innerHTML).toBe(' True'); + }); + + 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', () => { + filterArguments.searchTerms = ['female']; + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + mockColumn.filter.collectionOptions = { addBlankEntry: true }; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filter.init(filterArguments, true); + 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].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.clear(); + const filterFilledElms = divContainer.querySelectorAll('.ms-parent.ms-filter.search-filter.filter-gender.filled'); + + expect(filter.searchTerms.length).toBe(0); + expect(filterFilledElms.length).toBe(0); + expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: true }); + }); + + it('should trigger a callback with the clear filter but without querying when when calling the "clear" method with False as argument', () => { + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filterArguments.searchTerms = ['female']; + filter.init(filterArguments, true); + filter.clear(false); + const filterFilledElms = divContainer.querySelectorAll('.ms-parent.ms-filter.search-filter.filter-gender.filled'); + + expect(filter.searchTerms.length).toBe(0); + expect(filterFilledElms.length).toBe(0); + expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: false }); + }); + + it('should work with English locale when locale is changed', (done) => { + translate.use('en'); + gridOptionMock.enableTranslate = true; + mockColumn.filter = { + enableTranslateLabel: true, + collection: [ + { value: 'other', labelKey: 'OTHER' }, + { value: 'male', labelKey: 'MALE' }, + { value: 'female', labelKey: 'FEMALE' } + ], + filterOptions: { minimumCountSelected: 1 } + }; + + filterArguments.searchTerms = ['male', 'female']; + filter.init(filterArguments, true); + + 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'); + const filterListElm = divContainer.querySelectorAll(`[name=filter-gender].ms-drop ul>li span`); + const filterOkElm = divContainer.querySelector(`[name=filter-gender].ms-drop .ms-ok-button`); + const filterParentElm = divContainer.querySelector(`.ms-parent.ms-filter.search-filter.filter-gender button`); + filterBtnElm.click(); + + expect(filterListElm.length).toBe(3); + expect(filterListElm[0].textContent).toBe('Other'); + expect(filterListElm[1].textContent).toBe('Male'); + expect(filterListElm[2].textContent).toBe('Female'); + expect(filterOkElm.textContent).toBe('OK'); + expect(filterSelectAllElm.textContent).toBe('Select All'); + expect(filterParentElm.textContent).toBe('2 of 3 selected'); + done(); + }); + }); + + it('should work with French locale when locale is changed', (done) => { + translate.use('fr'); + gridOptionMock.enableTranslate = true; + mockColumn.filter = { + enableTranslateLabel: true, + collection: [ + { value: 'other', labelKey: 'OTHER' }, + { value: 'male', labelKey: 'MALE' }, + { value: 'female', labelKey: 'FEMALE' } + ], + filterOptions: { minimumCountSelected: 1 } + }; + + filterArguments.searchTerms = ['male', 'female']; + filter.init(filterArguments, true); + 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'); + const filterListElm = divContainer.querySelectorAll(`[name=filter-gender].ms-drop ul>li span`); + const filterOkElm = divContainer.querySelector(`[name=filter-gender].ms-drop .ms-ok-button`); + const filterParentElm = divContainer.querySelector(`.ms-parent.ms-filter.search-filter.filter-gender button`); + filterBtnElm.click(); + + expect(filterListElm.length).toBe(3); + expect(filterListElm[0].textContent).toBe('Autre'); + expect(filterListElm[1].textContent).toBe('Mâle'); + expect(filterListElm[2].textContent).toBe('Femme'); + expect(filterOkElm.textContent).toBe('Terminé'); + expect(filterSelectAllElm.textContent).toBe('Sélectionner tout'); + expect(filterParentElm.textContent).toBe('2 de 3 sélectionnés'); + done(); + }); + }); +}); diff --git a/src/app/modules/angular-slickgrid/filters/__tests__/selectFilterNoLibLoaded.spec.ts b/src/app/modules/angular-slickgrid/filters/__tests__/selectFilterNoLibLoaded.spec.ts new file mode 100644 index 000000000..42b0378a8 --- /dev/null +++ b/src/app/modules/angular-slickgrid/filters/__tests__/selectFilterNoLibLoaded.spec.ts @@ -0,0 +1,76 @@ +// import 3rd party lib multiple-select for the tests +// import '../../../../../assets/lib/multiple-select/multiple-select'; + +import { TestBed } from '@angular/core/testing'; +import { TranslateService, TranslateModule } from '@ngx-translate/core'; +import { Column, FilterArguments, GridOption } from '../../models'; +import { CollectionService } from '../../services/collection.service'; +import { Filters } from '..'; +import { SelectFilter } from '../selectFilter'; + +const containerId = 'demo-container'; + +// define a
container to simulate the grid container +const template = `
`; + +const gridOptionMock = { + enableFiltering: true, + enableFilterTrimWhiteSpace: true, +} as GridOption; + +const collectionServiceStub = { + +} as CollectionService; + +const gridStub = { + getOptions: () => gridOptionMock, + getColumns: jest.fn(), + getHeaderRowColumn: jest.fn(), + render: jest.fn(), +}; + +describe('SelectFilter', () => { + let divContainer: HTMLDivElement; + let filter: SelectFilter; + let filterArguments: FilterArguments; + let spyGetHeaderRow; + let mockColumn: Column; + let translate: TranslateService; + + beforeEach(() => { + divContainer = document.createElement('div'); + divContainer.innerHTML = template; + document.body.appendChild(divContainer); + spyGetHeaderRow = jest.spyOn(gridStub, 'getHeaderRowColumn').mockReturnValue(divContainer); + + mockColumn = { + id: 'gender', field: 'gender', filterable: true, + filter: { + model: Filters.select, + collection: [{ value: '', label: '' }, { value: 'male', label: 'male' }, { value: 'female', label: 'female' }] + } + }; + filterArguments = { + grid: gridStub, + columnDef: mockColumn, + callback: jest.fn() + }; + + TestBed.configureTestingModule({ + providers: [{ provide: CollectionService, useValue: collectionServiceStub }], + imports: [TranslateModule.forRoot()] + }); + translate = TestBed.get(TranslateService); + translate.setDefaultLang('en'); + + filter = new SelectFilter(translate, collectionServiceStub); + }); + + afterEach(() => { + filter.destroy(); + }); + + it('should throw an error when multiple-select.js is not provided or imported', () => { + expect(() => filter.init(filterArguments, true)).toThrowError(`multiple-select.js was not found, make sure to modify your "angular-cli.json" file`); + }); +}); diff --git a/src/app/modules/angular-slickgrid/filters/__tests__/singleSelectFilter.spec.ts b/src/app/modules/angular-slickgrid/filters/__tests__/singleSelectFilter.spec.ts new file mode 100644 index 000000000..b340ae896 --- /dev/null +++ b/src/app/modules/angular-slickgrid/filters/__tests__/singleSelectFilter.spec.ts @@ -0,0 +1,170 @@ +// import 3rd party lib multiple-select for the tests +import '../../../../../assets/lib/multiple-select/multiple-select'; + +import { TestBed } from '@angular/core/testing'; +import { TranslateService, TranslateModule } from '@ngx-translate/core'; +import { Column, FilterArguments, GridOption } from '../../models'; +import { CollectionService } from '../../services/collection.service'; +import { Filters } from '..'; +import { SingleSelectFilter } from '../singleSelectFilter'; +import { of, Subject } from 'rxjs'; + +const containerId = 'demo-container'; + +// define a
container to simulate the grid container +const template = `
`; + +const gridOptionMock = { + enableFiltering: true, + enableFilterTrimWhiteSpace: true, +} as GridOption; + +const gridStub = { + getOptions: () => gridOptionMock, + getColumns: jest.fn(), + getHeaderRowColumn: jest.fn(), + render: jest.fn(), +}; + +describe('SingleSelectFilter', () => { + let divContainer: HTMLDivElement; + let filter: SingleSelectFilter; + let filterArguments: FilterArguments; + let spyGetHeaderRow; + let mockColumn: Column; + let collectionService: CollectionService; + let translate: TranslateService; + + beforeEach(() => { + divContainer = document.createElement('div'); + divContainer.innerHTML = template; + document.body.appendChild(divContainer); + spyGetHeaderRow = jest.spyOn(gridStub, 'getHeaderRowColumn').mockReturnValue(divContainer); + + mockColumn = { + id: 'gender', field: 'gender', filterable: true, + filter: { + model: Filters.singleSelect, + } + }; + filterArguments = { + grid: gridStub, + columnDef: mockColumn, + callback: jest.fn() + }; + + TestBed.configureTestingModule({ + providers: [CollectionService], + imports: [TranslateModule.forRoot()] + }); + collectionService = TestBed.get(CollectionService); + translate = TestBed.get(TranslateService); + + translate.setTranslation('en', { + FEMALE: 'Female', + MALE: 'Male', + OTHER: 'Other', + }); + translate.setTranslation('fr', { + FEMALE: 'Femme', + MALE: 'Mâle', + OTHER: 'Autre', + }); + translate.setDefaultLang('en'); + + filter = new SingleSelectFilter(translate, collectionService); + }); + + afterEach(() => { + filter.destroy(); + }); + + it('should be a single-select filter', () => { + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + filter = new SingleSelectFilter(translate, collectionService); + filter.init(filterArguments, true); + const filterCount = divContainer.querySelectorAll('select.ms-filter.search-filter.filter-gender').length; + + expect(spyGetHeaderRow).toHaveBeenCalled(); + expect(filterCount).toBe(1); + expect(filter.isMultipleSelect).toBe(false); + }); + + it('should trigger single select change event and expect the callback to be called when we select a single search term from dropdown list', () => { + const spyCallback = jest.spyOn(filterArguments, 'callback'); + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + + filter.init(filterArguments, true); + 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=radio]`); + filterBtnElm.click(); + + filterListElm[1].dispatchEvent(new CustomEvent('click')); + + const filterFilledElms = divContainer.querySelectorAll('.ms-parent.ms-filter.search-filter.filter-gender.filled'); + expect(filterListElm.length).toBe(2); + expect(filterFilledElms.length).toBe(1); + expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, operator: 'EQ', searchTerms: ['female'], shouldTriggerQuery: true }); + }); + + it('should work with English locale when locale is changed', (done) => { + translate.use('en'); + gridOptionMock.enableTranslate = true; + mockColumn.filter = { + enableTranslateLabel: true, + collection: [ + { value: 'other', labelKey: 'OTHER' }, + { value: 'male', labelKey: 'MALE' }, + { value: 'female', labelKey: 'FEMALE' } + ], + filterOptions: { minimumCountSelected: 1 } + }; + + filterArguments.searchTerms = ['male', 'female']; + filter.init(filterArguments, true); + + setTimeout(() => { + 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`); + const filterOkElm = divContainer.querySelectorAll(`[name=filter-gender].ms-drop .ms-ok-button`); + const filterSelectAllElm = divContainer.querySelectorAll('.filter-gender .ms-select-all label span'); + filterBtnElm.click(); + + expect(filterOkElm.length).toBe(0); + expect(filterSelectAllElm.length).toBe(0); + expect(filterListElm.length).toBe(3); + expect(filterListElm[0].textContent).toBe('Other'); + expect(filterListElm[1].textContent).toBe('Male'); + expect(filterListElm[2].textContent).toBe('Female'); + done(); + }); + }); + + it('should work with French locale when locale is changed', (done) => { + translate.use('fr'); + gridOptionMock.enableTranslate = true; + mockColumn.filter = { + enableTranslateLabel: true, + collection: [ + { value: 'other', labelKey: 'OTHER' }, + { value: 'male', labelKey: 'MALE' }, + { value: 'female', labelKey: 'FEMALE' } + ], + filterOptions: { minimumCountSelected: 1 } + }; + + filterArguments.searchTerms = ['male', 'female']; + filter.init(filterArguments, true); + setTimeout(() => { + 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(); + + expect(filterListElm.length).toBe(3); + expect(filterListElm[0].textContent).toBe('Autre'); + expect(filterListElm[1].textContent).toBe('Mâle'); + expect(filterListElm[2].textContent).toBe('Femme'); + done(); + }); + }); +}); diff --git a/src/app/modules/angular-slickgrid/filters/__tests__/sliderRangeFilter.spec.ts b/src/app/modules/angular-slickgrid/filters/__tests__/sliderRangeFilter.spec.ts index a9a2cbae6..eda9168ca 100644 --- a/src/app/modules/angular-slickgrid/filters/__tests__/sliderRangeFilter.spec.ts +++ b/src/app/modules/angular-slickgrid/filters/__tests__/sliderRangeFilter.spec.ts @@ -51,21 +51,23 @@ describe('SliderRangeFilter', () => { expect(() => filter.init(null)).toThrowError('[Angular-SlickGrid] A filter must always have an "init()" with valid arguments.'); }); - it('should throw an error when trying override the slider "change" method', () => { + it('should throw an error when trying override the slider "change" method', (done) => { try { mockColumn.filter.filterOptions = { change: () => { } } as JQueryUiSliderOption; filter.init(filterArguments); } catch (e) { expect(e.toString()).toContain(`[Angular-Slickgrid] You cannot override the "change" and/or the "slide" callback methods`); + done(); } }); - it('should throw an error when trying override the slider "slide" method', () => { + it('should throw an error when trying override the slider "slide" method', (done) => { try { mockColumn.filter.filterOptions = { slide: () => { } } as JQueryUiSliderOption; filter.init(filterArguments); } catch (e) { expect(e.toString()).toContain(`[Angular-Slickgrid] You cannot override the "change" and/or the "slide" callback methods`); + done(); } }); diff --git a/src/app/modules/angular-slickgrid/filters/autoCompleteFilter.ts b/src/app/modules/angular-slickgrid/filters/autoCompleteFilter.ts index 6422624e6..517d56782 100644 --- a/src/app/modules/angular-slickgrid/filters/autoCompleteFilter.ts +++ b/src/app/modules/angular-slickgrid/filters/autoCompleteFilter.ts @@ -211,8 +211,9 @@ export class AutoCompleteFilter implements Filter { * and reinitialize filter collection with this new collection */ protected renderDomElementFromCollectionAsync(collection) { - if (this.collectionOptions && this.collectionOptions.collectionInObjectProperty) { - collection = getDescendantProperty(collection, this.collectionOptions.collectionInObjectProperty); + if (this.collectionOptions && (this.collectionOptions.collectionInsideObjectProperty || this.collectionOptions.collectionInObjectProperty)) { + const collectionInsideObjectProperty = this.collectionOptions.collectionInsideObjectProperty || this.collectionOptions.collectionInObjectProperty; + collection = getDescendantProperty(collection, collectionInsideObjectProperty); } if (!Array.isArray(collection)) { throw new Error('Something went wrong while trying to pull the collection from the "collectionAsync" call in the AutoComplete Filter, the collection is not a valid array.'); @@ -227,8 +228,9 @@ export class AutoCompleteFilter implements Filter { } protected renderDomElement(collection: any[]) { - if (!Array.isArray(collection) && this.collectionOptions && this.collectionOptions.collectionInObjectProperty) { - collection = getDescendantProperty(collection, this.collectionOptions.collectionInObjectProperty); + if (!Array.isArray(collection) && this.collectionOptions && (this.collectionOptions.collectionInsideObjectProperty || this.collectionOptions.collectionInObjectProperty)) { + const collectionInsideObjectProperty = this.collectionOptions.collectionInsideObjectProperty || this.collectionOptions.collectionInObjectProperty; + collection = getDescendantProperty(collection, collectionInsideObjectProperty); } if (!Array.isArray(collection)) { throw new Error('The "collection" passed to the Autocomplete Filter is not a valid array'); diff --git a/src/app/modules/angular-slickgrid/filters/selectFilter.ts b/src/app/modules/angular-slickgrid/filters/selectFilter.ts index e6cbca9d1..a6b28a35f 100644 --- a/src/app/modules/angular-slickgrid/filters/selectFilter.ts +++ b/src/app/modules/angular-slickgrid/filters/selectFilter.ts @@ -28,6 +28,7 @@ declare var $: any; export class SelectFilter implements Filter { private _isFilterFirstRender = true; + private _isMultipleSelect = true; private _locales: Locale; private _shouldTriggerQuery = true; @@ -57,7 +58,9 @@ export class SelectFilter implements Filter { /** * Initialize the Filter */ - constructor(@Optional() protected translate: TranslateService, protected collectionService: CollectionService, protected isMultipleSelect = true) { } + constructor(@Optional() protected translate: TranslateService, protected collectionService: CollectionService, isMultipleSelect = true) { + this._isMultipleSelect = isMultipleSelect; + } /** Getter for the Column Filter itself */ protected get columnFilter(): ColumnFilter { @@ -79,6 +82,11 @@ export class SelectFilter implements Filter { return (this.grid && this.grid.getOptions) ? this.grid.getOptions() : {}; } + /** Getter to know if the current filter is a multiple-select (false means it's a single select) */ + get isMultipleSelect(): boolean { + return this._isMultipleSelect; + } + /** Getter for the filter operator */ get operator(): OperatorType | OperatorString { if (this.columnDef && this.columnDef.filter && this.columnDef.filter.operator) { @@ -91,6 +99,9 @@ export class SelectFilter implements Filter { * Initialize the filter template */ init(args: FilterArguments, isFilterFirstRender: boolean) { + if (!args) { + throw new Error('[Angular-SlickGrid] A filter must always have an "init()" with valid arguments.'); + } this._isFilterFirstRender = isFilterFirstRender; this.grid = args.grid; this.callback = args.callback; @@ -109,7 +120,7 @@ export class SelectFilter implements Filter { this.valueName = this.customStructure && this.customStructure.value || 'value'; if (this.enableTranslateLabel && !this.gridOptions.enableTranslate && (!this.translate || typeof this.translate.instant !== 'function')) { - throw new Error(`[select-editor] The ngx-translate TranslateService is required for the Select Filter to work correctly`); + throw new Error(`[select-filter] The ngx-translate TranslateService is required for the Select Filter to work correctly when "enableTranslateLabel" is set.`); } // get locales provided by user in forRoot or else use default English locales via the Constants @@ -171,11 +182,22 @@ export class SelectFilter implements Filter { this.subscriptions = unsubscribeAllObservables(this.subscriptions); } + /** + * Get selected values retrieved from the multiple-selected element + * @params selected items + */ + getValues(): any[] { + if (this.$filterElm && typeof this.$filterElm.multipleSelect === 'function') { + return this.$filterElm.multipleSelect('getSelects'); + } + return []; + } + /** * Set value(s) on the DOM element */ setValues(values: SearchTerm | SearchTerm[]) { - if (values) { + if (values && this.$filterElm && typeof this.$filterElm.multipleSelect === 'function') { values = Array.isArray(values) ? values : [values]; this.$filterElm.multipleSelect('setSelects', values); } @@ -248,8 +270,9 @@ export class SelectFilter implements Filter { * and reinitialize filter collection with this new collection */ protected renderDomElementFromCollectionAsync(collection) { - if (this.collectionOptions && this.collectionOptions.collectionInObjectProperty) { - collection = getDescendantProperty(collection, this.collectionOptions.collectionInObjectProperty); + if (this.collectionOptions && (this.collectionOptions.collectionInsideObjectProperty || this.collectionOptions.collectionInObjectProperty)) { + const collectionInsideObjectProperty = this.collectionOptions.collectionInsideObjectProperty || this.collectionOptions.collectionInObjectProperty; + collection = getDescendantProperty(collection, collectionInsideObjectProperty); } if (!Array.isArray(collection)) { throw new Error('Something went wrong while trying to pull the collection from the "collectionAsync" call in the Select Filter, the collection is not a valid array.'); @@ -264,11 +287,12 @@ export class SelectFilter implements Filter { } protected renderDomElement(collection) { - if (!Array.isArray(collection) && this.collectionOptions && this.collectionOptions.collectionInObjectProperty) { - collection = getDescendantProperty(collection, this.collectionOptions.collectionInObjectProperty); + if (!Array.isArray(collection) && this.collectionOptions && (this.collectionOptions.collectionInsideObjectProperty || this.collectionOptions.collectionInObjectProperty)) { + const collectionInsideObjectProperty = this.collectionOptions.collectionInsideObjectProperty || this.collectionOptions.collectionInObjectProperty; + collection = getDescendantProperty(collection, collectionInsideObjectProperty); } if (!Array.isArray(collection)) { - throw new Error('The "collection" passed to the Select Filter is not a valid array'); + 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 @@ -295,6 +319,7 @@ export class SelectFilter implements Filter { let options = ''; const fieldId = this.columnDef && this.columnDef.id; const separatorBetweenLabels = this.collectionOptions && this.collectionOptions.separatorBetweenTextLabels || ''; + const isEnableTranslate = this.gridOptions && this.gridOptions.enableTranslate; const isRenderHtmlEnabled = this.columnFilter && this.columnFilter.enableRenderHtml || false; const sanitizedOptions = this.gridOptions && this.gridOptions.sanitizeHtmlOptions || {}; @@ -318,16 +343,16 @@ export class SelectFilter implements Filter { } const labelKey = (option.labelKey || option[this.labelName]) as string; const selected = (searchTerms.findIndex((term) => term === option[this.valueName]) >= 0) ? 'selected' : ''; - const labelText = ((option.labelKey || this.enableTranslateLabel) && labelKey && this.gridOptions.enableTranslate) ? this.translate && this.translate.instant(labelKey || ' ') : labelKey; + const labelText = ((option.labelKey || this.enableTranslateLabel) && labelKey && isEnableTranslate) ? this.translate && this.translate.instant(labelKey || ' ') : labelKey; 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 // also translate prefix/suffix if enableTranslateLabel is true and text is a string - prefixText = (this.enableTranslateLabel && this.gridOptions.enableTranslate && prefixText && typeof prefixText === 'string') ? this.translate && this.translate.instant(prefixText || ' ') : prefixText; - suffixText = (this.enableTranslateLabel && this.gridOptions.enableTranslate && suffixText && typeof suffixText === 'string') ? this.translate && this.translate.instant(suffixText || ' ') : suffixText; - optionLabel = (this.enableTranslateLabel && this.gridOptions.enableTranslate && optionLabel && typeof optionLabel === 'string') ? this.translate && this.translate.instant(optionLabel || ' ') : optionLabel; + prefixText = (this.enableTranslateLabel && isEnableTranslate && prefixText && typeof prefixText === 'string') ? this.translate && this.translate.instant(prefixText || ' ') : prefixText; + suffixText = (this.enableTranslateLabel && isEnableTranslate && suffixText && typeof suffixText === 'string') ? this.translate && this.translate.instant(suffixText || ' ') : suffixText; + optionLabel = (this.enableTranslateLabel && isEnableTranslate && optionLabel && typeof optionLabel === 'string') ? this.translate && this.translate.instant(optionLabel || ' ') : optionLabel; // add to a temp array for joining purpose and filter out empty text const tmpOptionArray = [prefixText, labelText !== undefined ? labelText.toString() : labelText, suffixText].filter((text) => text); @@ -424,14 +449,14 @@ export class SelectFilter implements Filter { single: true, textTemplate: ($elm) => { - // render HTML code or not, by default it is sanitized and won't be rendered + // are we rendering HTML code? by default it is sanitized and won't be rendered const isRenderHtmlEnabled = this.columnDef && this.columnDef.filter && this.columnDef.filter.enableRenderHtml || false; return isRenderHtmlEnabled ? $elm.text() : $elm.html(); }, onClose: () => { // we will subscribe to the onClose event for triggering our callback // also add/remove "filled" class for styling purposes - const selectedItems = this.$filterElm.multipleSelect('getSelects'); + const selectedItems = this.getValues(); if (Array.isArray(selectedItems) && selectedItems.length > 1 || (selectedItems.length === 1 && selectedItems[0] !== '')) { this.isFilled = true; this.$filterElm.addClass('filled').siblings('div .search-filter').addClass('filled'); @@ -441,6 +466,7 @@ export class SelectFilter implements Filter { this.$filterElm.siblings('div .search-filter').removeClass('filled'); } + this.searchTerms = selectedItems; this.callback(undefined, { columnDef: this.columnDef, operator: this.operator, searchTerms: selectedItems, shouldTriggerQuery: this._shouldTriggerQuery }); // reset flag for next use this._shouldTriggerQuery = true; diff --git a/src/app/modules/angular-slickgrid/models/collectionFilterBy.interface.ts b/src/app/modules/angular-slickgrid/models/collectionFilterBy.interface.ts index 0cffd52a8..a537b7883 100644 --- a/src/app/modules/angular-slickgrid/models/collectionFilterBy.interface.ts +++ b/src/app/modules/angular-slickgrid/models/collectionFilterBy.interface.ts @@ -1,7 +1,12 @@ import { OperatorType } from './operatorType.enum'; export interface CollectionFilterBy { - property: string; + /** Object Property name when the collection is an array of objects */ + property?: string; + + /** Value to filter from the collection */ value: any; - operator?: OperatorType.equal | OperatorType.notEqual | OperatorType.contains | OperatorType.notContains; + + /** Operator to use when filtering the value from the collection, we can only use */ + operator?: OperatorType.equal | OperatorType.notEqual | OperatorType.contains | OperatorType.notContains | 'EQ' | 'NE' | 'Contains' | 'NOT_CONTAINS' | 'Not_Contains'; } diff --git a/src/app/modules/angular-slickgrid/models/collectionOption.interface.ts b/src/app/modules/angular-slickgrid/models/collectionOption.interface.ts index ec57ebb8c..de9f36b34 100644 --- a/src/app/modules/angular-slickgrid/models/collectionOption.interface.ts +++ b/src/app/modules/angular-slickgrid/models/collectionOption.interface.ts @@ -8,15 +8,18 @@ export interface CollectionOption { */ addBlankEntry?: boolean; + /** @deprecated please use "collectionInsideObjectProperty" instead */ + collectionInObjectProperty?: string; + /** * 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. - * For example if our output data is: + * For example if our output data returned by the collectionAsync is inside an object of the following format: * myData = { someProperty: { myCollection: [] }, otherProperty: 'something' } * We can pass the dot notation string - * collectionInObjectProperty: 'someProperty.myCollection' + * collectionInsideObjectProperty: 'someProperty.myCollection' */ - collectionInObjectProperty?: string; + collectionInsideObjectProperty?: string; /** * Defaults to "chain", when using multiple "collectionFilterBy", do we want to "merge" or "chain" the result after each pass? diff --git a/src/app/modules/angular-slickgrid/models/collectionSortBy.interface.ts b/src/app/modules/angular-slickgrid/models/collectionSortBy.interface.ts index 8bec31efd..e978cb656 100644 --- a/src/app/modules/angular-slickgrid/models/collectionSortBy.interface.ts +++ b/src/app/modules/angular-slickgrid/models/collectionSortBy.interface.ts @@ -1,7 +1,12 @@ -import { FieldType, OperatorType } from './index'; +import { FieldType } from './index'; export interface CollectionSortBy { - property: string; + /** Object Property name when the collection is an array of objects */ + property?: string; + + /** defaults to false, is it in a descending order? */ sortDesc?: boolean; + + /** Field type of the value or object value content */ fieldType?: FieldType; } diff --git a/src/app/modules/angular-slickgrid/models/operatorString.ts b/src/app/modules/angular-slickgrid/models/operatorString.ts index 5c405a5e1..4dbe9f939 100644 --- a/src/app/modules/angular-slickgrid/models/operatorString.ts +++ b/src/app/modules/angular-slickgrid/models/operatorString.ts @@ -1 +1 @@ -export type OperatorString = '' | '<>' | '!=' | '=' | '==' | '>' | '>=' | '<' | '<=' | '*' | 'a*' | '*z' | 'EQ' | 'GE' | 'GT' | 'NE' | 'LE' | 'LT' | 'IN' | 'NIN' | 'NOT_IN' | 'IN_CONTAINS' | 'NIN_CONTAINS' | 'NOT_IN_CONTAINS' | 'NOT_CONTAINS' | 'Not_Contains' | 'Contains' | 'EndsWith' | 'StartsWith' | 'RangeInclusive' | 'RangeExclusive'; +export type OperatorString = '' | '<>' | '!=' | '=' | '==' | '>' | '>=' | '<' | '<=' | '*' | 'a*' | '*z' | 'EQ' | 'GE' | 'GT' | 'NE' | 'LE' | 'LT' | 'IN' | 'NIN' | 'NOT_IN' | 'IN_CONTAINS' | 'NIN_CONTAINS' | 'NOT_IN_CONTAINS' | 'NOT_CONTAINS' | 'Not_Contains' | 'CONTAINS' | 'Contains' | 'EndsWith' | 'StartsWith' | 'RangeInclusive' | 'RangeExclusive'; diff --git a/src/app/modules/angular-slickgrid/services/__tests__/collection.service.spec.ts b/src/app/modules/angular-slickgrid/services/__tests__/collection.service.spec.ts index 72d50e31c..dfac12c8e 100644 --- a/src/app/modules/angular-slickgrid/services/__tests__/collection.service.spec.ts +++ b/src/app/modules/angular-slickgrid/services/__tests__/collection.service.spec.ts @@ -12,6 +12,7 @@ import { describe('CollectionService', () => { let collection = []; + let stringCollection = []; let translate: TranslateService; let service: CollectionService; @@ -51,6 +52,8 @@ describe('CollectionService', () => { { firstName: 'John', lastName: 'Doe', position: 'DEVELOPER', order: 4 }, { firstName: 'John Foo', lastName: 'Bar', position: 'SALES_REP', order: 8 }, ]; + + stringCollection = ['John', 'Jane', 'Ava Luna', '', 'Bob', 'John', null, 'John Foo']; }); afterEach(() => { @@ -61,220 +64,280 @@ describe('CollectionService', () => { expect(service).toBeTruthy(); }); - describe('filterCollection method', () => { - it('should return on the columns that have firstName filled when the filtered value is actually undefined but will be checked as an empty string', () => { - const filterBy = { property: 'firstName', operator: 'EQ', value: undefined } as CollectionFilterBy; + describe('Collection of Objects', () => { + describe('filterCollection method', () => { + it('should return on the columns that have firstName filled when the filtered value is actually undefined but will be checked as an empty string', () => { + const filterBy = { property: 'firstName', operator: 'EQ', value: undefined } as CollectionFilterBy; + + const result = service.filterCollection(collection, filterBy); + + expect(result).toEqual([ + { firstName: '', lastName: 'Cash', position: 'SALES_REP', order: 3 } + ]); + }); + + it('should return an array without certain filtered values', () => { + const filterBy = { property: 'firstName', operator: 'NE', value: 'John' } as CollectionFilterBy; + + const result = service.filterCollection(collection, filterBy); + + expect(result).toEqual([ + { firstName: 'Jane', lastName: 'Doe', position: 'FINANCE_MANAGER', order: 1 }, + { firstName: 'Ava Luna', lastName: null, position: 'HUMAN_RESOURCES', order: 13 }, + { firstName: '', lastName: 'Cash', position: 'SALES_REP', order: 3 }, + { firstName: 'Bob', lastName: 'Cash', position: 'SALES_REP', order: 0 }, + { firstName: 'John Foo', lastName: 'Bar', position: 'SALES_REP', order: 8 }, + ]); + }); + + it('should return an array without certain values filtered in a "chain" way', () => { + const filterBy = [ + { property: 'firstName', operator: 'NE', value: 'John' }, + { property: 'lastName', operator: 'NE', value: 'Doe' } + ] as CollectionFilterBy[]; + + const result1 = service.filterCollection(collection, filterBy); + const result2 = service.filterCollection(collection, filterBy, 'chain'); // chain is default + + expect(result1).toEqual([ + { firstName: 'Ava Luna', lastName: null, position: 'HUMAN_RESOURCES', order: 13 }, + { firstName: '', lastName: 'Cash', position: 'SALES_REP', order: 3 }, + { firstName: 'Bob', lastName: 'Cash', position: 'SALES_REP', order: 0 }, + { firstName: 'John Foo', lastName: 'Bar', position: 'SALES_REP', order: 8 }, + ]); + expect(result1).toEqual(result2); + }); + + it('should return an array with merged output of filtered values', () => { + const filterBy = [ + { property: 'firstName', operator: OperatorType.equal, value: 'John' }, + { property: 'lastName', value: 'Doe' } // ommitted Operator are Equal by default + ] as CollectionFilterBy[]; + + const result = service.filterCollection(collection, filterBy, FilterMultiplePassType.merge); + + expect(result).toEqual([ + // the array will have all "John" 1st, then all "Doe" + { firstName: 'John', lastName: 'Z', position: 'SALES_REP', order: 10 }, + { firstName: 'John', lastName: 'Doe', position: null, order: 5 }, + { firstName: 'John', lastName: 'Zachary', position: 'SALES_REP', order: 2 }, + { firstName: 'John', lastName: 'Doe', position: 'DEVELOPER', order: 4 }, + { firstName: 'Jane', lastName: 'Doe', position: 'FINANCE_MANAGER', order: 1 }, + ]); + }); + }); - const result = service.filterCollection(collection, filterBy); + describe('singleFilterCollection method', () => { + it('should return an array by using the "contains" filter type', () => { + const filterBy = { property: 'firstName', operator: OperatorType.contains, value: 'Foo' } as CollectionFilterBy; - expect(result).toEqual([ - { firstName: '', lastName: 'Cash', position: 'SALES_REP', order: 3 } - ]); - }); + const result = service.singleFilterCollection(collection, filterBy); - it('should return an array without certain filtered values', () => { - const filterBy = { property: 'firstName', operator: 'NE', value: 'John' } as CollectionFilterBy; + expect(result).toEqual([{ firstName: 'John Foo', lastName: 'Bar', position: 'SALES_REP', order: 8 }]); + }); - const result = service.filterCollection(collection, filterBy); + it('should return an array by using the "notContains" filter type', () => { + const filterBy = { property: 'firstName', operator: OperatorType.notContains, value: 'John' } as CollectionFilterBy; - expect(result).toEqual([ - { firstName: 'Jane', lastName: 'Doe', position: 'FINANCE_MANAGER', order: 1 }, - { firstName: 'Ava Luna', lastName: null, position: 'HUMAN_RESOURCES', order: 13 }, - { firstName: '', lastName: 'Cash', position: 'SALES_REP', order: 3 }, - { firstName: 'Bob', lastName: 'Cash', position: 'SALES_REP', order: 0 }, - { firstName: 'John Foo', lastName: 'Bar', position: 'SALES_REP', order: 8 }, - ]); - }); + const result = service.singleFilterCollection(collection, filterBy); - it('should return an array without certain filtered valuess in a "chain" way', () => { - const filterBy = [ - { property: 'firstName', operator: 'NE', value: 'John' }, - { property: 'lastName', operator: 'NE', value: 'Doe' } - ] as CollectionFilterBy[]; - - const result1 = service.filterCollection(collection, filterBy); - const result2 = service.filterCollection(collection, filterBy, 'chain'); // chain is default - - expect(result1).toEqual([ - { firstName: 'Ava Luna', lastName: null, position: 'HUMAN_RESOURCES', order: 13 }, - { firstName: '', lastName: 'Cash', position: 'SALES_REP', order: 3 }, - { firstName: 'Bob', lastName: 'Cash', position: 'SALES_REP', order: 0 }, - { firstName: 'John Foo', lastName: 'Bar', position: 'SALES_REP', order: 8 }, - ]); - expect(result1).toEqual(result2); + expect(result).toEqual([ + { firstName: 'Jane', lastName: 'Doe', position: 'FINANCE_MANAGER', order: 1 }, + { firstName: 'Ava Luna', lastName: null, position: 'HUMAN_RESOURCES', order: 13 }, + { firstName: '', lastName: 'Cash', position: 'SALES_REP', order: 3 }, + { firstName: 'Bob', lastName: 'Cash', position: 'SALES_REP', order: 0 }, + ]); + }); }); - it('should return an array with merged output of filtered values', () => { - const filterBy = [ - { property: 'firstName', operator: OperatorType.equal, value: 'John' }, - { property: 'lastName', value: 'Doe' } // ommitted operator are Equal by default - ] as CollectionFilterBy[]; - - const result = service.filterCollection(collection, filterBy, FilterMultiplePassType.merge); - - expect(result).toEqual([ - // the array will have all "John" 1st, then all "Doe" - { firstName: 'John', lastName: 'Z', position: 'SALES_REP', order: 10 }, - { firstName: 'John', lastName: 'Doe', position: null, order: 5 }, - { firstName: 'John', lastName: 'Zachary', position: 'SALES_REP', order: 2 }, - { firstName: 'John', lastName: 'Doe', position: 'DEVELOPER', order: 4 }, - { firstName: 'Jane', lastName: 'Doe', position: 'FINANCE_MANAGER', order: 1 }, - ]); + describe('sortCollection method', () => { + it('should return a collection sorted by a "dataKey"', () => { + const columnDef = { id: 'users', field: 'users', dataKey: 'lastName' } as Column; + + const result = service.sortCollection(columnDef, collection, { property: 'lastName', sortDesc: true, fieldType: FieldType.string }); + + expect(result).toEqual([ + { firstName: 'John', lastName: 'Zachary', position: 'SALES_REP', order: 2 }, + { firstName: 'John', lastName: 'Z', position: 'SALES_REP', order: 10 }, + { firstName: 'Jane', lastName: 'Doe', position: 'FINANCE_MANAGER', order: 1 }, + { firstName: 'John', lastName: 'Doe', position: null, order: 5 }, + { firstName: 'John', lastName: 'Doe', position: 'DEVELOPER', order: 4 }, + { firstName: '', lastName: 'Cash', position: 'SALES_REP', order: 3 }, + { firstName: 'Bob', lastName: 'Cash', position: 'SALES_REP', order: 0 }, + { firstName: 'John Foo', lastName: 'Bar', position: 'SALES_REP', order: 8 }, + { firstName: 'Ava Luna', lastName: null, position: 'HUMAN_RESOURCES', order: 13 }, + ]); + }); + + it('should return a collection sorted by multiple sortBy entities', () => { + const columnDef = { id: 'users', field: 'users', dataKey: 'lastName' } as Column; + const sortBy = [ + { property: 'firstName', sortDesc: false, fieldType: FieldType.string }, + { property: 'lastName', sortDesc: true, fieldType: FieldType.string }, + ] as CollectionSortBy[]; + + const result = service.sortCollection(columnDef, collection, sortBy); + + expect(result).toEqual([ + { firstName: '', lastName: 'Cash', position: 'SALES_REP', order: 3 }, + { firstName: 'Ava Luna', lastName: null, position: 'HUMAN_RESOURCES', order: 13 }, + { firstName: 'Bob', lastName: 'Cash', position: 'SALES_REP', order: 0 }, + { firstName: 'Jane', lastName: 'Doe', position: 'FINANCE_MANAGER', order: 1 }, + { firstName: 'John', lastName: 'Zachary', position: 'SALES_REP', order: 2 }, + { firstName: 'John', lastName: 'Z', position: 'SALES_REP', order: 10 }, + { firstName: 'John', lastName: 'Doe', position: null, order: 5 }, + { firstName: 'John', lastName: 'Doe', position: 'DEVELOPER', order: 4 }, + { firstName: 'John Foo', lastName: 'Bar', position: 'SALES_REP', order: 8 }, + ]); + }); + + it('should return a collection sorted by a sortyBy entity being a number', () => { + const columnDef = { id: 'users', field: 'users', dataKey: 'lastName' } as Column; + const sortBy = [ + { property: 'order', sortDesc: true, fieldType: FieldType.number }, + ] as CollectionSortBy[]; + + const result = service.sortCollection(columnDef, collection, sortBy); + + expect(result).toEqual([ + { firstName: 'Ava Luna', lastName: null, position: 'HUMAN_RESOURCES', order: 13 }, + { firstName: 'John', lastName: 'Z', position: 'SALES_REP', order: 10 }, + { firstName: 'John Foo', lastName: 'Bar', position: 'SALES_REP', order: 8 }, + { firstName: 'John', lastName: 'Doe', position: null, order: 5 }, + { firstName: 'John', lastName: 'Doe', position: 'DEVELOPER', order: 4 }, + { firstName: '', lastName: 'Cash', position: 'SALES_REP', order: 3 }, + { firstName: 'John', lastName: 'Zachary', position: 'SALES_REP', order: 2 }, + { firstName: 'Jane', lastName: 'Doe', position: 'FINANCE_MANAGER', order: 1 }, + { firstName: 'Bob', lastName: 'Cash', position: 'SALES_REP', order: 0 }, + ]); + }); + + it('should return a collection sorted by multiple sortBy entities and their translated value', () => { + translate.use('fr'); + const columnDef = { id: 'users', field: 'users', dataKey: 'lastName' } as Column; + const sortBy = [ + { property: 'firstName', sortDesc: false, fieldType: FieldType.string }, + { property: 'position', sortDesc: true }, // fieldType is string by default + ] as CollectionSortBy[]; + + const result = service.sortCollection(columnDef, collection, sortBy, true); + + expect(result).toEqual([ + { firstName: '', lastName: 'Cash', position: 'SALES_REP', order: 3 }, + { firstName: 'Ava Luna', lastName: null, position: 'HUMAN_RESOURCES', order: 13 }, + { firstName: 'Bob', lastName: 'Cash', position: 'SALES_REP', order: 0 }, + { firstName: 'Jane', lastName: 'Doe', position: 'FINANCE_MANAGER', order: 1 }, + { firstName: 'John', lastName: 'Z', position: 'SALES_REP', order: 10 }, + { firstName: 'John', lastName: 'Zachary', position: 'SALES_REP', order: 2 }, + { firstName: 'John', lastName: 'Doe', position: 'DEVELOPER', order: 4 }, + { firstName: 'John', lastName: 'Doe', position: null, order: 5 }, + { firstName: 'John Foo', lastName: 'Bar', position: 'SALES_REP', order: 8 }, + ]); + }); + + it('should return a collection sorted by a single sortBy entity and their translated value', () => { + translate.use('en'); + const columnDef = { id: 'users', field: 'users' } as Column; + const sortBy = { property: 'position', sortDesc: false } as CollectionSortBy; // fieldType is string by default + + const result = service.sortCollection(columnDef, collection, sortBy, true); + + expect(result).toEqual([ + { firstName: 'John', lastName: 'Doe', position: null, order: 5 }, + { firstName: 'John', lastName: 'Doe', position: 'DEVELOPER', order: 4 }, + { firstName: 'Jane', lastName: 'Doe', position: 'FINANCE_MANAGER', order: 1 }, + { firstName: 'Ava Luna', lastName: null, position: 'HUMAN_RESOURCES', order: 13 }, + { firstName: 'John', lastName: 'Z', position: 'SALES_REP', order: 10 }, + { firstName: '', lastName: 'Cash', position: 'SALES_REP', order: 3 }, + { firstName: 'Bob', lastName: 'Cash', position: 'SALES_REP', order: 0 }, + { firstName: 'John', lastName: 'Zachary', position: 'SALES_REP', order: 2 }, + { firstName: 'John Foo', lastName: 'Bar', position: 'SALES_REP', order: 8 }, + ]); + }); }); }); - describe('singleFilterCollection method', () => { - it('should return an array by using the "contains" filter type', () => { - const filterBy = { property: 'firstName', operator: OperatorType.contains, value: 'Foo' } as CollectionFilterBy; + describe('Collection of Strings/Numbers', () => { + describe('filterCollection method', () => { + it('should return on the columns that have firstName filled when the filtered value is actually undefined but will be checked as an empty string', () => { + const filterBy = { operator: 'EQ', value: undefined } as CollectionFilterBy; - const result = service.singleFilterCollection(collection, filterBy); + const result = service.filterCollection(stringCollection, filterBy); - expect(result).toEqual([{ firstName: 'John Foo', lastName: 'Bar', position: 'SALES_REP', order: 8 }]); - }); + expect(result).toEqual(['']); + }); - it('should return an array by using the "notContains" filter type', () => { - const filterBy = { property: 'firstName', operator: OperatorType.notContains, value: 'John' } as CollectionFilterBy; + it('should return an array without certain values filtered in a "chain" way', () => { + const filterBy = [ + { operator: 'NE', value: 'John' }, + { operator: 'NE', value: 'Bob' } + ] as CollectionFilterBy[]; - const result = service.singleFilterCollection(collection, filterBy); + const result1 = service.filterCollection(stringCollection, filterBy); + const result2 = service.filterCollection(stringCollection, filterBy, 'chain'); // chain is default - expect(result).toEqual([ - { firstName: 'Jane', lastName: 'Doe', position: 'FINANCE_MANAGER', order: 1 }, - { firstName: 'Ava Luna', lastName: null, position: 'HUMAN_RESOURCES', order: 13 }, - { firstName: '', lastName: 'Cash', position: 'SALES_REP', order: 3 }, - { firstName: 'Bob', lastName: 'Cash', position: 'SALES_REP', order: 0 }, - ]); - }); - }); + expect(result1).toEqual(['Jane', 'Ava Luna', '', null, 'John Foo']); + expect(result1).toEqual(result2); + }); - describe('sortCollection method', () => { - it('should return a collection sorted by a "dataKey"', () => { - const columnDef = { id: 'users', field: 'users', dataKey: 'lastName' } as Column; - - const result = service.sortCollection(columnDef, collection, { property: 'lastName', sortDesc: true, fieldType: FieldType.string }); - - expect(result).toEqual([ - { firstName: 'John', lastName: 'Zachary', position: 'SALES_REP', order: 2 }, - { firstName: 'John', lastName: 'Z', position: 'SALES_REP', order: 10 }, - { firstName: 'Jane', lastName: 'Doe', position: 'FINANCE_MANAGER', order: 1 }, - { firstName: 'John', lastName: 'Doe', position: null, order: 5 }, - { firstName: 'John', lastName: 'Doe', position: 'DEVELOPER', order: 4 }, - { firstName: '', lastName: 'Cash', position: 'SALES_REP', order: 3 }, - { firstName: 'Bob', lastName: 'Cash', position: 'SALES_REP', order: 0 }, - { firstName: 'John Foo', lastName: 'Bar', position: 'SALES_REP', order: 8 }, - { firstName: 'Ava Luna', lastName: null, position: 'HUMAN_RESOURCES', order: 13 }, - ]); - }); + it('should return an array with merged (unique values no duplicate) output of filtered values', () => { + const filterBy = [ + { operator: OperatorType.equal, value: 'John' }, + { value: 'Bob' } // ommitted Operator are Equal by default + ] as CollectionFilterBy[]; - it('should return a collection sorted by multiple sortBy entities', () => { - const columnDef = { id: 'users', field: 'users', dataKey: 'lastName' } as Column; - const sortBy = [ - { property: 'firstName', sortDesc: false, fieldType: FieldType.string }, - { property: 'lastName', sortDesc: true, fieldType: FieldType.string }, - ] as CollectionSortBy[]; - - const result = service.sortCollection(columnDef, collection, sortBy); - - expect(result).toEqual([ - { firstName: '', lastName: 'Cash', position: 'SALES_REP', order: 3 }, - { firstName: 'Ava Luna', lastName: null, position: 'HUMAN_RESOURCES', order: 13 }, - { firstName: 'Bob', lastName: 'Cash', position: 'SALES_REP', order: 0 }, - { firstName: 'Jane', lastName: 'Doe', position: 'FINANCE_MANAGER', order: 1 }, - { firstName: 'John', lastName: 'Zachary', position: 'SALES_REP', order: 2 }, - { firstName: 'John', lastName: 'Z', position: 'SALES_REP', order: 10 }, - { firstName: 'John', lastName: 'Doe', position: null, order: 5 }, - { firstName: 'John', lastName: 'Doe', position: 'DEVELOPER', order: 4 }, - { firstName: 'John Foo', lastName: 'Bar', position: 'SALES_REP', order: 8 }, - ]); - }); + const result = service.filterCollection(stringCollection, filterBy, FilterMultiplePassType.merge); - it('should return a collection sorted by a sortyBy entity being a number', () => { - const columnDef = { id: 'users', field: 'users', dataKey: 'lastName' } as Column; - const sortBy = [ - { property: 'order', sortDesc: true, fieldType: FieldType.number }, - ] as CollectionSortBy[]; - - const result = service.sortCollection(columnDef, collection, sortBy); - - expect(result).toEqual([ - { firstName: 'Ava Luna', lastName: null, position: 'HUMAN_RESOURCES', order: 13 }, - { firstName: 'John', lastName: 'Z', position: 'SALES_REP', order: 10 }, - { firstName: 'John Foo', lastName: 'Bar', position: 'SALES_REP', order: 8 }, - { firstName: 'John', lastName: 'Doe', position: null, order: 5 }, - { firstName: 'John', lastName: 'Doe', position: 'DEVELOPER', order: 4 }, - { firstName: '', lastName: 'Cash', position: 'SALES_REP', order: 3 }, - { firstName: 'John', lastName: 'Zachary', position: 'SALES_REP', order: 2 }, - { firstName: 'Jane', lastName: 'Doe', position: 'FINANCE_MANAGER', order: 1 }, - { firstName: 'Bob', lastName: 'Cash', position: 'SALES_REP', order: 0 }, - ]); + expect(result).toEqual(['John', 'Bob']); + }); }); - it('should return a collection sorted by multiple sortBy entities and their translated value', () => { - translate.use('fr'); - const columnDef = { id: 'users', field: 'users', dataKey: 'lastName' } as Column; - const sortBy = [ - { property: 'firstName', sortDesc: false, fieldType: FieldType.string }, - { property: 'position', sortDesc: true }, // fieldType is string by default - ] as CollectionSortBy[]; - - const result = service.sortCollection(columnDef, collection, sortBy, true); - - expect(result).toEqual([ - { firstName: '', lastName: 'Cash', position: 'SALES_REP', order: 3 }, - { firstName: 'Ava Luna', lastName: null, position: 'HUMAN_RESOURCES', order: 13 }, - { firstName: 'Bob', lastName: 'Cash', position: 'SALES_REP', order: 0 }, - { firstName: 'Jane', lastName: 'Doe', position: 'FINANCE_MANAGER', order: 1 }, - { firstName: 'John', lastName: 'Z', position: 'SALES_REP', order: 10 }, - { firstName: 'John', lastName: 'Zachary', position: 'SALES_REP', order: 2 }, - { firstName: 'John', lastName: 'Doe', position: 'DEVELOPER', order: 4 }, - { firstName: 'John', lastName: 'Doe', position: null, order: 5 }, - { firstName: 'John Foo', lastName: 'Bar', position: 'SALES_REP', order: 8 }, - ]); - }); + describe('singleFilterCollection method', () => { + // stringCollection = ['John', 'Jane', 'Ava Luna', '', 'Bob', 'John', null, 'John Foo']; - it('should return a collection sorted by a single sortBy entity and their translated value', () => { - translate.use('en'); - const columnDef = { id: 'users', field: 'users' } as Column; - const sortBy = { property: 'position', sortDesc: false } as CollectionSortBy; // fieldType is string by default - - const result = service.sortCollection(columnDef, collection, sortBy, true); - - expect(result).toEqual([ - { firstName: 'John', lastName: 'Doe', position: null, order: 5 }, - { firstName: 'John', lastName: 'Doe', position: 'DEVELOPER', order: 4 }, - { firstName: 'Jane', lastName: 'Doe', position: 'FINANCE_MANAGER', order: 1 }, - { firstName: 'Ava Luna', lastName: null, position: 'HUMAN_RESOURCES', order: 13 }, - { firstName: 'John', lastName: 'Z', position: 'SALES_REP', order: 10 }, - { firstName: '', lastName: 'Cash', position: 'SALES_REP', order: 3 }, - { firstName: 'Bob', lastName: 'Cash', position: 'SALES_REP', order: 0 }, - { firstName: 'John', lastName: 'Zachary', position: 'SALES_REP', order: 2 }, - { firstName: 'John Foo', lastName: 'Bar', position: 'SALES_REP', order: 8 }, - ]); - }); + it('should return an array by using the "contains" filter type', () => { + const filterBy = { operator: OperatorType.contains, value: 'Foo' } as CollectionFilterBy; + + const result = service.singleFilterCollection(stringCollection, filterBy); - it('should return a collection of numbers sorted', () => { - translate.use('en'); - const columnDef = { id: 'count', field: 'count', fieldType: FieldType.number } as Column; + expect(result).toEqual(['John Foo']); + }); - const result1 = service.sortCollection(columnDef, [0, -11, 3, 99999, -200], { property: '', sortDesc: false } as CollectionSortBy); - const result2 = service.sortCollection(columnDef, [0, -11, 3, 99999, -200], { property: '', sortDesc: true } as CollectionSortBy); + it('should return an array by using the "notContains" filter type', () => { + const filterBy = { operator: OperatorType.notContains, value: 'John' } as CollectionFilterBy; - expect(result1).toEqual([-200, -11, 0, 3, 99999]); - expect(result2).toEqual([99999, 3, 0, -11, -200]); + const result = service.singleFilterCollection(stringCollection, filterBy); + + expect(result).toEqual(['Jane', 'Ava Luna', '', 'Bob']); + }); }); - it('should return a collection of translation values sorted', () => { - translate.use('en'); - const roleCollection = ['SALES_REP', 'DEVELOPER', 'SALES_REP', null, 'HUMAN_RESOURCES', 'FINANCE_MANAGER', 'UNKNOWN']; - const columnDef = { id: 'count', field: 'count', fieldType: FieldType.string } as Column; + describe('sortCollection method', () => { + it('should return a collection of numbers sorted', () => { + translate.use('en'); + const columnDef = { id: 'count', field: 'count', fieldType: FieldType.number } as Column; + + const result1 = service.sortCollection(columnDef, [0, -11, 3, 99999, -200], { sortDesc: false } as CollectionSortBy); + const result2 = service.sortCollection(columnDef, [0, -11, 3, 99999, -200], { sortDesc: true } as CollectionSortBy); - const result1 = service.sortCollection(columnDef, [...roleCollection], { property: '', sortDesc: false } as CollectionSortBy, true); - const result2 = service.sortCollection(columnDef, [...roleCollection], { property: '', sortDesc: true } as CollectionSortBy, true); + expect(result1).toEqual([-200, -11, 0, 3, 99999]); + expect(result2).toEqual([99999, 3, 0, -11, -200]); + }); - expect(result1).toEqual([null, 'DEVELOPER', 'FINANCE_MANAGER', 'HUMAN_RESOURCES', 'SALES_REP', 'SALES_REP', 'UNKNOWN']); - expect(result2).toEqual(['UNKNOWN', 'SALES_REP', 'SALES_REP', 'HUMAN_RESOURCES', 'FINANCE_MANAGER', 'DEVELOPER', null]); + it('should return a collection of translation values sorted', () => { + translate.use('en'); + const roleCollection = ['SALES_REP', 'DEVELOPER', 'SALES_REP', null, 'HUMAN_RESOURCES', 'FINANCE_MANAGER', 'UNKNOWN']; + const columnDef = { id: 'count', field: 'count', fieldType: FieldType.string } as Column; + + const result1 = service.sortCollection(columnDef, [...roleCollection], { sortDesc: false } as CollectionSortBy, true); + const result2 = service.sortCollection(columnDef, [...roleCollection], { sortDesc: true } as CollectionSortBy, true); + + expect(result1).toEqual([null, 'DEVELOPER', 'FINANCE_MANAGER', 'HUMAN_RESOURCES', 'SALES_REP', 'SALES_REP', 'UNKNOWN']); + expect(result2).toEqual(['UNKNOWN', 'SALES_REP', 'SALES_REP', 'HUMAN_RESOURCES', 'FINANCE_MANAGER', 'DEVELOPER', null]); + }); }); - }); - }); + }); // Collection of strings/numbers + }); // with ngx-translate describe('without ngx-translate', () => { beforeEach(() => { diff --git a/src/app/modules/angular-slickgrid/services/__tests__/export.service.spec.ts b/src/app/modules/angular-slickgrid/services/__tests__/export.service.spec.ts index 52b26d7c6..e515d1d3f 100644 --- a/src/app/modules/angular-slickgrid/services/__tests__/export.service.spec.ts +++ b/src/app/modules/angular-slickgrid/services/__tests__/export.service.spec.ts @@ -1,4 +1,3 @@ -import { Sorters } from './../../sorters/index'; import { TestBed } from '@angular/core/testing'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { @@ -10,8 +9,9 @@ import { FieldType, SortDirectionNumber, } from '../../models'; -import { Formatters } from './../../formatters/index'; import { ExportService } from '../export.service'; +import { Formatters } from './../../formatters/index'; +import { Sorters } from './../../sorters/index'; import { GroupTotalFormatters } from '../..'; function removeMultipleSpaces(textS) { diff --git a/src/app/modules/angular-slickgrid/services/__tests__/utilities.spec.ts b/src/app/modules/angular-slickgrid/services/__tests__/utilities.spec.ts index f3d991f7f..8d185096e 100644 --- a/src/app/modules/angular-slickgrid/services/__tests__/utilities.spec.ts +++ b/src/app/modules/angular-slickgrid/services/__tests__/utilities.spec.ts @@ -688,9 +688,11 @@ describe('Service/Utilies', () => { it('should return default OperatoryType associated to contains', () => { const output1 = mapOperatorType(''); const output2 = mapOperatorType('Contains'); + const output3 = mapOperatorType('CONTAINS'); expect(output1).toBe(OperatorType.contains); expect(output2).toBe(OperatorType.contains); + expect(output3).toBe(OperatorType.contains); }); }); diff --git a/src/app/modules/angular-slickgrid/services/collection.service.ts b/src/app/modules/angular-slickgrid/services/collection.service.ts index bd385f04b..aa1fa510d 100644 --- a/src/app/modules/angular-slickgrid/services/collection.service.ts +++ b/src/app/modules/angular-slickgrid/services/collection.service.ts @@ -53,25 +53,41 @@ export class CollectionService { singleFilterCollection(collection: any[], filterBy: CollectionFilterBy): any[] { let filteredCollection: any[] = []; - if (filterBy && filterBy.property) { - const property = filterBy.property; + if (filterBy) { + const objectProperty = filterBy.property; const operator = filterBy.operator || OperatorType.equal; // just check for undefined since the filter value could be null, 0, '', false etc const value = typeof filterBy.value === 'undefined' ? '' : filterBy.value; switch (operator) { case OperatorType.equal: - filteredCollection = collection.filter((item) => item[property] === value); + if (objectProperty) { + filteredCollection = collection.filter((item) => item[objectProperty] === value); + } else { + filteredCollection = collection.filter((item) => item === value); + } break; case OperatorType.contains: - filteredCollection = collection.filter((item) => item[property].toString().indexOf(value.toString()) !== -1); + if (objectProperty) { + filteredCollection = collection.filter((item) => item[objectProperty].toString().indexOf(value.toString()) !== -1); + } else { + filteredCollection = collection.filter((item) => (item !== null && item !== undefined) && item.toString().indexOf(value.toString()) !== -1); + } break; case OperatorType.notContains: - filteredCollection = collection.filter((item) => item[property].toString().indexOf(value.toString()) === -1); + if (objectProperty) { + filteredCollection = collection.filter((item) => item[objectProperty].toString().indexOf(value.toString()) === -1); + } else { + filteredCollection = collection.filter((item) => (item !== null && item !== undefined) && item.toString().indexOf(value.toString()) === -1); + } break; case OperatorType.notEqual: default: - filteredCollection = collection.filter((item) => item[property] !== value); + if (objectProperty) { + filteredCollection = collection.filter((item) => item[objectProperty] !== value); + } else { + filteredCollection = collection.filter((item) => item !== value); + } } } @@ -100,11 +116,12 @@ export class CollectionService { const sortBy = sortByOptions[i]; if (sortBy && sortBy.property) { + // collection of objects with a property name provided const sortDirection = sortBy.sortDesc ? SortDirectionNumber.desc : SortDirectionNumber.asc; - const propertyName = sortBy.property; + const objectProperty = sortBy.property; const fieldType = sortBy.fieldType || FieldType.string; - const value1 = (enableTranslateLabel) ? this.translate.instant(dataRow1[propertyName] || ' ') : dataRow1[propertyName]; - const value2 = (enableTranslateLabel) ? this.translate.instant(dataRow2[propertyName] || ' ') : dataRow2[propertyName]; + const value1 = (enableTranslateLabel) ? this.translate.instant(dataRow1[objectProperty] || ' ') : dataRow1[objectProperty]; + const value2 = (enableTranslateLabel) ? this.translate.instant(dataRow2[objectProperty] || ' ') : dataRow2[objectProperty]; const sortResult = sortByFieldType(fieldType, value1, value2, sortDirection, columnDef); if (sortResult !== SortDirectionNumber.neutral) { @@ -116,19 +133,22 @@ export class CollectionService { }); } else if (sortByOptions && sortByOptions.property) { // single sort - const propertyName = sortByOptions.property; + // collection of objects with a property name provided + const objectProperty = sortByOptions.property; const sortDirection = sortByOptions.sortDesc ? SortDirectionNumber.desc : SortDirectionNumber.asc; const fieldType = sortByOptions.fieldType || FieldType.string; - sortedCollection = collection.sort((dataRow1: any, dataRow2: any) => { - const value1 = (enableTranslateLabel) ? this.translate.instant(dataRow1[propertyName] || ' ') : dataRow1[propertyName]; - const value2 = (enableTranslateLabel) ? this.translate.instant(dataRow2[propertyName] || ' ') : dataRow2[propertyName]; - const sortResult = sortByFieldType(fieldType, value1, value2, sortDirection, columnDef); - if (sortResult !== SortDirectionNumber.neutral) { - return sortResult; - } - return SortDirectionNumber.neutral; - }); + if (objectProperty) { + sortedCollection = collection.sort((dataRow1: any, dataRow2: any) => { + const value1 = (enableTranslateLabel) ? this.translate.instant(dataRow1[objectProperty] || ' ') : dataRow1[objectProperty]; + const value2 = (enableTranslateLabel) ? this.translate.instant(dataRow2[objectProperty] || ' ') : dataRow2[objectProperty]; + const sortResult = sortByFieldType(fieldType, value1, value2, sortDirection, columnDef); + if (sortResult !== SortDirectionNumber.neutral) { + return sortResult; + } + return SortDirectionNumber.neutral; + }); + } } else if (sortByOptions && !sortByOptions.property) { const sortDirection = sortByOptions.sortDesc ? SortDirectionNumber.desc : SortDirectionNumber.asc; const fieldType = sortByOptions.fieldType || FieldType.string; diff --git a/src/app/modules/angular-slickgrid/services/utilities.ts b/src/app/modules/angular-slickgrid/services/utilities.ts index 90f22d29a..30ad04127 100644 --- a/src/app/modules/angular-slickgrid/services/utilities.ts +++ b/src/app/modules/angular-slickgrid/services/utilities.ts @@ -481,6 +481,7 @@ export function mapOperatorType(operator: string): OperatorType { map = OperatorType.notContains; break; case 'Contains': + case 'CONTAINS': default: map = OperatorType.contains; break; diff --git a/src/assets/i18n/fr.json b/src/assets/i18n/fr.json index 3a0785c0a..7d970a532 100644 --- a/src/assets/i18n/fr.json +++ b/src/assets/i18n/fr.json @@ -34,7 +34,7 @@ "SYNCHRONOUS_RESIZE": "Redimension synchrone", "TOGGLE_FILTER_ROW": "Basculer la ligne des filtres", "TOGGLE_PRE_HEADER_ROW": "Basculer la ligne de pré-en-tête", - "X_OF_Y_SELECTED": "# de % sélectionné", + "X_OF_Y_SELECTED": "# de % sélectionnés", "BILLING": { "ADDRESS": { "STREET": "Adresse de facturation",