Skip to content

Commit

Permalink
feat(core): add custom entry to Select Editor/Filter collections (#133)
Browse files Browse the repository at this point in the history
- add 2 new methods (`addCustomFirstEntry`, `addCustomLastEntry`) to optionally add entry on both end of the collection
  • Loading branch information
ghiscoding authored Sep 30, 2020
1 parent b34f6b2 commit 66effcf
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 18 deletions.
53 changes: 43 additions & 10 deletions packages/common/src/editors/__tests__/selectEditor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ describe('SelectEditor', () => {
expect(editorElm[0].value).toEqual('male');
});

it('should create the multi-select filter with a blank entry at the beginning of the collection when "addBlankEntry" is set in the "collectionOptions" property', () => {
it('should create the multi-select editor with a blank entry at the beginning of the collection when "addBlankEntry" is set in the "collectionOptions" property', () => {
mockColumn.internalColumnEditor.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }];
mockColumn.internalColumnEditor.collectionOptions = { addBlankEntry: true };

Expand All @@ -223,6 +223,39 @@ describe('SelectEditor', () => {
editorOkElm.click();

expect(editorListElm.length).toBe(3);
expect(editorListElm[0].value).toBe('');
expect(editorListElm[1].textContent).toBe('');
});

it('should create the multi-select editor with a custom entry at the beginning of the collection when "addCustomFirstEntry" is provided in the "collectionOptions" property', () => {
mockColumn.internalColumnEditor.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }];
mockColumn.internalColumnEditor.collectionOptions = { addCustomFirstEntry: { value: null, label: '' } };

editor = new SelectEditor(editorArguments, true);
const editorBtnElm = divContainer.querySelector<HTMLButtonElement>('.ms-parent.ms-filter.editor-gender button.ms-choice');
const editorListElm = divContainer.querySelectorAll<HTMLInputElement>(`[name=editor-gender].ms-drop ul>li input[type=checkbox]`);
const editorOkElm = divContainer.querySelector<HTMLButtonElement>(`[name=editor-gender].ms-drop .ms-ok-button`);
editorBtnElm.click();
editorOkElm.click();

expect(editorListElm.length).toBe(3);
expect(editorListElm[0].value).toBe('');
expect(editorListElm[1].textContent).toBe('');
});

it('should create the multi-select editor with a custom entry at the end of the collection when "addCustomFirstEntry" is provided in the "collectionOptions" property', () => {
mockColumn.internalColumnEditor.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }];
mockColumn.internalColumnEditor.collectionOptions = { addCustomLastEntry: { value: null, label: '' } };

editor = new SelectEditor(editorArguments, true);
const editorBtnElm = divContainer.querySelector<HTMLButtonElement>('.ms-parent.ms-filter.editor-gender button.ms-choice');
const editorListElm = divContainer.querySelectorAll<HTMLInputElement>(`[name=editor-gender].ms-drop ul>li input[type=checkbox]`);
const editorOkElm = divContainer.querySelector<HTMLButtonElement>(`[name=editor-gender].ms-drop .ms-ok-button`);
editorBtnElm.click();
editorOkElm.click();

expect(editorListElm.length).toBe(3);
expect(editorListElm[2].value).toBe('');
expect(editorListElm[1].textContent).toBe('');
});

Expand Down Expand Up @@ -450,7 +483,7 @@ describe('SelectEditor', () => {
});

