From 82bda776c9cb72c9d44aca24ecf289c839e6e24f Mon Sep 17 00:00:00 2001 From: Ghislain Beaulac Date: Wed, 25 Mar 2020 19:48:33 -0400 Subject: [PATCH] feat(filters): add Autocomplete Filter --- .../__tests__/autoCompleteFilter.spec.ts | 424 ++++++++++++++++++ packages/common/src/filters/index.ts | 4 +- .../services/__tests__/filter.service.spec.ts | 4 +- .../groupingAndColspan.service.spec.ts | 2 - packages/common/src/services/utilities.ts | 69 ++- 5 files changed, 483 insertions(+), 20 deletions(-) create mode 100644 packages/common/src/filters/__tests__/autoCompleteFilter.spec.ts diff --git a/packages/common/src/filters/__tests__/autoCompleteFilter.spec.ts b/packages/common/src/filters/__tests__/autoCompleteFilter.spec.ts new file mode 100644 index 000000000..4c403c190 --- /dev/null +++ b/packages/common/src/filters/__tests__/autoCompleteFilter.spec.ts @@ -0,0 +1,424 @@ +import { Filters } from '../index'; +import { AutoCompleteFilter } from '../autoCompleteFilter'; +import { FieldType, OperatorType } from '../../enums/index'; +import { AutocompleteOption, Column, FilterArguments, GridOption, } from '../../interfaces/index'; +import { CollectionService } from '../../services/collection.service'; +import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; + +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('AutoCompleteFilter', () => { + let translaterService: TranslateServiceStub; + let divContainer: HTMLDivElement; + let filter: AutoCompleteFilter; + let filterArguments: FilterArguments; + let spyGetHeaderRow; + let mockColumn: Column; + let collectionService: CollectionService; + + beforeEach(() => { + translaterService = new TranslateServiceStub(); + collectionService = new CollectionService(translaterService); + + 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.autoComplete, + } + }; + filterArguments = { + grid: gridStub, + columnDef: mockColumn, + callback: jest.fn() + }; + + filter = new AutoCompleteFilter(collectionService); + }); + + afterEach(() => { + filter.destroy(); + jest.clearAllMocks(); + }); + + it('should throw an error when trying to call init without any arguments', () => { + expect(() => filter.init(null)).toThrowError('[Slickgrid-Universal] 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); + } catch (e) { + expect(e.toString()).toContain(`[Slickgrid-Universal] You need to pass a "collection" for the AutoComplete 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); + } catch (e) { + expect(e.toString()).toContain(`The "collection" passed to the Autocomplete Filter is not a valid array.`); + done(); + } + }); + + it('should initialize the filter', () => { + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + filter.init(filterArguments); + const filterCount = divContainer.querySelectorAll('input.search-filter.filter-gender').length; + const autocompleteUlElms = document.body.querySelectorAll('ul.ui-autocomplete'); + + expect(autocompleteUlElms.length).toBe(1); + expect(spyGetHeaderRow).toHaveBeenCalled(); + expect(filterCount).toBe(1); + }); + + it('should initialize the filter even when user define his own filter options', () => { + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + mockColumn.filter.filterOptions = { minLength: 3 } as AutocompleteOption; + filter.init(filterArguments); + const filterCount = divContainer.querySelectorAll('input.search-filter.filter-gender').length; + const autocompleteUlElms = document.body.querySelectorAll('ul.ui-autocomplete'); + + expect(autocompleteUlElms.length).toBe(1); + expect(spyGetHeaderRow).toHaveBeenCalled(); + expect(filterCount).toBe(1); + }); + + 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); + const filterElm = divContainer.querySelector('input.search-filter.filter-gender'); + + expect(filterElm.placeholder).toBe(testValue); + }); + + it('should call "setValues" and expect that value to be in the callback when triggered', () => { + const spyCallback = jest.spyOn(filterArguments, 'callback'); + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + + filter.init(filterArguments); + filter.setValues('male'); + const filterElm = divContainer.querySelector('input.filter-gender'); + + filterElm.focus(); + filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('keydown', { keyCode: 109, bubbles: true, cancelable: true })); + filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 109, bubbles: true, cancelable: true })); + const filterFilledElms = divContainer.querySelectorAll('input.filter-gender.filled'); + const autocompleteListElms = document.body.querySelectorAll('ul.ui-autocomplete li'); + + expect(filterFilledElms.length).toBe(1); + // expect(autocompleteListElms.length).toBe(2); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['male'], shouldTriggerQuery: true }); + }); + + it('should call "setValues" with extra spaces at the beginning of the searchTerms and trim value when "enableFilterTrimWhiteSpace" is enabled in grid options', () => { + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + gridOptionMock.enableFilterTrimWhiteSpace = true; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filter.init(filterArguments); + filter.setValues(' abc '); + const filterElm = divContainer.querySelector('input.filter-gender'); + + filterElm.focus(); + filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + const filterFilledElms = divContainer.querySelectorAll('input.filter-gender.filled'); + + expect(filterFilledElms.length).toBe(1); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['abc'], shouldTriggerQuery: true }); + }); + + it('should call "setValues" with extra spaces at the beginning of the searchTerms and trim value when "enableTrimWhiteSpace" is enabled in the column filter', () => { + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + gridOptionMock.enableFilterTrimWhiteSpace = false; + mockColumn.filter.enableTrimWhiteSpace = true; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filter.init(filterArguments); + filter.setValues(' abc '); + const filterElm = divContainer.querySelector('input.filter-gender'); + + filterElm.focus(); + filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + const filterFilledElms = divContainer.querySelectorAll('input.filter-gender.filled'); + + expect(filterFilledElms.length).toBe(1); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['abc'], shouldTriggerQuery: true }); + }); + + it('should trigger the callback method when user types something in the input', () => { + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + + filter.init(filterArguments); + const filterElm = divContainer.querySelector('input.filter-gender'); + + filterElm.focus(); + filterElm.value = 'a'; + filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + const autocompleteListElms = document.body.querySelectorAll('ul.ui-autocomplete li'); + + // expect(autocompleteListElms.length).toBe(2); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['a'], shouldTriggerQuery: true }); + }); + + it('should create the input filter with a default search term when passed as a filter argument', () => { + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + filterArguments.searchTerms = ['xyz']; + + filter.init(filterArguments); + const filterElm = divContainer.querySelector('input.filter-gender'); + + expect(filterElm.value).toBe('xyz'); + }); + + it('should expect the input not to have the "filled" css class when the search term provided is an empty string', () => { + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + filterArguments.searchTerms = ['']; + + filter.init(filterArguments); + const filterElm = divContainer.querySelector('input.filter-gender'); + const filterFilledElms = divContainer.querySelectorAll('input.filter-gender.filled'); + + expect(filterElm.value).toBe(''); + expect(filterFilledElms.length).toBe(0); + }); + + it('should trigger a callback with the clear filter set when calling the "clear" method', () => { + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + const spyCallback = jest.spyOn(filterArguments, 'callback'); + filterArguments.searchTerms = ['xyz']; + + filter.init(filterArguments); + filter.clear(); + const filterElm = divContainer.querySelector('input.filter-gender'); + const filterFilledElms = divContainer.querySelectorAll('input.filter-gender.filled'); + + expect(filterElm.value).toBe(''); + expect(filterFilledElms.length).toBe(0); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { 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 = ['xyz']; + + filter.init(filterArguments); + filter.clear(false); + const filterElm = divContainer.querySelector('input.filter-gender'); + const filterFilledElms = divContainer.querySelectorAll('input.filter-gender.filled'); + + expect(filterElm.value).toBe(''); + expect(filterFilledElms.length).toBe(0); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: false }); + }); + + it('should create the 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); + const filterCollection = filter.collection; + + expect(filterCollection.length).toBe(1); + expect(filterCollection[0]).toBe('other'); + }); + + it('should create the 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); + const filterCollection = filter.collection; + + expect(filterCollection.length).toBe(1); + expect(filterCollection[0]).toEqual({ value: 'female', description: 'female' }); + }); + + it('should create the 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); + const filterCollection = filter.collection; + + expect(filterCollection.length).toBe(2); + expect(filterCollection[0]).toEqual({ value: 'other', description: 'other' }); + expect(filterCollection[1]).toEqual({ value: 'male', description: 'male' }); + }); + + it('should create the filter with a value/label pair collection that is inside an object when "collectionInsideObjectProperty" 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: { collectionInsideObjectProperty: 'deep.myCollection' }, + customStructure: { value: 'value', label: 'description', }, + }; + + filter.init(filterArguments); + const filterCollection = filter.collection; + + expect(filterCollection.length).toBe(3); + expect(filterCollection[0]).toEqual({ value: 'other', description: 'other' }); + expect(filterCollection[1]).toEqual({ value: 'male', description: 'male' }); + expect(filterCollection[2]).toEqual({ value: 'female', description: 'female' }); + }); + + it('should create the 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); + const filterCollection = filter.collection; + + expect(filterCollection.length).toBe(3); + expect(filterCollection[0]).toEqual('other'); + expect(filterCollection[1]).toEqual('male'); + expect(filterCollection[2]).toEqual('female'); + }); + + it('should create the 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); + const filterCollection = filter.collection; + + expect(filterCollection.length).toBe(3); + expect(filterCollection[0]).toEqual({ value: 'female', description: 'female' }); + expect(filterCollection[1]).toEqual({ value: 'male', description: 'male' }); + expect(filterCollection[2]).toEqual({ value: 'other', description: 'other' }); + }); + + describe('onSelect method', () => { + it('should expect "setValue" and "autoCommitEdit" to have been called with a string when item provided is a string', () => { + const spyCallback = jest.spyOn(filterArguments, 'callback'); + mockColumn.filter.collection = ['male', 'female']; + + filter.init(filterArguments); + const spySetValue = jest.spyOn(filter, 'setValues'); + const output = filter.onSelect(null, { item: 'female' }); + + expect(output).toBe(false); + expect(spySetValue).toHaveBeenCalledWith('female'); + expect(spyCallback).toHaveBeenCalledWith(null, { columnDef: mockColumn, operator: 'EQ', searchTerms: ['female'], shouldTriggerQuery: true }); + }); + + it('should expect "setValue" and "autoCommitEdit" to have been called with the string label when item provided is an object', () => { + const spyCallback = jest.spyOn(filterArguments, 'callback'); + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + + filter.init(filterArguments); + const spySetValue = jest.spyOn(filter, 'setValues'); + const output = filter.onSelect(null, { item: { value: 'f', label: 'Female' } }); + + expect(output).toBe(false); + expect(spySetValue).toHaveBeenCalledWith('Female'); + expect(spyCallback).toHaveBeenCalledWith(null, { columnDef: mockColumn, operator: 'EQ', searchTerms: ['f'], shouldTriggerQuery: true }); + }); + + it('should expect the "onSelect" method to be called when the callback method is triggered', () => { + const spy = jest.spyOn(filter, 'onSelect'); + const event = new CustomEvent('change'); + + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + filter.init(filterArguments); + filter.autoCompleteOptions.select(event, { item: 'fem' }); + + expect(spy).toHaveBeenCalledWith(event, { item: 'fem' }); + }); + + it('should initialize the filter with filterOptions and expect the "onSelect" method to be called when the callback method is triggered', () => { + const spy = jest.spyOn(filter, 'onSelect'); + const event = new CustomEvent('change'); + + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + mockColumn.filter.filterOptions = { minLength: 3 } as AutocompleteOption; + filter.init(filterArguments); + filter.autoCompleteOptions.select(event, { item: 'fem' }); + + expect(spy).toHaveBeenCalledWith(event, { item: 'fem' }); + }); + + it('should expect the "onSelect" method to be called when the callback method is triggered', () => { + const spy = jest.spyOn(filter, 'onSelect'); + const event = new CustomEvent('change'); + + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + filter.init(filterArguments); + filter.autoCompleteOptions.select(event, { item: 'fem' }); + + expect(spy).toHaveBeenCalledWith(event, { item: 'fem' }); + }); + + it('should initialize the filter with filterOptions and expect the "onSelect" method to be called when the callback method is triggered', () => { + const spy = jest.spyOn(filter, 'onSelect'); + const event = new CustomEvent('change'); + + mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; + mockColumn.filter.filterOptions = { minLength: 3 } as AutocompleteOption; + filter.init(filterArguments); + filter.autoCompleteOptions.select(event, { item: 'fem' }); + + expect(spy).toHaveBeenCalledWith(event, { item: 'fem' }); + }); + }); +}); diff --git a/packages/common/src/filters/index.ts b/packages/common/src/filters/index.ts index de6dcb01a..2bca1f4d2 100644 --- a/packages/common/src/filters/index.ts +++ b/packages/common/src/filters/index.ts @@ -1,5 +1,5 @@ import { Column, Filter } from '../interfaces/index'; -// import { AutoCompleteFilter } from './autoCompleteFilter'; +import { AutoCompleteFilter } from './autoCompleteFilter'; // import { CompoundDateFilter } from './compoundDateFilter'; import { CompoundInputFilter } from './compoundInputFilter'; import { CompoundInputNumberFilter } from './compoundInputNumberFilter'; @@ -18,7 +18,7 @@ import { SliderRangeFilter } from './sliderRangeFilter'; export const Filters = { /** AutoComplete Filter (using jQuery UI autocomplete feature) */ - // autoComplete: AutoCompleteFilter, + autoComplete: AutoCompleteFilter, /** Compound Date Filter (compound of Operator + Date picker) */ // compoundDate: CompoundDateFilter, diff --git a/packages/common/src/services/__tests__/filter.service.spec.ts b/packages/common/src/services/__tests__/filter.service.spec.ts index 7598f3f92..775829bdd 100644 --- a/packages/common/src/services/__tests__/filter.service.spec.ts +++ b/packages/common/src/services/__tests__/filter.service.spec.ts @@ -385,7 +385,7 @@ describe('FilterService', () => { service: backendServiceStub, process: () => new Promise((resolve) => resolve(jest.fn())), }; - // we must use 3 separate Filters because we aren't loading Aurelia and so our Filters are not transient (as defined in lib config) + // we must use 3 separate Filters because we aren't loading the Framework and so our Filters are not transient (as defined in lib config) mockColumn1 = { id: 'firstName', field: 'firstName', filterable: true, filter: { model: Filters.input } } as Column; mockColumn2 = { id: 'lastName', field: 'lastName', filterable: true, filter: { model: Filters.inputText } } as Column; mockColumn3 = { id: 'age', field: 'age', filterable: true, filter: { model: Filters.inputNumber } } as Column; @@ -488,7 +488,7 @@ describe('FilterService', () => { let mockColumn2: Column; beforeEach(() => { - // we must use 2 separate Filters because we aren't loading Aurelia and so our Filters are not transient (as defined in lib config) + // we must use 2 separate Filters because we aren't loading the Framework and so our Filters are not transient (as defined in lib config) gridOptionMock.backendServiceApi = undefined; mockColumn1 = { id: 'firstName', field: 'firstName', filterable: true, filter: { model: Filters.input } } as Column; mockColumn2 = { id: 'lastName', field: 'lastName', filterable: true, filter: { model: Filters.inputText } } as Column; diff --git a/packages/common/src/services/__tests__/groupingAndColspan.service.spec.ts b/packages/common/src/services/__tests__/groupingAndColspan.service.spec.ts index 724ced137..a748ac391 100644 --- a/packages/common/src/services/__tests__/groupingAndColspan.service.spec.ts +++ b/packages/common/src/services/__tests__/groupingAndColspan.service.spec.ts @@ -6,10 +6,8 @@ declare const Slick: any; const gridId = 'grid1'; const gridUid = 'slickgrid_124343'; const containerId = 'demo-container'; -const aureliaEventPrefix = 'asg'; const gridOptionMock = { - defaultAureliaEventPrefix: aureliaEventPrefix, enablePagination: true, createPreHeaderPanel: true, } as GridOption; diff --git a/packages/common/src/services/utilities.ts b/packages/common/src/services/utilities.ts index ba099c0a4..8c612b630 100644 --- a/packages/common/src/services/utilities.ts +++ b/packages/common/src/services/utilities.ts @@ -35,6 +35,61 @@ export function addWhiteSpaces(nbSpaces: number): string { return result; } +/** + * Convert a hierarchical array (with children) into a flat array structure array (where the children are pushed as next indexed item in the array) + * @param flatArray array input + * @param outputArray array output (passed by reference) + * @param options you can provide the following options:: "parentPropName" (defaults to "parent"), "childPropName" (defaults to "children") and "identifierPropName" (defaults to "id") + */ +export function convertArrayFlatToHierarchical(flatArray: any[], options?: { parentPropName?: string; childPropName?: string; identifierPropName?: string; }): any[] { + const childPropName = options?.childPropName || 'children'; + const parentPropName = options?.parentPropName || 'parent'; + const identifierPropName = options?.identifierPropName || 'id'; + + const roots = []; // things without parent + + // make them accessible by guid on this map + const all = {}; + + flatArray.forEach((item) => all[item[identifierPropName]] = item); + + // connect childrens to its parent, and split roots apart + Object.keys(all).forEach((id) => { + const item = all[id]; + if (item[parentPropName] === null) { + roots.push(item); + } else if (item[parentPropName] in all) { + const p = all[item[parentPropName]]; + if (!(childPropName in p)) { + p[childPropName] = []; + } + p[childPropName].push(item); + } + }); + + return roots; +} + +/** + * Convert a hierarchical array (with children) into a flat array structure array (where the children are pushed as next indexed item in the array) + * @param hierarchicalArray + * @param outputArray + * @param options you can provide "childPropName" (defaults to "children") + */ +export function convertArrayHierarchicalToFlat(hierarchicalArray: any[], outputArray: any[], options: { childPropName?: string; }) { + const childPropName = options?.childPropName || 'children'; + + for (const item of hierarchicalArray) { + if (item) { + outputArray.push(item); + if (Array.isArray(item[childPropName])) { + convertArrayHierarchicalToFlat(item[childPropName], outputArray, options); + delete item[childPropName]; // remove the children property + } + } + } +} + /** * HTML encode using jQuery with a
* Create a in-memory div, set it's inner text(which jQuery automatically encodes) @@ -166,20 +221,6 @@ export function formatNumber(input: number | string, minDecimal?: number, maxDec } } -/** - * Loop through and dispose of all subscriptions when they are disposable - * @param subscriptions - * @return empty array - */ -// export function disposeAllSubscriptions(subscriptions: Subscription[]) { -// subscriptions.forEach((subscription: Subscription) => { -// if (subscription && subscription.dispose) { -// subscription.dispose(); -// } -// }); -// return subscriptions = []; -// } - /** From a dot (.) notation path, find and return a property within an object given a path */ export function getDescendantProperty(obj: any, path: string | undefined): any { if (!obj || !path) {