container to simulate the grid container
+const template = `
`;
+
+const dataViewStub = {
+ refresh: jest.fn(),
+};
+
+const gridOptionMock = {
+ autoCommitEdit: false,
+ editable: true,
+ i18n: null,
+} as GridOption;
+
+const getEditorLockMock = {
+ commitCurrentEdit: jest.fn(),
+};
+
+const gridStub = {
+ getOptions: () => gridOptionMock,
+ getColumns: jest.fn(),
+ getEditorLock: () => getEditorLockMock,
+ getHeaderRowColumn: jest.fn(),
+ navigateNext: jest.fn(),
+ navigatePrev: jest.fn(),
+ render: jest.fn(),
+};
+
+describe('SelectEditor', () => {
+ let translateService: TranslateServiceStub;
+ let divContainer: HTMLDivElement;
+ let editor: SelectEditor;
+ let editorArguments: EditorArguments;
+ let mockColumn: Column;
+ let mockItemData: any;
+ let collectionService: CollectionService;
+
+ beforeEach(() => {
+ translateService = new TranslateServiceStub();
+ collectionService = new CollectionService(translateService);
+
+ divContainer = document.createElement('div');
+ divContainer.innerHTML = template;
+ document.body.appendChild(divContainer);
+
+ mockColumn = { id: 'gender', field: 'gender', editable: true, editor: { model: Editors.multipleSelect }, internalColumnEditor: {} } as Column;
+
+ editorArguments = {
+ grid: gridStub,
+ column: mockColumn,
+ item: mockItemData,
+ event: null,
+ cancelChanges: jest.fn(),
+ commitChanges: jest.fn(),
+ container: divContainer,
+ columnMetaData: null,
+ dataView: dataViewStub,
+ gridPosition: { top: 0, left: 0, bottom: 10, right: 10, height: 100, width: 100, visible: true },
+ position: { top: 0, left: 0, bottom: 10, right: 10, height: 100, width: 100, visible: true },
+ };
+ });
+
+ describe('with invalid Editor instance', () => {
+ it('should throw an error when trying to call init without any arguments', (done) => {
+ try {
+ editor = new SelectEditor(null, true);
+ } catch (e) {
+ expect(e.toString()).toContain(`[Slickgrid-Universal] Something is wrong with this grid, an Editor must always have valid arguments.`);
+ done();
+ }
+ });
+
+ it('should throw an error when there is no collection provided in the editor property', (done) => {
+ try {
+ mockColumn.internalColumnEditor.collection = undefined;
+ editor = new SelectEditor(editorArguments, true);
+ } catch (e) {
+ expect(e.toString()).toContain(`[Slickgrid-Universal] You need to pass a "collection" (or "collectionAsync") inside Column Definition Editor for the MultipleSelect/SingleSelect Editor to work correctly.`);
+ done();
+ }
+ });
+
+ it('should throw an error when collection is not a valid array', (done) => {
+ try {
+ // @ts-ignore
+ mockColumn.internalColumnEditor.collection = { hello: 'world' };
+ editor = new SelectEditor(editorArguments, true);
+ } catch (e) {
+ expect(e.toString()).toContain(`The "collection" passed to the Select Editor is not a valid array.`);
+ done();
+ }
+ });
+
+ it('should throw an error when collection is not a valid value/label pair array', (done) => {
+ try {
+ mockColumn.internalColumnEditor.collection = [{ hello: 'world' }];
+ editor = new SelectEditor(editorArguments, true);
+ } catch (e) {
+ expect(e.toString()).toContain(`[select-editor] 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 I18N Service', (done) => {
+ try {
+ translateService = undefined;
+ mockColumn.internalColumnEditor.enableTranslateLabel = true;
+ mockColumn.internalColumnEditor.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }];
+ editor = new SelectEditor(editorArguments, true);
+ } catch (e) {
+ expect(e.toString()).toContain(`[Slickgrid-Universal] requires a Translate Service to be installed and configured when the grid option "enableTranslate" is enabled.`);
+ done();
+ }
+ });
+ });
+
+ describe('with valid Editor instance', () => {
+ beforeEach(() => {
+ mockItemData = { id: 1, gender: 'male', isActive: true };
+ mockColumn = { id: 'gender', field: 'gender', editable: true, editor: { model: Editors.multipleSelect }, internalColumnEditor: {} } as Column;
+ mockColumn.internalColumnEditor.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }, { value: 'other', label: 'other' }];
+
+ editorArguments.column = mockColumn;
+ editorArguments.item = mockItemData;
+ });
+
+ afterEach(() => {
+ editor.destroy();
+ });
+
+ it('should initialize the editor', () => {
+ mockColumn.internalColumnEditor.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }];
+ gridOptionMock.i18n = translateService;
+ editor = new SelectEditor(editorArguments, true);
+ editor.focus();
+ const editorCount = document.body.querySelectorAll('select.ms-filter.editor-gender').length;
+
+ expect(editorCount).toBe(1);
+ });
+
+ it('should initialize the editor even when user define his own editor options', () => {
+ mockColumn.internalColumnEditor.editorOptions = { minLength: 3 } as AutocompleteOption;
+ editor = new SelectEditor(editorArguments, true);
+ const editorCount = document.body.querySelectorAll('select.ms-filter.editor-gender').length;
+
+ expect(editorCount).toBe(1);
+ });
+
+ it('should have a placeholder when defined in its column definition', () => {
+ const testValue = 'test placeholder';
+ mockColumn.internalColumnEditor.placeholder = testValue;
+ mockColumn.internalColumnEditor.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }];
+
+ editor = new SelectEditor(editorArguments, true);
+ const editorElm = divContainer.querySelector
('.ms-filter.editor-gender .placeholder');
+
+ expect(editorElm.innerHTML).toBe(testValue);
+ });
+
+ it('should call "columnEditor" GETTER and expect to equal the editor settings we provided', () => {
+ mockColumn.internalColumnEditor.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }];
+ mockColumn.internalColumnEditor.placeholder = 'test placeholder';
+
+ editor = new SelectEditor(editorArguments, true);
+
+ expect(editor.columnEditor).toEqual(mockColumn.internalColumnEditor);
+ });
+
+ it('should call "setValue" with a single string and expect the string to be returned in a single string array when calling "getValue" when using single select', () => {
+ editor = new SelectEditor(editorArguments, true);
+ editor.setValue(['male']);
+
+ expect(editor.getValue()).toEqual(['male']);
+ });
+
+ it('should define an item datacontext containing a string as cell value and expect this value to be loaded in the editor when calling "loadValue"', () => {
+ editor = new SelectEditor(editorArguments, true);
+ editor.loadValue(mockItemData);
+ const editorElm = editor.editorDomElement;
+
+ expect(editor.getValue()).toEqual(['male']);
+ 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', () => {
+ mockColumn.internalColumnEditor.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }];
+ mockColumn.internalColumnEditor.collectionOptions = { addBlankEntry: true };
+
+ editor = new SelectEditor(editorArguments, true);
+ const editorBtnElm = divContainer.querySelector('.ms-parent.ms-filter.editor-gender button.ms-choice');
+ const editorListElm = divContainer.querySelectorAll(`[name=editor-gender].ms-drop ul>li input[type=checkbox]`);
+ const editorOkElm = divContainer.querySelector(`[name=editor-gender].ms-drop .ms-ok-button`);
+ editorBtnElm.click();
+ editorOkElm.click();
+
+ expect(editorListElm.length).toBe(3);
+ expect(editorListElm[1].textContent).toBe('');
+ });
+
+ describe('isValueChanged method', () => {
+ it('should return True after doing a check of an option and clicking on the OK button', () => {
+ editor = new SelectEditor(editorArguments, true);
+ const editorBtnElm = divContainer.querySelector('.ms-parent.ms-filter.editor-gender button.ms-choice');
+ const editorListElm = divContainer.querySelectorAll(`[name=editor-gender].ms-drop ul>li input[type=checkbox]`);
+ const editorOkElm = divContainer.querySelector(`[name=editor-gender].ms-drop .ms-ok-button`);
+ editorBtnElm.click();
+
+ // we can use property "checked" or dispatch an event
+ editorListElm[0].dispatchEvent(new CustomEvent('click'));
+ editorOkElm.click();
+
+ expect(editorListElm.length).toBe(3);
+ expect(editor.isValueChanged()).toBe(true);
+ });
+
+ it('should return False after doing a check & uncheck of the same option and clicking on the OK button', () => {
+ editor = new SelectEditor(editorArguments, true);
+ const editorBtnElm = divContainer.querySelector('.ms-parent.ms-filter.editor-gender button.ms-choice');
+ const editorListElm = divContainer.querySelectorAll(`[name=editor-gender].ms-drop ul>li input[type=checkbox]`);
+ const editorOkElm = divContainer.querySelector(`[name=editor-gender].ms-drop .ms-ok-button`);
+ editorBtnElm.click();
+
+ // we can use property "checked" or dispatch an event
+ // check and uncheck the same option
+ editorListElm[0].checked = true;
+ editorListElm[0].checked = false;
+ editorOkElm.click();
+
+ expect(editorListElm.length).toBe(3);
+ expect(editor.isValueChanged()).toBe(true);
+ });
+ });
+
+ describe('applyValue method', () => {
+ it('should apply the value to the gender property when it passes validation', () => {
+ mockColumn.internalColumnEditor.validator = null;
+ mockItemData = { id: 1, gender: 'male', isActive: true };
+
+ editor = new SelectEditor(editorArguments, true);
+ editor.applyValue(mockItemData, 'female');
+
+ expect(mockItemData).toEqual({ id: 1, gender: 'female', isActive: true });
+ });
+
+ it('should apply the value to the gender (last property) when field has a dot notation (complex object) that passes validation', () => {
+ mockColumn.internalColumnEditor.validator = null;
+ mockColumn.field = 'person.bio.gender';
+ mockItemData = { id: 1, person: { bio: { gender: 'male' } }, isActive: true };
+
+ editor = new SelectEditor(editorArguments, true);
+ editor.applyValue(mockItemData, 'female');
+
+ expect(mockItemData).toEqual({ id: 1, person: { bio: { gender: 'female' } }, isActive: true });
+ });
+
+ it('should apply the value to the bio property (second last) when field has a dot notation (complex object) value provided is an object and it that passes validation', () => {
+ mockColumn.internalColumnEditor.validator = null;
+ mockColumn.internalColumnEditor.complexObjectPath = 'person.bio';
+ mockColumn.field = 'person.bio.gender';
+ mockItemData = { id: 1, person: { bio: { gender: 'male' } }, isActive: true };
+
+ editor = new SelectEditor(editorArguments, true);
+ editor.applyValue(mockItemData, { gender: 'female' });
+
+ expect(mockItemData).toEqual({ id: 1, person: { bio: { gender: 'female' } }, isActive: true });
+ });
+
+ it('should return item data with an empty string in its value when it fails the custom validation', () => {
+ mockColumn.internalColumnEditor.validator = (value: any, args: EditorArgs) => {
+ if (value.length < 10) {
+ return { valid: false, msg: 'Must be at least 10 chars long.' };
+ }
+ return { valid: true, msg: '' };
+ };
+ mockItemData = { id: 1, gender: 'male', isActive: true };
+
+ editor = new SelectEditor(editorArguments, true);
+ editor.applyValue(mockItemData, 'female');
+
+ expect(mockItemData).toEqual({ id: 1, gender: '', isActive: true });
+ });
+
+ it('should apply the value to the gender property as an array with multiple when the input value is a CSV string', () => {
+ mockColumn.internalColumnEditor.validator = null;
+ mockItemData = { id: 1, gender: 'male', isActive: true };
+
+ editor = new SelectEditor(editorArguments, true);
+ editor.applyValue(mockItemData, 'male,other');
+
+ expect(mockItemData).toEqual({ id: 1, gender: ['male', 'other'], isActive: true });
+ });
+
+ it('should parse the value as a float when field type is defined as float then apply the value', () => {
+ mockColumn = { id: 'age', field: 'age', type: FieldType.boolean, editable: true, editor: { model: Editors.multipleSelect }, internalColumnEditor: {} } as Column;
+ mockItemData = { id: 1, gender: 'male', isActive: true, age: 26 };
+ mockColumn.internalColumnEditor.collection = [{ value: 20, label: '20' }, { value: 25, label: '25' }];
+
+ editorArguments.column = mockColumn;
+ editor = new SelectEditor(editorArguments, true);
+ editor.applyValue(mockItemData, 25);
+
+ expect(mockItemData).toEqual({ id: 1, gender: 'male', isActive: true, age: 25 });
+ });
+ });
+
+ describe('serializeValue method', () => {
+ it('should return serialized value as a string', () => {
+ mockItemData = { id: 1, gender: 'male', isActive: true };
+
+ editor = new SelectEditor(editorArguments, true);
+ editor.loadValue(mockItemData);
+ const output = editor.serializeValue();
+
+ expect(output).toEqual(['male']);
+ });
+
+ it('should return serialized value as an empty array when item value is also an empty string', () => {
+ mockItemData = { id: 1, gender: '', isActive: true };
+
+ editor = new SelectEditor(editorArguments, true);
+ editor.loadValue(mockItemData);
+ const output = editor.serializeValue();
+
+ expect(output).toEqual([]);
+ });
+
+ it('should return serialized value as an empty string when item value is null', () => {
+ mockItemData = { id: 1, gender: null, isActive: true };
+
+ editor = new SelectEditor(editorArguments, true);
+ editor.loadValue(mockItemData);
+ const output = editor.serializeValue();
+ const currentValue = editor.currentValue;
+
+ expect(output).toEqual([]);
+ expect(currentValue).toEqual('');
+ });
+
+ it('should return value as a string when using a dot (.) notation for complex object with a collection of string values', () => {
+ mockColumn.field = 'employee.gender';
+ mockColumn.internalColumnEditor.collection = ['male', 'female'];
+ mockItemData = { id: 1, employee: { id: 24, gender: 'male' }, isActive: true };
+
+ editor = new SelectEditor(editorArguments, true);
+ editor.loadValue(mockItemData);
+ const output = editor.serializeValue();
+
+ expect(output).toEqual(['male']);
+ });
+
+ it('should return object value when using a dot (.) notation for complex object with a collection of option/label pair', () => {
+ mockColumn.field = 'employee.gender';
+ mockItemData = { id: 1, employee: { id: 24, gender: ['male', 'other'] }, isActive: true };
+ editor = new SelectEditor(editorArguments, true);
+ editor.loadValue(mockItemData);
+ const output = editor.serializeValue();
+ const currentValue = editor.currentValue;
+
+ expect(output).toEqual([{ label: 'male', value: 'male' }, { label: 'other', value: 'other' }]);
+ expect(currentValue).toEqual({});
+ });
+
+ it('should return object value when using a dot (.) notation and we override the object path using "complexObjectPath" to find correct values', () => {
+ mockColumn.field = 'employee.bio';
+ mockItemData = { id: 1, employee: { id: 24, bio: { gender: ['male', 'other'] } }, isActive: true };
+ mockColumn.internalColumnEditor.complexObjectPath = 'employee.bio.gender';
+ editor = new SelectEditor(editorArguments, true);
+ editor.loadValue(mockItemData);
+ const output = editor.serializeValue();
+
+ expect(output).toEqual([{ label: 'male', value: 'male' }, { label: 'other', value: 'other' }]);
+ });
+ });
+
+ describe('save method', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should call "getEditorLock" method when "hasAutoCommitEdit" is enabled', () => {
+ mockItemData = { id: 1, gender: 'male', isActive: true };
+ gridOptionMock.autoCommitEdit = true;
+ const spy = jest.spyOn(gridStub.getEditorLock(), 'commitCurrentEdit');
+
+ editor = new SelectEditor(editorArguments, true);
+ editor.loadValue(mockItemData);
+ editor.save();
+
+ expect(spy).toHaveBeenCalled();
+ });
+
+ it('should not call anything when "hasAutoCommitEdit" is disabled', () => {
+ mockItemData = { id: 1, gender: 'male', isActive: true };
+ gridOptionMock.autoCommitEdit = false;
+ const spy = jest.spyOn(editorArguments, 'commitChanges');
+
+ editor = new SelectEditor(editorArguments, true);
+ editor.loadValue(mockItemData);
+ editor.save();
+
+ expect(spy).not.toHaveBeenCalled();
+ });
+
+ it('should not call anything when the input value is empty but is required', () => {
+ mockItemData = { id: 1, gender: '', isActive: true };
+ mockColumn.internalColumnEditor.required = true;
+ gridOptionMock.autoCommitEdit = true;
+ const spy = jest.spyOn(gridStub.getEditorLock(), 'commitCurrentEdit');
+
+ editor = new SelectEditor(editorArguments, true);
+ editor.loadValue(mockItemData);
+ editor.save();
+
+ expect(spy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('validate method', () => {
+ it('should return False when field is required and field is empty', () => {
+ mockColumn.internalColumnEditor.required = true;
+ editor = new SelectEditor(editorArguments, true);
+ const validation = editor.validate('');
+
+ expect(validation).toEqual({ valid: false, msg: 'Field is required' });
+ });
+
+ it('should return True when field is required and input is a valid input value', () => {
+ mockColumn.internalColumnEditor.required = true;
+ editor = new SelectEditor(editorArguments, true);
+ const validation = editor.validate('text');
+
+ expect(validation).toEqual({ valid: true, msg: null });
+ });
+ });
+
+ 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', () => {
+ mockColumn.internalColumnEditor.collection = ['male', 'female'];
+
+ editor = new SelectEditor(editorArguments, true);
+ const editorBtnElm = divContainer.querySelector('.ms-parent.ms-filter.editor-gender button.ms-choice');
+ const editorListElm = divContainer.querySelectorAll(`[name=editor-gender].ms-drop ul>li input[type=checkbox]`);
+ const editorOkElm = divContainer.querySelector(`[name=editor-gender].ms-drop .ms-ok-button`);
+ editorBtnElm.click();
+ editorOkElm.click();
+
+ expect(editorListElm.length).toBe(2);
+ expect(editorListElm[0].value).toBe('male');
+ expect(editorListElm[1].value).toBe('female');
+ });
+ });
+
+ describe('collectionSortBy setting', () => {
+ it('should create the multi-select filter and sort the string collection when "collectionSortBy" is set', () => {
+ mockColumn.internalColumnEditor = {
+ collection: ['other', 'male', 'female'],
+ collectionSortBy: {
+ sortDesc: true,
+ fieldType: FieldType.string
+ }
+ };
+
+ editor = new SelectEditor(editorArguments, true);
+ const editorBtnElm = divContainer.querySelector('.ms-parent.ms-filter.editor-gender button.ms-choice');
+ const editorListElm = divContainer.querySelectorAll(`[name=editor-gender].ms-drop ul>li input[type=checkbox]`);
+ editorBtnElm.click();
+
+ expect(editorListElm.length).toBe(3);
+ expect(editorListElm[0].value).toBe('other');
+ expect(editorListElm[1].value).toBe('male');
+ expect(editorListElm[2].value).toBe('female');
+ });
+
+ it('should create the multi-select filter 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: {
+ property: 'value',
+ sortDesc: false,
+ fieldType: FieldType.string
+ },
+ customStructure: {
+ value: 'value',
+ label: 'description',
+ },
+ };
+
+ editor = new SelectEditor(editorArguments, true);
+ const editorBtnElm = divContainer.querySelector('.ms-parent.ms-filter.editor-gender button.ms-choice');
+ const editorListElm = divContainer.querySelectorAll(`[name=editor-gender].ms-drop ul>li input[type=checkbox]`);
+ editorBtnElm.click();
+
+ expect(editorListElm.length).toBe(3);
+ expect(editorListElm[0].value).toBe('female');
+ expect(editorListElm[1].value).toBe('male');
+ expect(editorListElm[2].value).toBe('other');
+ });
+ });
+
+ describe('collectionFilterBy setting', () => {
+ it('should create the multi-select filter and filter the string collection when "collectionFilterBy" is set', () => {
+ mockColumn.internalColumnEditor = {
+ collection: ['other', 'male', 'female'],
+ collectionFilterBy: {
+ operator: OperatorType.equal,
+ value: 'other'
+ }
+ };
+
+ editor = new SelectEditor(editorArguments, true);
+ const editorBtnElm = divContainer.querySelector('.ms-parent.ms-filter.editor-gender button.ms-choice');
+ const editorListElm = divContainer.querySelectorAll(`[name=editor-gender].ms-drop ul>li input[type=checkbox]`);
+ editorBtnElm.click();
+
+ expect(editorListElm.length).toBe(1);
+ expect(editorListElm[0].value).toBe('other');
+ });
+
+ it('should create the multi-select filter 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: [
+ { property: 'value', operator: OperatorType.notEqual, value: 'other' },
+ { property: 'value', operator: OperatorType.notEqual, value: 'male' }
+ ],
+ customStructure: {
+ value: 'value',
+ label: 'description',
+ },
+ };
+
+ editor = new SelectEditor(editorArguments, true);
+ const editorBtnElm = divContainer.querySelector('.ms-parent.ms-filter.editor-gender button.ms-choice');
+ const editorListElm = divContainer.querySelectorAll(`[name=editor-gender].ms-drop ul>li input[type=checkbox]`);
+ editorBtnElm.click();
+
+ expect(editorListElm.length).toBe(1);
+ 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"', () => {
+ mockColumn.internalColumnEditor = {
+ 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',
+ },
+ };
+
+ editor = new SelectEditor(editorArguments, true);
+ const editorBtnElm = divContainer.querySelector('.ms-parent.ms-filter.editor-gender button.ms-choice');
+ const editorListElm = divContainer.querySelectorAll(`[name=editor-gender].ms-drop ul>li input[type=checkbox]`);
+ editorBtnElm.click();
+
+ expect(editorListElm.length).toBe(2);
+ expect(editorListElm[0].value).toBe('other');
+ expect(editorListElm[1].value).toBe('male');
+ });
+ });
+
+ describe('collectionInsideObjectProperty setting', () => {
+ it('should create the multi-select editor with a value/label pair collection that is inside an object when "collectionInsideObjectProperty" is defined with a dot notation', () => {
+ mockColumn.internalColumnEditor = {
+ // @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',
+ },
+ };
+
+ editor = new SelectEditor(editorArguments, true);
+ const editorBtnElm = divContainer.querySelector('.ms-parent.ms-filter.editor-gender button.ms-choice');
+ const editorListElm = divContainer.querySelectorAll(`[name=editor-gender].ms-drop ul>li input[type=checkbox]`);
+ editorBtnElm.click();
+
+ expect(editorListElm.length).toBe(3);
+ expect(editorListElm[0].value).toBe('other');
+ expect(editorListElm[1].value).toBe('male');
+ expect(editorListElm[2].value).toBe('female');
+ });
+ });
+
+ describe('enableRenderHtml property', () => {
+ it('should create the multi-select filter with a default search term and have the HTML rendered when "enableRenderHtml" is set', () => {
+ mockColumn.internalColumnEditor = {
+ enableRenderHtml: true,
+ collection: [{ value: true, label: 'True', labelPrefix: ` ` }, { value: false, label: 'False' }],
+ customStructure: {
+ value: 'isEffort',
+ label: 'label',
+ labelPrefix: 'labelPrefix',
+ },
+ };
+
+ editor = new SelectEditor(editorArguments, true);
+ const editorBtnElm = divContainer.querySelector('.ms-parent.ms-filter.editor-gender button.ms-choice');
+ const editorListElm = divContainer.querySelectorAll(`[name=editor-gender].ms-drop ul>li span`);
+ editorBtnElm.click();
+
+ expect(editorListElm.length).toBe(2);
+ expect(editorListElm[0].innerHTML).toBe(' True');
+ });
+
+ it('should create the multi-select filter with a default search term and have the HTML rendered and sanitized when "enableRenderHtml" is set and has ` }, { isEffort: false, label: 'False' }],
+ collectionOptions: {
+ separatorBetweenTextLabels: ': ',
+ includePrefixSuffixToSelectedValues: true,
+ },
+ customStructure: {
+ value: 'isEffort',
+ label: 'label',
+ labelPrefix: 'labelPrefix',
+ },
+ };
+ mockItemData = { id: 1, gender: 'male', isEffort: false };
+
+ editor = new SelectEditor(editorArguments, true);
+ editor.loadValue(mockItemData);
+ editor.setValue([false]);
+ const editorBtnElm = divContainer.querySelector('.ms-parent.ms-filter.editor-gender button.ms-choice');
+ const editorListElm = divContainer.querySelectorAll(`[name=editor-gender].ms-drop ul>li span`);
+ editorBtnElm.click();
+
+ expect(editor.getValue()).toEqual(['']);
+ expect(editorListElm.length).toBe(2);
+ expect(editorListElm[0].innerHTML).toBe(' : True');
+ });
+
+ it('should create the multi-select filter with a default search term and have the HTML rendered and sanitized when using a custom "sanitizer" and "enableRenderHtml" flag is set and has ` }, { isEffort: false, label: 'False' }],
+ collectionOptions: {
+ separatorBetweenTextLabels: ': ',
+ includePrefixSuffixToSelectedValues: true,
+ },
+ customStructure: {
+ value: 'isEffort',
+ label: 'label',
+ labelPrefix: 'labelPrefix',
+ },
+ };
+ mockItemData = { id: 1, gender: 'male', isEffort: false };
+ gridOptionMock.sanitizer = (dirtyHtml) => dirtyHtml.replace(/( ` }, { isEffort: false, label: 'False' }],
+ collectionOptions: {
+ separatorBetweenTextLabels: ': ',
+ includePrefixSuffixToSelectedValues: true,
+ },
+ customStructure: {
+ value: 'isEffort',
+ label: 'label',
+ labelPrefix: 'labelPrefix',
+ },
+ };
+ mockItemData = { id: 1, gender: 'male', isEffort: false };
+
+ editor = new SingleSelectEditor(editorArguments);
+ editor.loadValue(mockItemData);
+ const editorBtnElm = divContainer.querySelector('.ms-parent.ms-filter.editor-gender button.ms-choice');
+ const editorListElm = divContainer.querySelectorAll(`[name=editor-gender].ms-drop ul>li span`);
+ editorBtnElm.click();
+
+ expect(editor.getValue()).toEqual(` : true`);
+ expect(editorListElm.length).toBe(2);
+ expect(editorListElm[0].innerHTML).toBe(' : True');
+ });
+ });
+ });
+});
diff --git a/packages/common/src/editors/index.ts b/packages/common/src/editors/index.ts
index cea38684f..11d721f61 100644
--- a/packages/common/src/editors/index.ts
+++ b/packages/common/src/editors/index.ts
@@ -1,4 +1,6 @@
import { LongTextEditor } from './longTextEditor';
+import { MultipleSelectEditor } from './multipleSelectEditor';
+import { SingleSelectEditor } from './singleSelectEditor';
import { SliderEditor } from './sliderEditor';
import { TextEditor } from './textEditor';
@@ -6,6 +8,12 @@ export const Editors = {
/** Long Text Editor (uses a textarea) */
longText: LongTextEditor,
+ /** Multiple Select editor (which uses 3rd party lib "multiple-select.js") */
+ multipleSelect: MultipleSelectEditor,
+
+ /** Single Select editor (which uses 3rd party lib "multiple-select.js") */
+ singleSelect: SingleSelectEditor,
+
/** Slider Editor */
slider: SliderEditor,
diff --git a/packages/common/src/editors/multipleSelectEditor.ts b/packages/common/src/editors/multipleSelectEditor.ts
new file mode 100644
index 000000000..81219cb77
--- /dev/null
+++ b/packages/common/src/editors/multipleSelectEditor.ts
@@ -0,0 +1,10 @@
+import { SelectEditor } from './selectEditor';
+
+export class MultipleSelectEditor extends SelectEditor {
+ /**
+ * Initialize the Editor
+ */
+ constructor(protected args: any) {
+ super(args, true);
+ }
+}
diff --git a/packages/common/src/editors/selectEditor.ts b/packages/common/src/editors/selectEditor.ts
new file mode 100644
index 000000000..f56b3307d
--- /dev/null
+++ b/packages/common/src/editors/selectEditor.ts
@@ -0,0 +1,628 @@
+import * as DOMPurify from 'dompurify';
+
+import { Constants } from '../constants';
+import { FieldType } from './../enums/index';
+import {
+ CollectionCustomStructure,
+ CollectionOption,
+ Column,
+ ColumnEditor,
+ Editor,
+ EditorArguments,
+ EditorValidator,
+ EditorValidatorOutput,
+ GridOption,
+ Locale,
+ MultipleSelectOption,
+ SelectOption,
+} from './../interfaces/index';
+import { CollectionService, findOrDefault, TranslaterService } from '../services/index';
+import { charArraysEqual, getDescendantProperty, htmlEncode, setDeepValue } from '../services/utilities';
+
+/**
+ * Slickgrid editor class for multiple/single select lists
+ */
+export class SelectEditor implements Editor {
+ /** The JQuery DOM element */
+ $editorElm: any;
+
+ /** Editor Multiple-Select options */
+ editorElmOptions: MultipleSelectOption;
+
+ /** DOM Element Name, useful for auto-detecting positioning (dropup / dropdown) */
+ elementName: string;
+
+ /** The multiple-select options for a multiple select list */
+ defaultOptions: MultipleSelectOption;
+
+ /** The original item values that are set at the beginning */
+ originalValue: any[];
+
+ /** The property name for labels in the collection */
+ labelName: string;
+
+ /** The property name for a prefix that can be added to the labels in the collection */
+ labelPrefixName: string;
+
+ /** The property name for a suffix that can be added to the labels in the collection */
+ labelSuffixName: string;
+
+ /** A label that can be added to each option and can be used as an alternative to display selected options */
+ optionLabel: string;
+
+ /** The property name for values in the collection */
+ valueName: string;
+
+ /** Grid options */
+ gridOptions: GridOption;
+
+ /** Do we translate the label? */
+ enableTranslateLabel: boolean;
+
+ /** Locales */
+ protected _locales: Locale;
+
+ // flag to signal that the editor is destroying itself, helps prevent
+ // commit changes from being called twice and erroring
+ protected _destroying = false;
+
+ /** Collection Service */
+ protected _collectionService: CollectionService;
+
+ /** The translate library */
+ protected _translaterService: TranslaterService;
+
+ /** SlickGrid Grid object */
+ grid: any;
+
+ constructor(protected args: EditorArguments, protected isMultipleSelect: boolean) {
+ if (!args) {
+ throw new Error('[Slickgrid-Universal] Something is wrong with this grid, an Editor must always have valid arguments.');
+ }
+ this.grid = args.grid;
+ this.gridOptions = (this.grid.getOptions() || {}) as GridOption;
+ if (this.gridOptions && this.gridOptions.i18n) {
+ this._translaterService = this.gridOptions.i18n;
+ }
+
+ // get locales provided by user in main file or else use default English locales via the Constants
+ this._locales = this.gridOptions.locales || Constants.locales;
+
+ // provide the name attribute to the DOM element which will be needed to auto-adjust drop position (dropup / dropdown)
+ const columnId = this.columnDef && this.columnDef.id;
+ this.elementName = `editor-${columnId}`;
+
+ const libOptions: MultipleSelectOption = {
+ autoAdjustDropHeight: true,
+ autoAdjustDropPosition: true,
+ autoAdjustDropWidthByTextSize: true,
+ container: 'body',
+ filter: false,
+ maxHeight: 275,
+ name: this.elementName,
+ single: true,
+ textTemplate: ($elm) => {
+ // render HTML code or not, by default it is sanitized and won't be rendered
+ const isRenderHtmlEnabled = this.columnEditor && this.columnEditor.enableRenderHtml || false;
+ return isRenderHtmlEnabled ? $elm.text() : $elm.html();
+ },
+ onClose: () => this.save(),
+ };
+
+ if (isMultipleSelect) {
+ libOptions.single = false;
+ libOptions.addTitle = true;
+ libOptions.okButton = true;
+ libOptions.selectAllDelimiter = ['', ''];
+
+ if (this._translaterService && this._translaterService.translate && this._translaterService.getCurrentLocale && this._translaterService.getCurrentLocale()) {
+ libOptions.countSelected = this._translaterService.translate('X_OF_Y_SELECTED');
+ libOptions.allSelected = this._translaterService.translate('ALL_SELECTED');
+ libOptions.selectAllText = this._translaterService.translate('SELECT_ALL');
+ libOptions.okButtonText = this._translaterService.translate('OK');
+ } else {
+ libOptions.countSelected = this._locales && this._locales.TEXT_X_OF_Y_SELECTED;
+ libOptions.allSelected = this._locales && this._locales.TEXT_ALL_SELECTED;
+ libOptions.selectAllText = this._locales && this._locales.TEXT_SELECT_ALL;
+ libOptions.okButtonText = this._locales && this._locales.TEXT_OK;
+ }
+ }
+
+ // assign the multiple select lib options
+ this.defaultOptions = libOptions;
+ this.init();
+ }
+
+ /** Get the Collection */
+ get collection(): SelectOption[] {
+ return this.columnEditor && this.columnEditor.collection || [];
+ }
+
+ /** Getter for the Collection Options */
+ get collectionOptions(): CollectionOption | undefined {
+ return this.columnEditor && this.columnEditor.collectionOptions;
+ }
+
+ /** Get Column Definition object */
+ get columnDef(): Column | undefined {
+ return this.args && this.args.column;
+ }
+
+ /** Get Column Editor object */
+ get columnEditor(): ColumnEditor | undefined {
+ return this.columnDef && this.columnDef.internalColumnEditor || {};
+ }
+
+ /** Get the Editor DOM Element */
+ get editorDomElement(): any {
+ return this.$editorElm;
+ }
+
+ /** Getter for the Custom Structure if exist */
+ protected get customStructure(): CollectionCustomStructure | undefined {
+ return this.columnDef && this.columnDef.internalColumnEditor && this.columnDef.internalColumnEditor.customStructure;
+ }
+
+ get hasAutoCommitEdit() {
+ return this.grid.getOptions().autoCommitEdit;
+ }
+
+ /**
+ * The current selected values (multiple select) from the collection
+ */
+ get currentValues(): any[] | null {
+ const elmValue = this.$editorElm.val();
+
+ // collection of strings, just return the filtered string that are equals
+ if (this.collection.every(x => typeof x === 'string')) {
+ return this.collection.filter(c => elmValue.indexOf(c.toString()) !== -1);
+ }
+
+ // collection of label/value pair
+ const separatorBetweenLabels = this.collectionOptions && this.collectionOptions.separatorBetweenTextLabels || '';
+ const isIncludingPrefixSuffix = this.collectionOptions && this.collectionOptions.includePrefixSuffixToSelectedValues || false;
+
+ return this.collection
+ .filter(c => elmValue.indexOf(c.hasOwnProperty(this.valueName) && c[this.valueName].toString()) !== -1)
+ .map(c => {
+ const labelText = c[this.valueName];
+ let prefixText = c[this.labelPrefixName] || '';
+ let suffixText = c[this.labelSuffixName] || '';
+
+ // when it's a complex object, then pull the object name only, e.g.: "user.firstName" => "user"
+ const fieldName = this.columnDef && this.columnDef.field || '';
+
+ // is the field a complex object, "address.streetNumber"
+ const isComplexObject = fieldName.indexOf('.') > 0;
+ if (isComplexObject && typeof c === 'object') {
+ return c;
+ }
+
+ // 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;
+ suffixText = (this.enableTranslateLabel && suffixText && typeof suffixText === 'string') ? this._translaterService.translate(suffixText || ' ') : suffixText;
+
+ if (isIncludingPrefixSuffix) {
+ const tmpOptionArray = [prefixText, labelText, suffixText].filter((text) => text); // add to a temp array for joining purpose and filter out empty text
+ return tmpOptionArray.join(separatorBetweenLabels);
+ }
+ return labelText;
+ });
+ }
+
+ /**
+ * The current selected values (single select) from the collection
+ */
+ get currentValue(): number | string {
+ const elmValue = this.$editorElm.val();
+ const fieldName = this.columnDef && this.columnDef.field;
+
+ if (fieldName !== undefined) {
+ // collection of strings, just return the filtered string that are equals
+ if (this.collection.every(x => typeof x === 'string')) {
+ return findOrDefault(this.collection, (c: any) => c.toString() === elmValue);
+ }
+
+ // collection of label/value pair
+ const separatorBetweenLabels = this.collectionOptions && this.collectionOptions.separatorBetweenTextLabels || '';
+ const isIncludingPrefixSuffix = this.collectionOptions && this.collectionOptions.includePrefixSuffixToSelectedValues || false;
+ const itemFound = findOrDefault(this.collection, (c: any) => c.hasOwnProperty(this.valueName) && c[this.valueName].toString() === elmValue);
+
+ // is the field a complex object, "address.streetNumber"
+ const isComplexObject = fieldName.indexOf('.') > 0;
+
+ if (isComplexObject && typeof itemFound === 'object') {
+ return itemFound;
+ } else if (itemFound && itemFound.hasOwnProperty(this.valueName)) {
+ const labelText = itemFound[this.valueName];
+
+ if (isIncludingPrefixSuffix) {
+ let prefixText = itemFound[this.labelPrefixName] || '';
+ let suffixText = itemFound[this.labelSuffixName] || '';
+
+ // 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;
+ suffixText = (this.enableTranslateLabel && suffixText && typeof suffixText === 'string') ? this._translaterService.translate(suffixText || ' ') : suffixText;
+
+ // add to a temp array for joining purpose and filter out empty text
+ const tmpOptionArray = [prefixText, labelText, suffixText].filter((text) => text);
+ return tmpOptionArray.join(separatorBetweenLabels);
+ }
+ return labelText;
+ }
+ }
+ return '';
+ }
+
+ /** Get the Validator function, can be passed in Editor property or Column Definition */
+ get validator(): EditorValidator | undefined {
+ return (this.columnEditor && this.columnEditor.validator) || (this.columnDef && this.columnDef.validator);
+ }
+
+ init() {
+ if (!this.columnDef || !this.columnDef.internalColumnEditor || (!this.columnDef.internalColumnEditor.collection && !this.columnDef.internalColumnEditor.collectionAsync)) {
+ throw new Error(`[Slickgrid-Universal] You need to pass a "collection" (or "collectionAsync") inside Column Definition Editor for the MultipleSelect/SingleSelect Editor to work correctly.
+ Also each option should include a value/label pair (or value/labelKey when using Locale).
+ For example: { editor: { collection: [{ value: true, label: 'True' },{ value: false, label: 'False'}] } }`);
+ }
+
+ this._collectionService = new CollectionService(this._translaterService);
+ this.enableTranslateLabel = this.columnEditor && this.columnEditor.enableTranslateLabel || false;
+ this.labelName = this.customStructure && this.customStructure.label || 'label';
+ this.labelPrefixName = this.customStructure && this.customStructure.labelPrefix || 'labelPrefix';
+ this.labelSuffixName = this.customStructure && this.customStructure.labelSuffix || 'labelSuffix';
+ this.optionLabel = this.customStructure && this.customStructure.optionLabel || 'value';
+ this.valueName = this.customStructure && this.customStructure.value || 'value';
+
+ if (this.enableTranslateLabel && (!this._translaterService || typeof this._translaterService.translate !== 'function')) {
+ throw new Error('[Slickgrid-Universal] requires a Translate Service to be installed and configured when the grid option "enableTranslate" is enabled.');
+ }
+
+ // always render the Select (dropdown) DOM element, even if user passed a "collectionAsync",
+ // if that is the case, the Select will simply be without any options but we still have to render it (else SlickGrid would throw an error)
+ this.renderDomElement(this.collection);
+ }
+
+ getValue(): any | any[] {
+ return (this.isMultipleSelect) ? this.currentValues : this.currentValue;
+ }
+
+ setValue(value: any | any[]) {
+ if (this.isMultipleSelect && Array.isArray(value)) {
+ this.loadMultipleValues(value);
+ } else {
+ this.loadSingleValue(value);
+ }
+ }
+
+ hide() {
+ if (this.$editorElm && typeof this.$editorElm.multipleSelect === 'function') {
+ this.$editorElm.multipleSelect('close');
+ }
+ }
+
+ show() {
+ if (this.$editorElm && typeof this.$editorElm.multipleSelect === 'function') {
+ this.$editorElm.multipleSelect('open');
+ }
+ }
+
+ applyValue(item: any, state: any): void {
+ const fieldName = this.columnDef && this.columnDef.field;
+ const fieldType = this.columnDef && this.columnDef.type;
+ let newValue = state;
+
+ if (fieldName !== undefined) {
+ // when the provided user defined the column field type as a possible number then try parsing the state value as that
+ if (fieldType === FieldType.number || fieldType === FieldType.integer || fieldType === FieldType.boolean) {
+ newValue = parseFloat(state);
+ }
+
+ // when set as a multiple selection, we can assume that the 3rd party lib multiple-select will return a CSV string
+ // we need to re-split that into an array to be the same as the original column
+ if (this.isMultipleSelect && typeof state === 'string' && state.indexOf(',') >= 0) {
+ newValue = state.split(',');
+ }
+
+ // is the field a complex object, "user.address.streetNumber"
+ const isComplexObject = fieldName !== undefined && fieldName.indexOf('.') > 0;
+
+ // validate the value before applying it (if not valid we'll set an empty string)
+ const validation = this.validate(newValue);
+ newValue = (validation && validation.valid) ? newValue : '';
+
+ // set the new value to the item datacontext
+ if (isComplexObject) {
+ // when it's a complex object, user could override the object path (where the editable object is located)
+ // else we use the path provided in the Field Column Definition
+ const objectPath = this.columnEditor && this.columnEditor.complexObjectPath || fieldName || '';
+ setDeepValue(item, objectPath, newValue);
+ } else if (fieldName !== undefined) {
+ item[fieldName] = newValue;
+ }
+ }
+ }
+
+ destroy() {
+ this._destroying = true;
+ if (this.$editorElm && typeof this.$editorElm.multipleSelect === 'function') {
+ this.$editorElm.multipleSelect('destroy');
+ const elementClassName = this.elementName.toString().replace('.', '\\.'); // make sure to escape any dot "." from CSS class to avoid console error
+ $(`[name=${elementClassName}].ms-drop`).remove();
+ }
+ if (this.$editorElm && typeof this.$editorElm.remove === 'function') {
+ this.$editorElm.remove();
+ }
+ }
+
+ loadValue(item: any): void {
+ const fieldName = this.columnDef && this.columnDef.field;
+
+ // is the field a complex object, "address.streetNumber"
+ const isComplexObject = fieldName !== undefined && fieldName.indexOf('.') > 0;
+
+ if (item && this.columnDef && fieldName !== undefined && (item.hasOwnProperty(fieldName) || isComplexObject)) {
+ // when it's a complex object, user could override the object path (where the editable object is located)
+ // else we use the path provided in the Field Column Definition
+ const objectPath = this.columnEditor && this.columnEditor.complexObjectPath || fieldName;
+ const currentValue = (isComplexObject) ? getDescendantProperty(item, objectPath as string) : (item.hasOwnProperty(fieldName) && item[fieldName]);
+ const value = (isComplexObject && currentValue.hasOwnProperty(this.valueName)) ? currentValue[this.valueName] : currentValue;
+
+ if (this.isMultipleSelect && Array.isArray(value)) {
+ this.loadMultipleValues(value);
+ } else {
+ this.loadSingleValue(value);
+ }
+ this.refresh();
+ }
+ }
+
+ loadMultipleValues(currentValues: any[]) {
+ // convert to string because that is how the DOM will return these values
+ if (Array.isArray(currentValues)) {
+ // keep the default values in memory for references
+ this.originalValue = currentValues.map((i: any) => i);
+
+ // compare all the array values but as string type since multiple-select always return string
+ const currentStringValues = currentValues.map((i: any) => i.toString());
+ this.$editorElm.find('option').each((i: number, $e: any) => {
+ $e.selected = (currentStringValues.indexOf($e.value) !== -1);
+ });
+ }
+ }
+
+ loadSingleValue(currentValue: any) {
+ // keep the default value in memory for references
+ this.originalValue = typeof currentValue === 'number' ? `${currentValue}` : currentValue;
+ this.$editorElm.val(currentValue);
+
+ // make sure the prop exists first
+ this.$editorElm.find('option').each((i: number, $e: any) => {
+ // check equality after converting originalValue to string since the DOM value will always be of type string
+ const strValue = currentValue && currentValue.toString && currentValue.toString();
+ $e.selected = (strValue === $e.value);
+ });
+ }
+
+ save() {
+ // autocommit will not focus the next editor
+ const validation = this.validate();
+ if (validation && validation.valid && this.isValueChanged()) {
+ if (!this._destroying && this.hasAutoCommitEdit) {
+ // do not use args.commitChanges() as this sets the focus to the next
+ // row. Also the select list will stay shown when clicking off the grid
+ this.grid.getEditorLock().commitCurrentEdit();
+ }
+ }
+ }
+
+ serializeValue(): any | any[] {
+ return (this.isMultipleSelect) ? this.currentValues : this.currentValue;
+ }
+
+ focus() {
+ if (this.$editorElm && this.$editorElm.multipleSelect) {
+ this.$editorElm.multipleSelect('focus');
+ }
+ }
+
+ isValueChanged(): boolean {
+ if (this.isMultipleSelect) {
+ return !charArraysEqual(this.$editorElm.val(), this.originalValue);
+ }
+ return this.$editorElm.val() !== this.originalValue;
+ }
+
+ validate(inputValue?: any): EditorValidatorOutput {
+ const isRequired = this.columnEditor && this.columnEditor.required;
+ const elmValue = (inputValue !== undefined) ? inputValue : this.$editorElm && this.$editorElm.val && this.$editorElm.val();
+ const errorMsg = this.columnEditor && this.columnEditor.errorMessage;
+
+ if (this.validator) {
+ const value = (inputValue !== undefined) ? inputValue : (this.isMultipleSelect ? this.currentValues : this.currentValue);
+ return this.validator(value, this.args);
+ }
+
+ // by default the editor is almost always valid (except when it's required but not provided)
+ if (isRequired && (elmValue === '' || (Array.isArray(elmValue) && elmValue.length === 0))) {
+ return {
+ valid: false,
+ msg: errorMsg || Constants.VALIDATION_REQUIRED_FIELD
+ };
+ }
+
+ return {
+ valid: true,
+ msg: null
+ };
+ }
+
+ //
+ // protected functions
+ // ------------------
+
+ /**
+ * user might want to filter certain items of the collection
+ * @param inputCollection
+ * @return outputCollection filtered and/or sorted collection
+ */
+ protected filterCollection(inputCollection: any[]): any[] {
+ let outputCollection = inputCollection;
+
+ // user might want to filter certain items of the collection
+ if (this.columnEditor && this.columnEditor.collectionFilterBy) {
+ const filterBy = this.columnEditor.collectionFilterBy;
+ const filterCollectionBy = this.columnEditor.collectionOptions && this.columnEditor.collectionOptions.filterResultAfterEachPass || null;
+ outputCollection = this._collectionService.filterCollection(outputCollection, filterBy, filterCollectionBy);
+ }
+
+ return outputCollection;
+ }
+
+ /**
+ * user might want to sort the collection in a certain way
+ * @param inputCollection
+ * @return outputCollection filtered and/or sorted collection
+ */
+ protected sortCollection(inputCollection: any[]): any[] {
+ let outputCollection = inputCollection;
+
+ // user might want to sort the collection
+ if (this.columnDef && this.columnEditor && this.columnEditor.collectionSortBy) {
+ const sortBy = this.columnEditor.collectionSortBy;
+ outputCollection = this._collectionService.sortCollection(this.columnDef, outputCollection, sortBy, this.enableTranslateLabel);
+ }
+
+ return outputCollection;
+ }
+
+ protected renderDomElement(collection: any[]) {
+ 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.');
+ }
+
+ // user can optionally add a blank entry at the beginning of the collection
+ if (this.collectionOptions && this.collectionOptions.addBlankEntry) {
+ collection.unshift(this.createBlankEntry());
+ }
+
+ // assign the collection to a temp variable before filtering/sorting the collection
+ let newCollection = collection;
+
+ // user might want to filter and/or sort certain items of the collection
+ newCollection = this.filterCollection(newCollection);
+ newCollection = this.sortCollection(newCollection);
+
+ // step 1, create HTML string template
+ const editorTemplate = this.buildTemplateHtmlString(newCollection);
+
+ // step 2, create the DOM Element of the editor
+ // also subscribe to the onClose event
+ this.createDomElement(editorTemplate);
+ }
+
+ /** Build the template HTML string */
+ protected buildTemplateHtmlString(collection: any[]): string {
+ let options = '';
+ const columnId = this.columnDef && this.columnDef.id || '';
+ const separatorBetweenLabels = this.collectionOptions && this.collectionOptions.separatorBetweenTextLabels || '';
+ const isRenderHtmlEnabled = this.columnEditor && this.columnEditor.enableRenderHtml || false;
+ const sanitizedOptions = this.gridOptions && this.gridOptions.sanitizeHtmlOptions || {};
+
+ // collection could be an Array of Strings OR Objects
+ if (collection.every((x: any) => typeof x === 'string')) {
+ collection.forEach((option: string) => {
+ options += ``;
+ });
+ } else {
+ // array of objects will require a label/value pair unless a customStructure is passed
+ collection.forEach((option: SelectOption) => {
+ if (!option || (option[this.labelName] === undefined && option.labelKey === undefined)) {
+ throw new Error('[select-editor] A collection with value/label (or value/labelKey when using ' +
+ 'Locale) is required to populate the Select list, for example: ' +
+ '{ collection: [ { value: \'1\', label: \'One\' } ])');
+ }
+ const labelKey = (option.labelKey || option[this.labelName]) as string;
+ const labelText = ((option.labelKey || this.enableTranslateLabel) && labelKey) ? this._translaterService.translate(labelKey || ' ') : labelKey;
+ 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
+
+ // 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;
+ suffixText = (this.enableTranslateLabel && suffixText && typeof suffixText === 'string') ? this._translaterService.translate(suffixText || ' ') : suffixText;
+ optionLabel = (this.enableTranslateLabel && optionLabel && typeof optionLabel === 'string') ? this._translaterService.translate(optionLabel || ' ') : optionLabel;
+
+ // add to a temp array for joining purpose and filter out empty text
+ const tmpOptionArray = [prefixText, labelText, suffixText].filter(text => (text !== undefined && text !== ''));
+ let optionText = tmpOptionArray.join(separatorBetweenLabels);
+
+ // if user specifically wants to render html text, he needs to opt-in else it will stripped out by default
+ // also, the 3rd party lib will saninitze any html code unless it's encoded, so we'll do that
+ if (isRenderHtmlEnabled) {
+ // sanitize any unauthorized html tags like script and others
+ // for the remaining allowed tags we'll permit all attributes
+ let sanitizedText = '';
+ if (this.gridOptions && typeof this.gridOptions.sanitizer === 'function') {
+ sanitizedText = this.gridOptions.sanitizer(optionText);
+ } else {
+ sanitizedText = (DOMPurify.sanitize(optionText, sanitizedOptions) || '').toString();
+ }
+ optionText = htmlEncode(sanitizedText);
+ }
+
+ options += ``;
+ });
+ }
+ return ``;
+ }
+
+ /** Create a blank entry that can be added to the collection. It will also reuse the same collection structure provided by the user */
+ protected createBlankEntry(): any {
+ const blankEntry = {
+ [this.labelName]: '',
+ [this.valueName]: ''
+ };
+ if (this.labelPrefixName) {
+ blankEntry[this.labelPrefixName] = '';
+ }
+ if (this.labelSuffixName) {
+ blankEntry[this.labelSuffixName] = '';
+ }
+ return blankEntry;
+ }
+
+ /** From the html template string, create the DOM element of the Multiple/Single Select Editor */
+ protected createDomElement(editorTemplate: string) {
+ this.$editorElm = $(editorTemplate);
+
+ if (this.$editorElm && typeof this.$editorElm.appendTo === 'function') {
+ this.$editorElm.appendTo(this.args.container);
+ }
+
+ // add placeholder when found
+ const placeholder = this.columnEditor && this.columnEditor.placeholder || '';
+ this.defaultOptions.placeholder = placeholder || '';
+
+ if (typeof this.$editorElm.multipleSelect === 'function') {
+ const elementOptions = (this.columnEditor) ? this.columnEditor.elementOptions : {};
+ const editorOptions = (this.columnDef && this.columnDef.internalColumnEditor) ? this.columnDef.internalColumnEditor.editorOptions : {};
+ this.editorElmOptions = { ...this.defaultOptions, ...elementOptions, ...editorOptions };
+ this.$editorElm = this.$editorElm.multipleSelect(this.editorElmOptions);
+ setTimeout(() => this.show());
+ }
+ }
+
+ // refresh the jquery object because the selected checkboxes were already set
+ // prior to this method being called
+ protected refresh() {
+ if (typeof this.$editorElm.multipleSelect === 'function') {
+ this.$editorElm.multipleSelect('refresh');
+ }
+ }
+}
diff --git a/packages/common/src/editors/singleSelectEditor.ts b/packages/common/src/editors/singleSelectEditor.ts
new file mode 100644
index 000000000..d9abcae7f
--- /dev/null
+++ b/packages/common/src/editors/singleSelectEditor.ts
@@ -0,0 +1,10 @@
+import { SelectEditor } from './selectEditor';
+
+export class SingleSelectEditor extends SelectEditor {
+ /**
+ * Initialize the Editor
+ */
+ constructor(protected args: any) {
+ super(args, false);
+ }
+}
diff --git a/packages/common/src/formatters/__tests__/checkmarkMaterialFormatter.spec.ts b/packages/common/src/formatters/__tests__/checkmarkMaterialFormatter.spec.ts
new file mode 100644
index 000000000..850733f62
--- /dev/null
+++ b/packages/common/src/formatters/__tests__/checkmarkMaterialFormatter.spec.ts
@@ -0,0 +1,98 @@
+import { Column } from '../../interfaces/index';
+import { checkmarkMaterialFormatter } from '../checkmarkMaterialFormatter';
+
+describe('the Checkmark Formatter with Material Design Icon', () => {
+ it('should return an empty string when no value is passed', () => {
+ const value = null;
+ const result = checkmarkMaterialFormatter(0, 0, value, {} as Column, {});
+ expect(result).toBe('');
+ });
+
+ it('should return an empty string when False is provided', () => {
+ const value = false;
+ const result = checkmarkMaterialFormatter(0, 0, value, {} as Column, {});
+ expect(result).toBe('');
+ });
+
+ it('should return an empty string when the string "FALSE" (case insensitive) is provided', () => {
+ const value = 'FALSE';
+ const result1 = checkmarkMaterialFormatter(0, 0, value.toLowerCase(), {} as Column, {});
+ const result2 = checkmarkMaterialFormatter(0, 0, value.toUpperCase(), {} as Column, {});
+ expect(result1).toBe('');
+ expect(result2).toBe('');
+ });
+
+ it('should return the Font Awesome Checkmark icon when the string "True" (case insensitive) is provided', () => {
+ const value = 'True';
+ const result1 = checkmarkMaterialFormatter(0, 0, value.toLowerCase(), {} as Column, {});
+ const result2 = checkmarkMaterialFormatter(0, 0, value.toUpperCase(), {} as Column, {});
+ expect(result1).toBe('');
+ expect(result2).toBe('');
+ });
+
+ it('should return the Font Awesome Checkmark icon when input is True', () => {
+ const value = true;
+ const result = checkmarkMaterialFormatter(0, 0, value, {} as Column, {});
+ expect(result).toBe('');
+ });
+
+ it('should return the Font Awesome Checkmark icon when input is a string even if it start with 0', () => {
+ const value = '005A00ABC';
+ const result1 = checkmarkMaterialFormatter(0, 0, value, {} as Column, {});
+ expect(result1).toBe('');
+ });
+
+ it('should return an empty string when the string "0" is provided', () => {
+ const value = '0';
+ const result = checkmarkMaterialFormatter(0, 0, value, {} as Column, {});
+ expect(result).toBe('');
+ });
+
+ it('should return the Font Awesome Checkmark icon when input is a number greater than 0', () => {
+ const value = 0.000001;
+ const result1 = checkmarkMaterialFormatter(0, 0, value, {} as Column, {});
+ expect(result1).toBe('');
+ });
+
+ it('should return the Font Awesome Checkmark icon when input is a number as a text greater than 0', () => {
+ const value = '0.000001';
+ const result1 = checkmarkMaterialFormatter(0, 0, value, {} as Column, {});
+ expect(result1).toBe('');
+ });
+
+ it('should return an empty string when input is a number lower or equal to 0', () => {
+ const value1 = 0;
+ const value2 = -0.5;
+ const result1 = checkmarkMaterialFormatter(0, 0, value1, {} as Column, {});
+ const result2 = checkmarkMaterialFormatter(0, 0, value2, {} as Column, {});
+ expect(result1).toBe('');
+ expect(result2).toBe('');
+ });
+
+ it('should return an empty string when input is a number as a text and lower or equal to 0', () => {
+ const value1 = '0';
+ const value2 = '-0.5';
+ const result1 = checkmarkMaterialFormatter(0, 0, value1, {} as Column, {});
+ const result2 = checkmarkMaterialFormatter(0, 0, value2, {} as Column, {});
+ expect(result1).toBe('');
+ expect(result2).toBe('');
+ });
+
+ it('should return an empty string when input is type null or undefined', () => {
+ const value1 = null;
+ const value2 = undefined;
+ const result1 = checkmarkMaterialFormatter(0, 0, value1, {} as Column, {});
+ const result2 = checkmarkMaterialFormatter(0, 0, value2, {} as Column, {});
+ expect(result1).toBe('');
+ expect(result2).toBe('');
+ });
+
+ it('should return the Font Awesome Checkmark icon when input is the "null" or "undefined"', () => {
+ const value1 = 'null';
+ const value2 = 'undefined';
+ const result1 = checkmarkMaterialFormatter(0, 0, value1, {} as Column, {});
+ const result2 = checkmarkMaterialFormatter(0, 0, value2, {} as Column, {});
+ expect(result1).toBe('');
+ expect(result2).toBe('');
+ });
+});
diff --git a/packages/common/src/formatters/__tests__/collectionEditorFormatter.spec.ts b/packages/common/src/formatters/__tests__/collectionEditorFormatter.spec.ts
index d616e33eb..aee016454 100644
--- a/packages/common/src/formatters/__tests__/collectionEditorFormatter.spec.ts
+++ b/packages/common/src/formatters/__tests__/collectionEditorFormatter.spec.ts
@@ -4,7 +4,7 @@ import { Editors } from '../../editors';
jest.mock('flatpickr', () => { });
-xdescribe('the CollectionEditor Formatter', () => {
+describe('the CollectionEditor Formatter', () => {
let columnDef: Column;
beforeEach(() => {
diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts
index 0186277fe..8471ed3e9 100644
--- a/packages/common/src/index.ts
+++ b/packages/common/src/index.ts
@@ -1,3 +1,5 @@
+import 'multiple-select-adapted/src/multiple-select.js';
+
// Public classes.
export * from './global-grid-options';
export * from './enums/index';
@@ -14,4 +16,4 @@ export * from './grouping-formatters/index';
export * from './sorters/index';
import * as Enums from './enums/index';
-export { Enums }
\ No newline at end of file
+export { Enums }
diff --git a/packages/common/src/interfaces/gridOption.interface.ts b/packages/common/src/interfaces/gridOption.interface.ts
index d221d2ded..8e62e137f 100644
--- a/packages/common/src/interfaces/gridOption.interface.ts
+++ b/packages/common/src/interfaces/gridOption.interface.ts
@@ -394,6 +394,12 @@ export interface GridOption {
*/
sanitizeHtmlOptions?: any;
+ /**
+ * By default the lib will use DOMPurify to sanitize any Html,
+ * but you could optionally pass your own sanitizer function which will run instead of DOM Purify
+ */
+ sanitizer?: (dirtyHtml: string) => string;
+
/** CSS class name used when cell is selected */
selectedCellCssClass?: string;
diff --git a/packages/common/src/interfaces/index.ts b/packages/common/src/interfaces/index.ts
index 17301a227..bd771a919 100644
--- a/packages/common/src/interfaces/index.ts
+++ b/packages/common/src/interfaces/index.ts
@@ -7,6 +7,7 @@ export * from './backendServiceOption.interface';
export * from './cellArgs.interface';
export * from './cellMenu.interface';
export * from './checkboxSelector.interface';
+export * from './collectionCustomStructure.interface';
export * from './collectionFilterBy.interface';
export * from './collectionOption.interface';
export * from './collectionSortBy.interface';
@@ -19,10 +20,9 @@ export * from './columnSort.interface';
export * from './contextMenu.interface';
export * from './currentColumn.interface';
export * from './currentFilter.interface';
-export * from './collectionCustomStructure.interface';
export * from './currentPagination.interface';
-export * from './currentSorter.interface';
export * from './currentRowSelection.interface';
+export * from './currentSorter.interface';
export * from './draggableGrouping.interface';
export * from './editCommand.interface';
export * from './editor.interface';
@@ -76,6 +76,7 @@ export * from './onEventArgs.interface';
export * from './pagination.interface';
export * from './paginationChangedArgs.interface';
export * from './selectedRange.interface';
+export * from './selectOption.interface';
export * from './slickEvent.interface';
export * from './slickEventData.interface';
export * from './slickEventHandler.interface';
diff --git a/packages/common/src/interfaces/selectOption.interface.ts b/packages/common/src/interfaces/selectOption.interface.ts
new file mode 100644
index 000000000..e0237c691
--- /dev/null
+++ b/packages/common/src/interfaces/selectOption.interface.ts
@@ -0,0 +1,6 @@
+export interface SelectOption {
+ label: string;
+ labelKey: string;
+ value: number | string;
+ [labelValue: string]: number | string;
+}
diff --git a/packages/common/src/services/collection.service.ts b/packages/common/src/services/collection.service.ts
new file mode 100644
index 000000000..f4f4574a9
--- /dev/null
+++ b/packages/common/src/services/collection.service.ts
@@ -0,0 +1,162 @@
+import {
+ FilterMultiplePassType,
+ FilterMultiplePassTypeString,
+ FieldType,
+ OperatorType,
+ SortDirectionNumber,
+} from './../enums/index';
+import { CollectionFilterBy, CollectionSortBy, Column } from './../interfaces/index';
+import { sortByFieldType } from '../sorters/sorterUtilities';
+import { uniqueArray } from './utilities';
+import { TranslaterService } from './translater.service';
+
+export class CollectionService {
+ constructor(private translaterService: TranslaterService) { }
+
+ /**
+ * Filter 1 or more items from a collection
+ * @param collection
+ * @param filterByOptions
+ */
+ filterCollection(collection: any[], filterByOptions: CollectionFilterBy | CollectionFilterBy[], filterResultBy: FilterMultiplePassType | FilterMultiplePassTypeString | null = FilterMultiplePassType.chain): any[] {
+ let filteredCollection: any[] = [];
+
+ // when it's array, we will use the new filtered collection after every pass
+ // basically if input collection has 10 items on 1st pass and 1 item is filtered out, then on 2nd pass the input collection will be 9 items
+ if (Array.isArray(filterByOptions)) {
+ filteredCollection = (filterResultBy === FilterMultiplePassType.merge) ? [] : [...collection];
+
+ for (const filter of filterByOptions) {
+ if (filterResultBy === FilterMultiplePassType.merge) {
+ const filteredPass = this.singleFilterCollection(collection, filter);
+ filteredCollection = uniqueArray([...filteredCollection, ...filteredPass]);
+ } else {
+ filteredCollection = this.singleFilterCollection(filteredCollection, filter);
+ }
+ }
+ } else {
+ filteredCollection = this.singleFilterCollection(collection, filterByOptions);
+ }
+
+ return filteredCollection;
+ }
+
+ /**
+ * Filter an item from a collection
+ * @param collection
+ * @param filterBy
+ */
+ singleFilterCollection(collection: any[], filterBy: CollectionFilterBy): any[] {
+ let filteredCollection: any[] = [];
+
+ 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:
+ if (objectProperty) {
+ filteredCollection = collection.filter((item) => item[objectProperty] === value);
+ } else {
+ filteredCollection = collection.filter((item) => item === value);
+ }
+ break;
+ case OperatorType.contains:
+ 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:
+ 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:
+ if (objectProperty) {
+ filteredCollection = collection.filter((item) => item[objectProperty] !== value);
+ } else {
+ filteredCollection = collection.filter((item) => item !== value);
+ }
+ }
+ }
+ return filteredCollection;
+ }
+
+ /**
+ * Sort 1 or more items in a collection
+ * @param column definition
+ * @param collection
+ * @param sortByOptions
+ * @param enableTranslateLabel
+ */
+ sortCollection(columnDef: Column, collection: any[], sortByOptions: CollectionSortBy | CollectionSortBy[], enableTranslateLabel?: boolean): any[] {
+ if (enableTranslateLabel && (!this.translaterService || !this.translaterService.translate)) {
+ throw new Error('[Aurelia-Slickgrid] requires "I18N" to be installed and configured when the grid option "enableTranslate" is enabled.');
+ }
+
+ let sortedCollection: any[] = [];
+
+ if (sortByOptions) {
+ if (Array.isArray(sortByOptions)) {
+ // multi-sort
+ sortedCollection = collection.sort((dataRow1: any, dataRow2: any) => {
+ for (let i = 0, l = sortByOptions.length; i < l; i++) {
+ 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 objectProperty = sortBy.property;
+ const fieldType = sortBy.fieldType || FieldType.string;
+ const value1 = (enableTranslateLabel) ? this.translaterService && this.translaterService.translate && this.translaterService.getCurrentLocale && this.translaterService.getCurrentLocale() && this.translaterService.translate(dataRow1[objectProperty] || ' ') : dataRow1[objectProperty];
+ const value2 = (enableTranslateLabel) ? this.translaterService && this.translaterService.translate && this.translaterService.getCurrentLocale && this.translaterService.getCurrentLocale() && this.translaterService.translate(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) {
+ // single sort
+ // 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.translaterService && this.translaterService.translate && this.translaterService.getCurrentLocale && this.translaterService.getCurrentLocale() && this.translaterService.translate(dataRow1[objectProperty] || ' ') : dataRow1[objectProperty];
+ const value2 = (enableTranslateLabel) ? this.translaterService && this.translaterService.translate && this.translaterService.getCurrentLocale && this.translaterService.getCurrentLocale() && this.translaterService.translate(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;
+
+ sortedCollection = collection.sort((dataRow1: any, dataRow2: any) => {
+ const value1 = (enableTranslateLabel) ? this.translaterService && this.translaterService.translate && this.translaterService.getCurrentLocale && this.translaterService.getCurrentLocale() && this.translaterService.translate(dataRow1 || ' ') : dataRow1;
+ const value2 = (enableTranslateLabel) ? this.translaterService && this.translaterService.translate && this.translaterService.getCurrentLocale && this.translaterService.getCurrentLocale() && this.translaterService.translate(dataRow2 || ' ') : dataRow2;
+ const sortResult = sortByFieldType(fieldType, value1, value2, sortDirection, columnDef);
+ if (sortResult !== SortDirectionNumber.neutral) {
+ return sortResult;
+ }
+ return SortDirectionNumber.neutral;
+ });
+ }
+ }
+ return sortedCollection;
+ }
+}
diff --git a/packages/common/src/services/index.ts b/packages/common/src/services/index.ts
index d5ac797c7..3277d4398 100644
--- a/packages/common/src/services/index.ts
+++ b/packages/common/src/services/index.ts
@@ -1,3 +1,4 @@
+export * from './collection.service';
export * from './export-utilities';
export * from './extension.service';
export * from './grid.service';
diff --git a/packages/common/src/services/translater.service.d.ts b/packages/common/src/services/translater.service.d.ts
deleted file mode 100644
index f72c06f85..000000000
--- a/packages/common/src/services/translater.service.d.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-export declare abstract class TranslaterService {
- /**
- * Method to return the current locale used by the App
- * @return {string} current locale
- */
- getCurrentLocale(): string;
- /**
- * Method to set the locale to use in the App
- * @param locale
- */
- setLocale(locale: string): Promise;
- /**
- * Method which receives a translation key and returns the translated value from that key
- * @param {string} translation key
- * @return {string} translated value
- */
- translate(translationKey: string): string;
-}
-//# sourceMappingURL=translater.service.d.ts.map
\ No newline at end of file
diff --git a/packages/common/src/services/translater.service.d.ts.map b/packages/common/src/services/translater.service.d.ts.map
deleted file mode 100644
index 65cae0eff..000000000
--- a/packages/common/src/services/translater.service.d.ts.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"translater.service.d.ts","sourceRoot":"","sources":["translater.service.ts"],"names":[],"mappings":"AAAA,8BAAsB,iBAAiB;IACrC;;;OAGG;IACH,gBAAgB,IAAI,MAAM;IAM1B;;;OAGG;IACH,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAIvC;;;;OAIG;IACH,SAAS,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM;CAI1C"}
\ No newline at end of file