describe('initialize with collection', () => {
it('should create the multi-select filter with a default search term when passed as a filter argument even with collection an array of strings', () => {
it('should create the multi-select editor with a default search term when passed as a filter argument even with collection an array of strings', () => {
mockColumn.internalColumnEditor.collection = ['male', 'female'];

editor = new SelectEditor(editorArguments, true);
Expand All @@ -467,7 +500,7 @@ describe('SelectEditor', () => {
});

describe('collectionSortBy setting', () => {
it('should create the multi-select filter and sort the string collection when "collectionSortBy" is set', () => {
it('should create the multi-select editor and sort the string collection when "collectionSortBy" is set', () => {
mockColumn.internalColumnEditor = {
collection: ['other', 'male', 'female'],
collectionSortBy: {
Expand All @@ -487,7 +520,7 @@ describe('SelectEditor', () => {
expect(editorListElm[2].value).toBe('female');
});

it('should create the multi-select filter and sort the value/label pair collection when "collectionSortBy" is set', () => {
it('should create the multi-select editor and sort the value/label pair collection when "collectionSortBy" is set', () => {
mockColumn.internalColumnEditor = {
collection: [{ value: 'other', description: 'other' }, { value: 'male', description: 'male' }, { value: 'female', description: 'female' }],
collectionSortBy: {
Expand All @@ -514,7 +547,7 @@ describe('SelectEditor', () => {
});

describe('collectionFilterBy setting', () => {
it('should create the multi-select filter and filter the string collection when "collectionFilterBy" is set', () => {
it('should create the multi-select editor and filter the string collection when "collectionFilterBy" is set', () => {
mockColumn.internalColumnEditor = {
collection: ['other', 'male', 'female'],
collectionFilterBy: {
Expand All @@ -532,7 +565,7 @@ describe('SelectEditor', () => {
expect(editorListElm[0].value).toBe('other');
});

it('should create the multi-select filter and filter the value/label pair collection when "collectionFilterBy" is set', () => {
it('should create the multi-select editor and filter the value/label pair collection when "collectionFilterBy" is set', () => {
mockColumn.internalColumnEditor = {
collection: [{ value: 'other', description: 'other' }, { value: 'male', description: 'male' }, { value: 'female', description: 'female' }],
collectionFilterBy: [
Expand All @@ -554,7 +587,7 @@ describe('SelectEditor', () => {
expect(editorListElm[0].value).toBe('female');
});

it('should create the multi-select filter and filter the value/label pair collection when "collectionFilterBy" is set and "filterResultAfterEachPass" is set to "merge"', () => {
it('should create the multi-select editor and filter the value/label pair collection when "collectionFilterBy" is set and "filterResultAfterEachPass" is set to "merge"', () => {
mockColumn.internalColumnEditor = {
collection: [{ value: 'other', description: 'other' }, { value: 'male', description: 'male' }, { value: 'female', description: 'female' }],
collectionFilterBy: [
Expand Down Expand Up @@ -608,7 +641,7 @@ describe('SelectEditor', () => {
});

describe('enableRenderHtml property', () => {
it('should create the multi-select filter with a default search term and have the HTML rendered when "enableRenderHtml" is set', () => {
it('should create the multi-select editor with a default search term and have the HTML rendered when "enableRenderHtml" is set', () => {
mockColumn.internalColumnEditor = {
enableRenderHtml: true,
collection: [{ value: true, label: 'True', labelPrefix: `<i class="fa fa-check"></i> ` }, { value: false, label: 'False' }],
Expand All @@ -628,7 +661,7 @@ describe('SelectEditor', () => {
expect(editorListElm[0].innerHTML).toBe('<i class="fa fa-check"></i> 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 <script> tag', () => {
it('should create the multi-select editor with a default search term and have the HTML rendered and sanitized when "enableRenderHtml" is set and has <script> tag', () => {
mockColumn.internalColumnEditor = {
enableRenderHtml: true,
collection: [{ isEffort: true, label: 'True', labelPrefix: `<script>alert('test')></script><i class="fa fa-check"></i> ` }, { isEffort: false, label: 'False' }],
Expand Down Expand Up @@ -656,7 +689,7 @@ describe('SelectEditor', () => {
expect(editorListElm[0].innerHTML).toBe('<i class="fa fa-check"></i> : True');
});

it('should create the multi-select filter with a default search term and have the HTML rendered and sanitized when using a custom "sanitizer" and "enableRenderHtml" flag is set and has <script> tag', () => {
it('should create the multi-select editor with a default search term and have the HTML rendered and sanitized when using a custom "sanitizer" and "enableRenderHtml" flag is set and has <script> tag', () => {
mockColumn.internalColumnEditor = {
enableRenderHtml: true,
collection: [{ isEffort: true, label: 'True', labelPrefix: `<script>alert('test')></script><i class="fa fa-check"></i> ` }, { isEffort: false, label: 'False' }],
Expand Down
29 changes: 25 additions & 4 deletions packages/common/src/editors/selectEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,7 @@ export class SelectEditor implements Editor {
}

renderDomElement(collection: any[]) {
if (!Array.isArray(collection) && this.collectionOptions && (this.collectionOptions.collectionInsideObjectProperty)) {
if (!Array.isArray(collection) && this.collectionOptions?.collectionInsideObjectProperty) {
const collectionInsideObjectProperty = this.collectionOptions.collectionInsideObjectProperty;
collection = getDescendantProperty(collection, collectionInsideObjectProperty);
}
Expand All @@ -569,10 +569,24 @@ export class SelectEditor implements Editor {
}

// user can optionally add a blank entry at the beginning of the collection
if (this.collectionOptions && this.collectionOptions.addBlankEntry) {
// make sure however that it wasn't added more than once
if (this.collectionOptions?.addBlankEntry && Array.isArray(collection) && collection.length > 0 && collection[0][this.valueName] !== '') {
collection.unshift(this.createBlankEntry());
}

// user can optionally add his own custom entry at the beginning of the collection
if (this.collectionOptions?.addCustomFirstEntry && Array.isArray(collection) && collection.length > 0 && collection[0][this.valueName] !== this.collectionOptions.addCustomFirstEntry[this.valueName]) {
collection.unshift(this.collectionOptions.addCustomFirstEntry);
}

// user can optionally add his own custom entry at the end of the collection
if (this.collectionOptions?.addCustomLastEntry && Array.isArray(collection) && collection.length > 0) {
const lastCollectionIndex = collection.length - 1;
if (collection[lastCollectionIndex][this.valueName] !== this.collectionOptions.addCustomLastEntry[this.valueName]) {
collection.push(this.collectionOptions.addCustomLastEntry);
}
}

// assign the collection to a temp variable before filtering/sorting the collection
let newCollection = collection;

Expand Down Expand Up @@ -614,7 +628,9 @@ export class SelectEditor implements Editor {
let prefixText = option[this.labelPrefixName] || '';
let suffixText = option[this.labelSuffixName] || '';
let optionLabel = option[this.optionLabel] || '';
optionLabel = optionLabel.toString().replace(/\"/g, '\''); // replace double quotes by single quotes to avoid interfering with regular html
if (optionLabel?.toString) {
optionLabel = optionLabel.toString().replace(/\"/g, '\''); // replace double quotes by single quotes to avoid interfering with regular html
}

// also translate prefix/suffix if enableTranslateLabel is true and text is a string
prefixText = (this.enableTranslateLabel && prefixText && typeof prefixText === 'string') ? this._translaterService.translate(prefixText || ' ') : prefixText;
Expand All @@ -634,7 +650,12 @@ export class SelectEditor implements Editor {
optionText = htmlEncode(sanitizedText);
}

options += `<option value="${option[this.valueName]}" label="${optionLabel}">${optionText}</option>`;
// html text of each select option
let optionValue = option[this.valueName];
if (optionValue === undefined || optionValue === null) {
optionValue = '';
}
options += `<option value="${optionValue}" label="${optionLabel}">${optionText}</option>`;
});
}
return `<select id="${this.elementName}" class="ms-filter search-filter editor-${columnId}" ${this.isMultipleSelect ? 'multiple="multiple"' : ''}>${options}</select>`;
Expand Down
43 changes: 43 additions & 0 deletions packages/common/src/filters/__tests__/selectFilter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,10 +485,53 @@ describe('SelectFilter', () => {

expect(filterListElm.length).toBe(3);
expect(filterFilledElms.length).toBe(1);
expect(filterListElm[0].value).toBe('');
expect(filterListElm[2].checked).toBe(true);
expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, operator: 'IN', searchTerms: ['female'], shouldTriggerQuery: true });
});

it('should create the multi-select filter with a custom entry at the beginning of the collection when "addCustomFirstEntry" is provided in the "collectionOptions" property', () => {
filterArguments.searchTerms = ['female'];
mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }];
mockColumn.filter.collectionOptions = { addCustomFirstEntry: { value: null, label: '' } };
const spyCallback = jest.spyOn(filterArguments, 'callback');

filter.init(filterArguments);
const filterBtnElm = divContainer.querySelector<HTMLButtonElement>('.ms-parent.ms-filter.search-filter.filter-gender button.ms-choice');
const filterListElm = divContainer.querySelectorAll<HTMLInputElement>(`[name=filter-gender].ms-drop ul>li input[type=checkbox]`);
const filterFilledElms = divContainer.querySelectorAll<HTMLDivElement>('.ms-parent.ms-filter.search-filter.filter-gender.filled');
const filterOkElm = divContainer.querySelector<HTMLButtonElement>(`[name=filter-gender].ms-drop .ms-ok-button`);
filterBtnElm.click();
filterOkElm.click();

expect(filterListElm.length).toBe(3);
expect(filterFilledElms.length).toBe(1);
expect(filterListElm[0].value).toBe('');
expect(filterListElm[2].checked).toBe(true);
expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, operator: 'IN', searchTerms: ['female'], shouldTriggerQuery: true });
});

it('should create the multi-select filter with a custom entry at the end of the collection when "addCustomFirstEntry" is provided in the "collectionOptions" property', () => {
filterArguments.searchTerms = ['female'];
mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }];
mockColumn.filter.collectionOptions = { addCustomLastEntry: { value: null, label: '' } };
const spyCallback = jest.spyOn(filterArguments, 'callback');

filter.init(filterArguments);
const filterBtnElm = divContainer.querySelector<HTMLButtonElement>('.ms-parent.ms-filter.search-filter.filter-gender button.ms-choice');
const filterListElm = divContainer.querySelectorAll<HTMLInputElement>(`[name=filter-gender].ms-drop ul>li input[type=checkbox]`);
const filterFilledElms = divContainer.querySelectorAll<HTMLDivElement>('.ms-parent.ms-filter.search-filter.filter-gender.filled');
const filterOkElm = divContainer.querySelector<HTMLButtonElement>(`[name=filter-gender].ms-drop .ms-ok-button`);
filterBtnElm.click();
filterOkElm.click();

expect(filterListElm.length).toBe(3);
expect(filterFilledElms.length).toBe(1);
expect(filterListElm[2].value).toBe('');
expect(filterListElm[1].checked).toBe(true);
expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, operator: 'IN', searchTerms: ['female'], shouldTriggerQuery: true });
});

it('should trigger a callback with the clear filter set when calling the "clear" method', () => {
filterArguments.searchTerms = ['female'];
mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }];
Expand Down
25 changes: 22 additions & 3 deletions packages/common/src/filters/selectFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,10 +254,23 @@ export class SelectFilter implements Filter {

// user can optionally add a blank entry at the beginning of the collection
// make sure however that it wasn't added more than once
if (this.collectionOptions && this.collectionOptions.addBlankEntry && Array.isArray(collection) && collection.length > 0 && collection[0][this.labelName] !== '') {
if (this.collectionOptions?.addBlankEntry && Array.isArray(collection) && collection.length > 0 && collection[0][this.valueName] !== '') {
collection.unshift(this.createBlankEntry());
}

// user can optionally add his own custom entry at the beginning of the collection
if (this.collectionOptions?.addCustomFirstEntry && Array.isArray(collection) && collection.length > 0 && collection[0][this.valueName] !== this.collectionOptions.addCustomFirstEntry[this.valueName]) {
collection.unshift(this.collectionOptions.addCustomFirstEntry);
}

// user can optionally add his own custom entry at the end of the collection
if (this.collectionOptions?.addCustomLastEntry && Array.isArray(collection) && collection.length > 0) {
const lastCollectionIndex = collection.length - 1;
if (collection[lastCollectionIndex][this.valueName] !== this.collectionOptions.addCustomLastEntry[this.valueName]) {
collection.push(this.collectionOptions.addCustomLastEntry);
}
}

// assign the collection to a temp variable before filtering/sorting the collection
let newCollection = collection;

Expand Down Expand Up @@ -312,7 +325,9 @@ export class SelectFilter implements Filter {
let prefixText = option[this.labelPrefixName] || '';
let suffixText = option[this.labelSuffixName] || '';
let optionLabel = option.hasOwnProperty(this.optionLabel) ? option[this.optionLabel] : '';
optionLabel = optionLabel.toString().replace(/\"/g, '\''); // replace double quotes by single quotes to avoid interfering with regular html
if (optionLabel?.toString) {
optionLabel = optionLabel.toString().replace(/\"/g, '\''); // replace double quotes by single quotes to avoid interfering with regular html
}

// also translate prefix/suffix if enableTranslateLabel is true and text is a string
prefixText = (this.enableTranslateLabel && isTranslateEnabled && prefixText && typeof prefixText === 'string') ? this.translaterService?.getCurrentLanguage && this.translaterService.getCurrentLanguage() && this.translaterService.translate(prefixText || ' ') : prefixText;
Expand All @@ -332,7 +347,11 @@ export class SelectFilter implements Filter {
}

// html text of each select option
options += `<option value="${option[this.valueName]}" label="${optionLabel}" ${selected}>${optionText}</option>`;
let optionValue = option[this.valueName];
if (optionValue === undefined || optionValue === null) {
optionValue = '';
}
options += `<option value="${optionValue}" label="${optionLabel}" ${selected}>${optionText}</option>`;

// if there's a search term, we will add the "filled" class for styling purposes
// on a single select, we'll also make sure the single value is not an empty string to consider this being filled
Expand Down
8 changes: 7 additions & 1 deletion packages/common/src/interfaces/collectionOption.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ import { FilterMultiplePassTypeString } from '../enums/filterMultiplePassTypeStr

export interface CollectionOption {
/**
* Optionally add a blank entry to the beginning of the collection.
* Optionally add a blank entry to the beginning of the collection (only used by the SingleSelect/MultipleSelect Editor or Filter).
* Useful when we want to return all data by setting an empty filter that might not exist in the original collection
*/
addBlankEntry?: boolean;

/** Optionally add a custom entry at the beginning of the collection (only used by the SingleSelect/MultipleSelect Editor or Filter). */
addCustomFirstEntry?: any;

/** Optionally add a custom entry at the end of the collection (only used by the SingleSelect/MultipleSelect Editor or Filter). */
addCustomLastEntry?: any;

/**
* When the collection is inside an object descendant property
* we can optionally pass a dot (.) notation string to pull the collection from an object property.
Expand Down
Loading

0 comments on commit 66effcf

Please sign in to comment.