diff --git a/examples/webpack-demo-vanilla-bundle/package.json b/examples/webpack-demo-vanilla-bundle/package.json
index 581361130..cbd0fe412 100644
--- a/examples/webpack-demo-vanilla-bundle/package.json
+++ b/examples/webpack-demo-vanilla-bundle/package.json
@@ -38,20 +38,21 @@
"@types/node": "^14.14.35",
"@types/webpack": "^4.41.26",
"clean-webpack-plugin": "^3.0.0",
- "copy-webpack-plugin": "^8.0.0",
- "css-loader": "^5.1.3",
+ "copy-webpack-plugin": "^8.1.0",
+ "css-loader": "^5.2.0",
"file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "^6.2.0",
"html-loader": "^2.1.2",
"html-webpack-plugin": "5.3.1",
"mini-css-extract-plugin": "^1.3.9",
"node-sass": "5.0.0",
+ "rxjs": "^6.6.6",
"sass-loader": "^11.0.1",
"style-loader": "^2.0.0",
"ts-loader": "^8.0.18",
"ts-node": "^9.1.1",
"url-loader": "^4.1.1",
- "webpack": "^5.27.0",
+ "webpack": "^5.28.0",
"webpack-cli": "^4.5.0",
"webpack-dev-server": "^3.11.2"
}
diff --git a/examples/webpack-demo-vanilla-bundle/src/app-routing.ts b/examples/webpack-demo-vanilla-bundle/src/app-routing.ts
index 028778c2e..eac478d95 100644
--- a/examples/webpack-demo-vanilla-bundle/src/app-routing.ts
+++ b/examples/webpack-demo-vanilla-bundle/src/app-routing.ts
@@ -17,6 +17,7 @@ export class AppRouting {
{ route: 'example11', name: 'example11', title: 'Example11', moduleId: './examples/example11' },
{ route: 'example12', name: 'example12', title: 'Example12', moduleId: './examples/example12' },
{ route: 'example13', name: 'example13', title: 'Example13', moduleId: './examples/example13' },
+ { route: 'example15', name: 'example15', title: 'Example15', moduleId: './examples/example15' },
{ route: 'icons', name: 'icons', title: 'icons', moduleId: './examples/icons' },
{ route: '', redirect: 'example01' },
{ route: '**', redirect: 'example01' }
diff --git a/examples/webpack-demo-vanilla-bundle/src/app.html b/examples/webpack-demo-vanilla-bundle/src/app.html
index fe98b4e66..c9c368b13 100644
--- a/examples/webpack-demo-vanilla-bundle/src/app.html
+++ b/examples/webpack-demo-vanilla-bundle/src/app.html
@@ -72,6 +72,9 @@
Slickgrid-Universal
Example13 - Header Button Plugin
+
+ Example15 - OData Backend Service with RxJS
+
diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example09.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example09.ts
index 8158df7e3..81a5aca26 100644
--- a/examples/webpack-demo-vanilla-bundle/src/examples/example09.ts
+++ b/examples/webpack-demo-vanilla-bundle/src/examples/example09.ts
@@ -142,7 +142,7 @@ export class Example09 {
* This function is only here to mock a WebAPI call (since we are using a JSON file for the demo)
* in your case the getCustomer() should be a WebAPI function returning a Promise
*/
- getCustomerDataApiMock(query) {
+ getCustomerDataApiMock(query): Promise {
// the mock is returning a Promise, just like a WebAPI typically does
return new Promise((resolve) => {
const queryParams = query.toLowerCase().split('&');
diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example15.html b/examples/webpack-demo-vanilla-bundle/src/examples/example15.html
new file mode 100644
index 000000000..4e415b4c4
--- /dev/null
+++ b/examples/webpack-demo-vanilla-bundle/src/examples/example15.html
@@ -0,0 +1,78 @@
+
+ Example 15 - Grid with OData Backend Service using RxJS Observables
+
+
+
+
+
+
+ Clear all Filter & Sorts
+
+
+ Set Filters Dynamically
+
+
+ Set Sorting Dynamically
+
+
+ Add Other Gender via RxJS
+
+
+
+
+
+
+
+ Programmatically go to first/last page:
+
+ <
+
+
+ >
+
+
+
+
+ OData Version:
+
+
+ 2
+
+
+ 4
+
+
+
+
+
+ Enable Count (add to OData query)
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example15.scss b/examples/webpack-demo-vanilla-bundle/src/examples/example15.scss
new file mode 100644
index 000000000..05c404aa8
--- /dev/null
+++ b/examples/webpack-demo-vanilla-bundle/src/examples/example15.scss
@@ -0,0 +1 @@
+@import '@slickgrid-universal/common/dist/styles/sass/slickgrid-theme-salesforce.scss';
\ No newline at end of file
diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example15.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example15.ts
new file mode 100644
index 000000000..a300d2eeb
--- /dev/null
+++ b/examples/webpack-demo-vanilla-bundle/src/examples/example15.ts
@@ -0,0 +1,367 @@
+import { BindingEventService, Column, Editors, FieldType, Filters, GridOption, GridStateChange, Metrics, OperatorType, } from '@slickgrid-universal/common';
+import { GridOdataService, OdataServiceApi, OdataOption } from '@slickgrid-universal/odata';
+import { RxJsResource } from '@slickgrid-universal/rxjs-observable';
+import { Slicker, SlickVanillaGridBundle } from '@slickgrid-universal/vanilla-bundle';
+import { Observable, of, Subject } from 'rxjs';
+
+import { ExampleGridOptions } from './example-grid-options';
+import '../salesforce-styles.scss';
+import './example15.scss';
+
+const defaultPageSize = 20;
+
+export class Example15 {
+ private _bindingEventService: BindingEventService;
+ columnDefinitions: Column[];
+ gridOptions: GridOption;
+ metrics: Metrics;
+ sgb: SlickVanillaGridBundle;
+
+ isCountEnabled = true;
+ odataVersion = 2;
+ odataQuery = '';
+ processing = false;
+ status = '';
+ statusClass = 'is-success';
+ isOtherGenderAdded = false;
+ genderCollection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }];
+
+ constructor() {
+ this._bindingEventService = new BindingEventService();
+ }
+
+ attached() {
+ this.initializeGrid();
+ const gridContainerElm = document.querySelector(`.grid15`);
+
+ this._bindingEventService.bind(gridContainerElm, 'ongridstatechanged', this.gridStateChanged.bind(this));
+ // this._bindingEventService.bind(gridContainerElm, 'onbeforeexporttoexcel', () => console.log('onBeforeExportToExcel'));
+ // this._bindingEventService.bind(gridContainerElm, 'onafterexporttoexcel', () => console.log('onAfterExportToExcel'));
+ this.sgb = new Slicker.GridBundle(gridContainerElm, this.columnDefinitions, { ...ExampleGridOptions, ...this.gridOptions }, []);
+ }
+
+ dispose() {
+ if (this.sgb) {
+ this.sgb?.dispose();
+ }
+ this._bindingEventService.unbindAll();
+ }
+
+ initializeGrid() {
+ this.columnDefinitions = [
+ {
+ id: 'name', name: 'Name', field: 'name', sortable: true,
+ type: FieldType.string,
+ filterable: true,
+ filter: {
+ model: Filters.compoundInput
+ }
+ },
+ {
+ id: 'gender', name: 'Gender', field: 'gender', filterable: true,
+ editor: {
+ model: Editors.singleSelect,
+ // collection: this.genderCollection,
+ collectionAsync: of(this.genderCollection)
+ },
+ filter: {
+ model: Filters.singleSelect,
+ collectionAsync: of(this.genderCollection),
+ collectionOptions: {
+ addBlankEntry: true
+ }
+ }
+ },
+ { id: 'company', name: 'Company', field: 'company' },
+ ];
+
+ this.gridOptions = {
+ enableAutoResize: true,
+ autoResize: {
+ container: '.demo-container',
+ rightPadding: 10
+ },
+ checkboxSelector: {
+ // you can toggle these 2 properties to show the "select all" checkbox in different location
+ hideInFilterHeaderRow: false,
+ hideInColumnTitleRow: true
+ },
+ editable: true,
+ autoEdit: true,
+ autoCommitEdit: true,
+ rowHeight: 33,
+ headerRowHeight: 35,
+ enableCellNavigation: true,
+ enableFiltering: true,
+ enableCheckboxSelector: true,
+ enableRowSelection: true,
+ enablePagination: true, // you could optionally disable the Pagination
+ pagination: {
+ pageSizes: [10, 20, 50, 100, 500],
+ pageSize: defaultPageSize,
+ },
+ presets: {
+ // you can also type operator as string, e.g.: operator: 'EQ'
+ filters: [
+ // { columnId: 'name', searchTerms: ['w'], operator: OperatorType.startsWith },
+ { columnId: 'gender', searchTerms: ['male'], operator: OperatorType.equal },
+ ],
+ sorters: [
+ // direction can be written as 'asc' (uppercase or lowercase) and/or use the SortDirection type
+ { columnId: 'name', direction: 'asc' },
+ ],
+ pagination: { pageNumber: 2, pageSize: 20 }
+ },
+ backendServiceApi: {
+ service: new GridOdataService(),
+ options: {
+ enableCount: this.isCountEnabled, // add the count in the OData query, which will return a property named "odata.count" (v2) or "@odata.count" (v4)
+ version: this.odataVersion // defaults to 2, the query string is slightly different between OData 2 and 4
+ },
+ preProcess: () => this.displaySpinner(true),
+ process: (query) => this.getCustomerApiCall(query),
+ postProcess: (response) => {
+ this.metrics = response.metrics;
+ this.displaySpinner(false);
+ this.getCustomerCallback(response);
+ }
+ } as OdataServiceApi,
+ registerExternalResources: [new RxJsResource()]
+ };
+ }
+
+ addOtherGender() {
+ const newGender = { value: 'other', label: 'other' };
+ const genderColumn = this.columnDefinitions.find((column: Column) => column.id === 'gender');
+
+ if (genderColumn) {
+ let editorCollection = genderColumn.editor!.collection;
+ const filterCollectionAsync = genderColumn.filter!.collectionAsync as Subject;
+
+ if (Array.isArray(editorCollection)) {
+ // refresh the Editor "collection", we have 2 ways of doing it
+
+ // 1. simply Push to the Editor "collection"
+ // editorCollection.push(newGender);
+
+ // 2. or replace the entire "collection"
+ genderColumn.editor.collection = [...this.genderCollection, newGender];
+ editorCollection = genderColumn.editor.collection;
+
+ // However, for the Filter only, we have to trigger an RxJS/Subject change with the new collection
+ // we do this because Filter(s) are shown at all time, while on Editor it's unnecessary since they are only shown when opening them
+ if (filterCollectionAsync?.next) {
+ filterCollectionAsync.next(editorCollection);
+ }
+ }
+ }
+
+ // don't add it more than once
+ this.isOtherGenderAdded = true;
+ }
+
+ displaySpinner(isProcessing) {
+ this.processing = isProcessing;
+ this.status = (isProcessing) ? 'loading...' : 'finished!!';
+ this.statusClass = (isProcessing) ? 'notification is-light is-warning' : 'notification is-light is-success';
+ }
+
+ getCustomerCallback(data) {
+ // totalItems property needs to be filled for pagination to work correctly
+ // however we need to force Aurelia to do a dirty check, doing a clone object will do just that
+ let countPropName = 'totalRecordCount'; // you can use "totalRecordCount" or any name or "odata.count" when "enableCount" is set
+ if (this.isCountEnabled) {
+ countPropName = (this.odataVersion === 4) ? '@odata.count' : 'odata.count';
+ }
+ if (this.metrics) {
+ this.metrics.totalItemCount = data[countPropName];
+ }
+
+ // once pagination totalItems is filled, we can update the dataset
+ this.sgb.paginationOptions.totalItems = data[countPropName];
+ this.sgb.dataset = data['items'];
+ this.odataQuery = data['query'];
+ }
+
+ getCustomerApiCall(query): Observable {
+ // in your case, you will call your WebAPI function (wich needs to return an Observable)
+ // for the demo purpose, we will call a mock WebAPI function
+ return this.getCustomerDataApiMock(query);
+ }
+
+ /**
+ * This function is only here to mock a WebAPI call (since we are using a JSON file for the demo)
+ * in your case the getCustomer() should be a WebAPI function returning a Promise
+ */
+ getCustomerDataApiMock(query): Observable {
+ // the mock is returning an Observable
+ return new Observable((observer) => {
+ const queryParams = query.toLowerCase().split('&');
+ let top: number;
+ let skip = 0;
+ let orderBy = '';
+ let countTotalItems = 100;
+ const columnFilters = {};
+
+ for (const param of queryParams) {
+ if (param.includes('$top=')) {
+ top = +(param.substring('$top='.length));
+ }
+ if (param.includes('$skip=')) {
+ skip = +(param.substring('$skip='.length));
+ }
+ if (param.includes('$orderby=')) {
+ orderBy = param.substring('$orderby='.length);
+ }
+ if (param.includes('$filter=')) {
+ const filterBy = param.substring('$filter='.length).replace('%20', ' ');
+ if (filterBy.includes('contains')) {
+ const filterMatch = filterBy.match(/contains\(([a-zA-Z\/]+),\s?'(.*?)'/);
+ const fieldName = filterMatch[1].trim();
+ columnFilters[fieldName] = { type: 'substring', term: filterMatch[2].trim() };
+ }
+ if (filterBy.includes('substringof')) {
+ const filterMatch = filterBy.match(/substringof\('(.*?)',([a-zA-Z ]*)/);
+ const fieldName = filterMatch[2].trim();
+ columnFilters[fieldName] = { type: 'substring', term: filterMatch[1].trim() };
+ }
+ if (filterBy.includes('eq')) {
+ const filterMatch = filterBy.match(/([a-zA-Z ]*) eq '(.*?)'/);
+ if (Array.isArray(filterMatch)) {
+ const fieldName = filterMatch[1].trim();
+ columnFilters[fieldName] = { type: 'equal', term: filterMatch[2].trim() };
+ }
+ }
+ if (filterBy.includes('startswith')) {
+ const filterMatch = filterBy.match(/startswith\(([a-zA-Z ]*),\s?'(.*?)'/);
+ const fieldName = filterMatch[1].trim();
+ columnFilters[fieldName] = { type: 'starts', term: filterMatch[2].trim() };
+ }
+ if (filterBy.includes('endswith')) {
+ const filterMatch = filterBy.match(/endswith\(([a-zA-Z ]*),\s?'(.*?)'/);
+ const fieldName = filterMatch[1].trim();
+ columnFilters[fieldName] = { type: 'ends', term: filterMatch[2].trim() };
+ }
+ }
+ }
+
+ const sort = orderBy.includes('asc')
+ ? 'ASC'
+ : orderBy.includes('desc')
+ ? 'DESC'
+ : '';
+
+ let data;
+ switch (sort) {
+ case 'ASC':
+ data = require('./data/customers_100_ASC.json');
+ break;
+ case 'DESC':
+ data = require('./data/customers_100_DESC.json');
+ break;
+ default:
+ data = require('./data/customers_100.json');
+ break;
+ }
+
+ // Read the result field from the JSON response.
+ const firstRow = skip;
+ let filteredData = data;
+ if (columnFilters) {
+ for (const columnId in columnFilters) {
+ if (columnFilters.hasOwnProperty(columnId)) {
+ filteredData = filteredData.filter(column => {
+ const filterType = columnFilters[columnId].type;
+ const searchTerm = columnFilters[columnId].term;
+ let colId = columnId;
+ if (columnId && columnId.indexOf(' ') !== -1) {
+ const splitIds = columnId.split(' ');
+ colId = splitIds[splitIds.length - 1];
+ }
+ const filterTerm = column[colId];
+ if (filterTerm) {
+ switch (filterType) {
+ case 'equal': return filterTerm.toLowerCase() === searchTerm;
+ case 'ends': return filterTerm.toLowerCase().endsWith(searchTerm);
+ case 'starts': return filterTerm.toLowerCase().startsWith(searchTerm);
+ case 'substring': return filterTerm.toLowerCase().includes(searchTerm);
+ }
+ }
+ });
+ }
+ }
+ countTotalItems = filteredData.length;
+ }
+ const updatedData = filteredData.slice(firstRow, firstRow + top);
+
+ setTimeout(() => {
+ let countPropName = 'totalRecordCount';
+ if (this.isCountEnabled) {
+ countPropName = (this.odataVersion === 4) ? '@odata.count' : 'odata.count';
+ }
+ const backendResult = { items: updatedData, [countPropName]: countTotalItems, query };
+ // console.log('Backend Result', backendResult);
+ observer.next(backendResult);
+ observer.complete();
+ }, 150);
+ });
+ }
+
+ clearAllFiltersAndSorts() {
+ if (this.sgb?.gridService) {
+ this.sgb.gridService.clearAllFiltersAndSorts();
+ }
+ }
+
+ goToFirstPage() {
+ this.sgb?.paginationService?.goToFirstPage();
+ }
+
+ goToLastPage() {
+ this.sgb?.paginationService?.goToLastPage();
+ }
+
+ /** Dispatched event of a Grid State Changed event */
+ gridStateChanged(event) {
+ if (event?.detail) {
+ const gridStateChanges: GridStateChange = event.detail;
+ // console.log('Client sample, Grid State changed:: ', gridStateChanges);
+ console.log('Client sample, Grid State changed:: ', gridStateChanges.change);
+ }
+ }
+
+ setFiltersDynamically() {
+ // we can Set Filters Dynamically (or different filters) afterward through the FilterService
+ this.sgb?.filterService.updateFilters([
+ // { columnId: 'gender', searchTerms: ['male'], operator: OperatorType.equal },
+ { columnId: 'name', searchTerms: ['A'], operator: 'a*' },
+ ]);
+ }
+
+ setSortingDynamically() {
+ this.sgb?.sortService.updateSorting([
+ { columnId: 'name', direction: 'DESC' },
+ ]);
+ }
+
+ // THE FOLLOWING METHODS ARE ONLY FOR DEMO PURPOSES DO NOT USE THIS CODE
+ // ---
+
+ changeCountEnableFlag() {
+ this.isCountEnabled = !this.isCountEnabled;
+ const odataService = this.gridOptions.backendServiceApi.service;
+ odataService.updateOptions({ enableCount: this.isCountEnabled } as OdataOption);
+ odataService.clearFilters();
+ this.sgb?.filterService.clearFilters();
+ return true;
+ }
+
+ setOdataVersion(version: number) {
+ this.odataVersion = version;
+ const odataService = this.gridOptions.backendServiceApi.service;
+ odataService.updateOptions({ version: this.odataVersion } as OdataOption);
+ odataService.clearFilters();
+ this.sgb?.filterService.clearFilters();
+ return true;
+ }
+}
diff --git a/examples/webpack-demo-vanilla-bundle/webpack.config.js b/examples/webpack-demo-vanilla-bundle/webpack.config.js
index 0958c0532..ae36a9552 100644
--- a/examples/webpack-demo-vanilla-bundle/webpack.config.js
+++ b/examples/webpack-demo-vanilla-bundle/webpack.config.js
@@ -82,7 +82,8 @@ module.exports = ({ production } = {}) => ({
new CopyWebpackPlugin({
patterns: [
{ from: `${srcDir}/favicon.ico`, to: 'favicon.ico' },
- { from: 'assets', to: 'assets' }
+ { from: 'assets', to: 'assets' },
+ { from: 'src/examples/data', to: 'assets/data' }
]
}),
new MiniCssExtractPlugin({ // updated to match the naming conventions for the js files
diff --git a/package.json b/package.json
index 14972cd74..7c8cb6347 100644
--- a/package.json
+++ b/package.json
@@ -52,8 +52,8 @@
"devDependencies": {
"@types/jest": "^26.0.21",
"@types/node": "^14.14.35",
- "@typescript-eslint/eslint-plugin": "^4.18.0",
- "@typescript-eslint/parser": "^4.18.0",
+ "@typescript-eslint/eslint-plugin": "^4.19.0",
+ "@typescript-eslint/parser": "^4.19.0",
"cypress": "^6.8.0",
"eslint": "^7.22.0",
"eslint-plugin-import": "^2.22.1",
@@ -70,6 +70,7 @@
"mocha": "^8.3.2",
"mochawesome": "^6.2.2",
"npm-run-all": "^4.1.5",
+ "rxjs": "next",
"ts-jest": "^26.5.4",
"typescript": "^4.2.3"
},
diff --git a/packages/common/package.json b/packages/common/package.json
index cb90609f0..5ab4ca9b5 100644
--- a/packages/common/package.json
+++ b/packages/common/package.json
@@ -65,7 +65,7 @@
"dequal": "^2.0.2",
"dompurify": "^2.2.7",
"flatpickr": "^4.6.9",
- "jquery": "^3.5.1",
+ "jquery": "~3.5.1",
"jquery-ui-dist": "^1.12.1",
"moment-mini": "^2.24.0",
"multiple-select-modified": "^1.3.11",
diff --git a/packages/common/src/extensions/__tests__/gridMenuExtension.spec.ts b/packages/common/src/extensions/__tests__/gridMenuExtension.spec.ts
index 761b39a0c..f7341137f 100644
--- a/packages/common/src/extensions/__tests__/gridMenuExtension.spec.ts
+++ b/packages/common/src/extensions/__tests__/gridMenuExtension.spec.ts
@@ -3,7 +3,7 @@ import { Column, SlickDataView, GridOption, SlickGrid, SlickNamespace, GridMenu,
import { GridMenuExtension } from '../gridMenuExtension';
import { ExtensionUtility } from '../extensionUtility';
import { SharedService } from '../../services/shared.service';
-import { ExcelExportService, TextExportService, FilterService, SortService } from '../../services';
+import { ExcelExportService, TextExportService, FilterService, SortService, BackendUtilityService } from '../../services';
import { TranslateServiceStub } from '../../../../../test/translateServiceStub';
declare const Slick: SlickNamespace;
@@ -76,6 +76,7 @@ describe('gridMenuExtension', () => {
const columnsMock: Column[] = [{ id: 'field1', field: 'field1', width: 100, nameKey: 'TITLE' }, { id: 'field2', field: 'field2', width: 75 }];
let divElement: HTMLDivElement;
+ let backendUtilityService: BackendUtilityService;
let extensionUtility: ExtensionUtility;
let translateService: TranslateServiceStub;
let extension: GridMenuExtension;
@@ -122,10 +123,11 @@ describe('gridMenuExtension', () => {
div.innerHTML = template;
document.body.appendChild(div);
+ backendUtilityService = new BackendUtilityService();
sharedService = new SharedService();
translateService = new TranslateServiceStub();
extensionUtility = new ExtensionUtility(sharedService, translateService);
- extension = new GridMenuExtension(extensionUtility, filterServiceStub, sharedService, sortServiceStub, translateService);
+ extension = new GridMenuExtension(extensionUtility, filterServiceStub, sharedService, sortServiceStub, backendUtilityService, translateService);
translateService.use('fr');
});
@@ -809,7 +811,8 @@ describe('gridMenuExtension', () => {
describe('without Translate Service', () => {
beforeEach(() => {
translateService = undefined as any;
- extension = new GridMenuExtension({} as ExtensionUtility, filterServiceStub, { gridOptions: { enableTranslate: true } } as SharedService, {} as SortService, translateService);
+ backendUtilityService = new BackendUtilityService();
+ extension = new GridMenuExtension({} as ExtensionUtility, filterServiceStub, { gridOptions: { enableTranslate: true } } as SharedService, {} as SortService, backendUtilityService, translateService);
});
it('should throw an error if "enableTranslate" is set but the I18N Service is null', () => {
diff --git a/packages/common/src/extensions/gridMenuExtension.ts b/packages/common/src/extensions/gridMenuExtension.ts
index 1b3ceb2cc..638ffcf0a 100644
--- a/packages/common/src/extensions/gridMenuExtension.ts
+++ b/packages/common/src/extensions/gridMenuExtension.ts
@@ -19,7 +19,7 @@ import { FilterService } from '../services/filter.service';
import { SortService } from '../services/sort.service';
import { SharedService } from '../services/shared.service';
import { TranslaterService } from '../services/translater.service';
-import { refreshBackendDataset } from '../services/backend-utilities';
+import { BackendUtilityService } from '../services/backendUtility.service';
import { getTranslationPrefix } from '../services/utilities';
// using external js libraries
@@ -37,6 +37,7 @@ export class GridMenuExtension implements Extension {
private readonly filterService: FilterService,
private readonly sharedService: SharedService,
private readonly sortService: SortService,
+ private readonly backendUtilities?: BackendUtilityService,
private readonly translaterService?: TranslaterService,
) {
this._eventHandler = new Slick.EventHandler();
@@ -176,7 +177,7 @@ export class GridMenuExtension implements Extension {
if (gridOptions) {
this.sharedService.gridOptions = { ...this.sharedService.gridOptions, ...gridOptions };
}
- refreshBackendDataset(this.sharedService.gridOptions);
+ this.backendUtilities?.refreshBackendDataset(this.sharedService.gridOptions);
}
showGridMenu(e: SlickEventData) {
diff --git a/packages/common/src/filters/__tests__/autoCompleteFilter.spec.ts b/packages/common/src/filters/__tests__/autoCompleteFilter.spec.ts
index b2fc357a5..169ba4ba5 100644
--- a/packages/common/src/filters/__tests__/autoCompleteFilter.spec.ts
+++ b/packages/common/src/filters/__tests__/autoCompleteFilter.spec.ts
@@ -1,9 +1,12 @@
+import { of, Subject } from 'rxjs';
+
import { Filters } from '../index';
import { AutoCompleteFilter } from '../autoCompleteFilter';
import { FieldType, OperatorType, KeyCode } from '../../enums/index';
import { AutocompleteOption, Column, FilterArguments, GridOption, SlickGrid } from '../../interfaces/index';
import { CollectionService } from '../../services/collection.service';
import { HttpStub } from '../../../../../test/httpClientStub';
+import { RxJsResourceStub } from '../../../../../test/rxjsResourceStub';
import { TranslateServiceStub } from '../../../../../test/translateServiceStub';
jest.useFakeTimers();
@@ -652,7 +655,123 @@ describe('AutoCompleteFilter', () => {
const promise = Promise.resolve({ hello: 'world' });
mockColumn.filter!.collectionAsync = promise;
filter.init(filterArguments).catch((e) => {
- expect(e.toString()).toContain(`Something went wrong while trying to pull the collection from the "collectionAsync" call in the AutoComplete Filter, the collection is not a valid array.`);
+ expect(e.toString()).toContain(`Something went wrong while trying to pull the collection from the "collectionAsync" call in the Filter, the collection is not a valid array.`);
+ done();
+ });
+ });
+ });
+
+ describe('AutoCompleteFilter using RxJS Observables', () => {
+ let divContainer: HTMLDivElement;
+ let filter: AutoCompleteFilter;
+ let filterArguments: FilterArguments;
+ let spyGetHeaderRow;
+ let mockColumn: Column;
+ let collectionService: CollectionService;
+ let rxjs: RxJsResourceStub;
+ let translaterService: TranslateServiceStub;
+ const http = new HttpStub();
+
+ beforeEach(() => {
+ translaterService = new TranslateServiceStub();
+ collectionService = new CollectionService(translaterService);
+ rxjs = new RxJsResourceStub();
+
+ divContainer = document.createElement('div');
+ divContainer.innerHTML = template;
+ document.body.appendChild(divContainer);
+ spyGetHeaderRow = jest.spyOn(gridStub, 'getHeaderRowColumn').mockReturnValue(divContainer);
+
+ mockColumn = {
+ id: 'gender', field: 'gender', filterable: true,
+ filter: {
+ model: Filters.autoComplete,
+ }
+ };
+ filterArguments = {
+ grid: gridStub,
+ columnDef: mockColumn,
+ callback: jest.fn()
+ };
+
+ filter = new AutoCompleteFilter(translaterService, collectionService, rxjs);
+ });
+
+ afterEach(() => {
+ filter.destroy();
+ jest.clearAllMocks();
+ });
+
+ it('should create the filter with a default search term when using "collectionAsync" as an Observable', async () => {
+ const spyCallback = jest.spyOn(filterArguments, 'callback');
+ mockColumn.filter.collectionAsync = of(['male', 'female']);
+
+ filterArguments.searchTerms = ['female'];
+ await filter.init(filterArguments);
+
+ const filterElm = divContainer.querySelector('input.filter-gender');
+ const autocompleteUlElms = document.body.querySelectorAll('ul.ui-autocomplete');
+ filter.setValues('male');
+
+ filterElm.focus();
+ filterElm.dispatchEvent(new (window.window as any).Event('input', { keyCode: 97, bubbles: true, cancelable: true }));
+ const filterFilledElms = divContainer.querySelectorAll('input.filter-gender.filled');
+
+ expect(autocompleteUlElms.length).toBe(1);
+ expect(filterFilledElms.length).toBe(1);
+ expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['male'], shouldTriggerQuery: true });
+ });
+
+ it('should create the multi-select filter with a "collectionAsync" as an Observable and be able to call next on it', async () => {
+ const mockCollection = ['male', 'female'];
+ mockColumn.filter.collectionAsync = of(mockCollection);
+
+ filterArguments.searchTerms = ['female'];
+ await filter.init(filterArguments);
+
+ const filterElm = divContainer.querySelector('input.filter-gender');
+ filter.setValues('male');
+
+ filterElm.focus();
+ filterElm.dispatchEvent(new (window.window as any).Event('input', { keyCode: 97, bubbles: true, cancelable: true }));
+
+ // after await (or timeout delay) we'll get the Subject Observable
+ mockCollection.push('other');
+ (mockColumn.filter.collectionAsync as Subject).next(mockCollection);
+
+ const autocompleteUlElms = document.body.querySelectorAll('ul.ui-autocomplete');
+ const filterFilledElms = divContainer.querySelectorAll('input.filter-gender.filled');
+
+ expect(autocompleteUlElms.length).toBe(1);
+ expect(filterFilledElms.length).toBe(1);
+ });
+
+ it('should create the filter with a value/label pair collectionAsync that is inside an object when "collectionInsideObjectProperty" is defined with a dot notation', async () => {
+ mockColumn.filter = {
+ collectionAsync: of({ deep: { myCollection: [{ value: 'other', description: 'other' }, { value: 'male', description: 'male' }, { value: 'female', description: 'female' }] } }),
+ collectionOptions: {
+ collectionInsideObjectProperty: 'deep.myCollection'
+ },
+ customStructure: {
+ value: 'value',
+ label: 'description',
+ },
+ };
+
+ await filter.init(filterArguments);
+
+ const filterCollection = filter.collection;
+
+ expect(filterCollection.length).toBe(3);
+ expect(filterCollection[0]).toEqual({ value: 'other', description: 'other' });
+ expect(filterCollection[1]).toEqual({ value: 'male', description: 'male' });
+ expect(filterCollection[2]).toEqual({ value: 'female', description: 'female' });
+ });
+
+ it('should throw an error when "collectionAsync" Observable does not return a valid array', (done) => {
+ mockColumn.filter.collectionAsync = of({ hello: 'world' });
+ filter.init(filterArguments).catch((e) => {
+ expect(e.toString()).toContain(`Something went wrong while trying to pull the collection from the "collectionAsync" call in the Filter, the collection is not a valid array.`);
done();
});
});
diff --git a/packages/common/src/filters/__tests__/filterFactory.spec.ts b/packages/common/src/filters/__tests__/filterFactory.spec.ts
new file mode 100644
index 000000000..90d899638
--- /dev/null
+++ b/packages/common/src/filters/__tests__/filterFactory.spec.ts
@@ -0,0 +1,81 @@
+import { Column } from '../../interfaces/index';
+import { InputFilter } from '../inputFilter';
+import { FilterFactory } from '../filterFactory';
+import { SlickgridConfig } from '../../slickgrid-config';
+import { CollectionService } from '../../services/collection.service';
+import { RxJsResourceStub } from '../../../../../test/rxjsResourceStub';
+import { TranslateServiceStub } from '../../../../../test/translateServiceStub';
+import { AutoCompleteFilter } from '../autoCompleteFilter';
+
+const mockAutocompleteFilter = jest.fn().mockImplementation(() => ({
+ constructor: jest.fn(),
+ init: jest.fn(),
+ destroy: jest.fn(),
+} as unknown as AutoCompleteFilter));
+
+describe('Filter Factory', () => {
+ jest.mock('../autoCompleteFilter', () => mockAutocompleteFilter);
+ const Filters = {
+ input: InputFilter,
+ autoComplete: mockAutocompleteFilter
+ };
+ let factory: FilterFactory;
+ let collectionService: CollectionService;
+ let slickgridConfig: SlickgridConfig;
+ let rxjsResourceStub: RxJsResourceStub;
+ let translateService: TranslateServiceStub;
+
+ beforeEach(() => {
+ translateService = new TranslateServiceStub();
+ collectionService = new CollectionService(translateService);
+ rxjsResourceStub = new RxJsResourceStub();
+ slickgridConfig = new SlickgridConfig();
+ slickgridConfig.options.defaultFilter = Filters.input;
+ factory = new FilterFactory(slickgridConfig, translateService, collectionService);
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should create the factory', () => {
+ expect(factory).toBeTruthy();
+ });
+
+ it('should create default Filter when no argument provided', () => {
+ const newFilter = factory.createFilter();
+ expect(newFilter).toEqual(new Filters.input());
+ });
+
+ it('should create AutoComplete Filter when that is the Filter provided as a model', () => {
+ const mockColumn = { filter: { model: Filters.autoComplete } } as Column;
+ const filterSpy = jest.spyOn(mockAutocompleteFilter.prototype, 'constructor');
+
+ const newFilter = factory.createFilter(mockColumn.filter);
+
+ expect(newFilter).toBeTruthy();
+ expect(filterSpy).toHaveBeenCalledWith(translateService, collectionService, undefined);
+ });
+
+ it('should create AutoComplete Filter with RxJS when that is the Filter provided as a model', () => {
+ factory = new FilterFactory(slickgridConfig, translateService, collectionService, rxjsResourceStub);
+ const mockColumn = { filter: { model: Filters.autoComplete } } as Column;
+ const filterSpy = jest.spyOn(mockAutocompleteFilter.prototype, 'constructor');
+
+ const newFilter = factory.createFilter(mockColumn.filter);
+
+ expect(newFilter).toBeTruthy();
+ expect(filterSpy).toHaveBeenCalledWith(translateService, collectionService, rxjsResourceStub);
+ });
+
+ it('should create AutoComplete Filter with RxJS when that is the Filter provided as a model', () => {
+ const mockColumn = { filter: { model: Filters.autoComplete } } as Column;
+ const filterSpy = jest.spyOn(mockAutocompleteFilter.prototype, 'constructor');
+
+ factory.addRxJsResource(rxjsResourceStub);
+ const newFilter = factory.createFilter(mockColumn.filter);
+
+ expect(newFilter).toBeTruthy();
+ expect(filterSpy).toHaveBeenCalledWith(translateService, collectionService, rxjsResourceStub);
+ });
+});
\ No newline at end of file
diff --git a/packages/common/src/filters/__tests__/selectFilter.spec.ts b/packages/common/src/filters/__tests__/selectFilter.spec.ts
index 7e4fcd24e..23a342fac 100644
--- a/packages/common/src/filters/__tests__/selectFilter.spec.ts
+++ b/packages/common/src/filters/__tests__/selectFilter.spec.ts
@@ -1,5 +1,6 @@
// import 3rd party lib multiple-select for the tests
import 'multiple-select-modified';
+import { of, Subject } from 'rxjs';
import { FieldType, OperatorType } from '../../enums/index';
import { Column, FilterArguments, GridOption, SlickGrid } from '../../interfaces/index';
@@ -7,6 +8,7 @@ import { CollectionService } from '../../services/collection.service';
import { Filters } from '..';
import { SelectFilter } from '../selectFilter';
import { HttpStub } from '../../../../../test/httpClientStub';
+import { RxJsResourceStub } from '../../../../../test/rxjsResourceStub';
import { TranslateServiceStub } from '../../../../../test/translateServiceStub';
jest.useFakeTimers();
@@ -794,7 +796,7 @@ describe('SelectFilter', () => {
try {
await filter.init(filterArguments);
} catch (e) {
- expect(e.toString()).toContain(`Something went wrong while trying to pull the collection from the "collectionAsync" call in the Select Filter, the collection is not a valid array.`);
+ expect(e.toString()).toContain(`Something went wrong while trying to pull the collection from the "collectionAsync" call in the Filter, the collection is not a valid array.`);
done();
}
});
@@ -803,8 +805,158 @@ describe('SelectFilter', () => {
const promise = Promise.resolve({ hello: 'world' });
mockColumn.filter!.collectionAsync = promise;
filter.init(filterArguments).catch((e) => {
- expect(e.toString()).toContain(`Something went wrong while trying to pull the collection from the "collectionAsync" call in the Select Filter, the collection is not a valid array.`);
+ expect(e.toString()).toContain(`Something went wrong while trying to pull the collection from the "collectionAsync" call in the Filter, the collection is not a valid array.`);
done();
});
});
+
+ describe('SelectFilter using RxJS Observables', () => {
+ let divContainer: HTMLDivElement;
+ let filter: SelectFilter;
+ let filterArguments: FilterArguments;
+ let spyGetHeaderRow;
+ let mockColumn: Column;
+ let collectionService: CollectionService;
+ let rxjs: RxJsResourceStub;
+ let translateService: TranslateServiceStub;
+ const http = new HttpStub();
+
+ beforeEach(() => {
+ translateService = new TranslateServiceStub();
+ collectionService = new CollectionService(translateService);
+ rxjs = new RxJsResourceStub();
+
+ divContainer = document.createElement('div');
+ divContainer.innerHTML = template;
+ document.body.appendChild(divContainer);
+ spyGetHeaderRow = jest.spyOn(gridStub, 'getHeaderRowColumn').mockReturnValue(divContainer);
+
+ mockColumn = {
+ id: 'gender', field: 'gender', filterable: true,
+ filter: {
+ model: Filters.multipleSelect,
+ }
+ };
+
+ filterArguments = {
+ grid: gridStub,
+ columnDef: mockColumn,
+ callback: jest.fn()
+ };
+
+ filter = new SelectFilter(translateService, collectionService, rxjs);
+ });
+
+ afterEach(() => {
+ filter.destroy();
+ jest.clearAllMocks();
+ });
+
+ it('should create the multi-select filter with a value/label pair collectionAsync that is inside an object when "collectionInsideObjectProperty" is defined with a dot notation', async () => {
+ mockColumn.filter = {
+ collectionAsync: of({ deep: { myCollection: [{ value: 'other', description: 'other' }, { value: 'male', description: 'male' }, { value: 'female', description: 'female' }] } }),
+ collectionOptions: {
+ collectionInsideObjectProperty: 'deep.myCollection'
+ },
+ customStructure: {
+ value: 'value',
+ label: 'description',
+ },
+ };
+
+ await filter.init(filterArguments);
+
+ const filterBtnElm = divContainer.querySelector('.ms-parent.ms-filter.search-filter.filter-gender button.ms-choice') as HTMLButtonElement;
+ const filterListElm = divContainer.querySelectorAll(`[name=filter-gender].ms-drop ul>li span`);
+ filterBtnElm.click();
+
+ expect(filterListElm.length).toBe(3);
+ expect(filterListElm[0].textContent).toBe('other');
+ expect(filterListElm[1].textContent).toBe('male');
+ expect(filterListElm[2].textContent).toBe('female');
+ });
+
+ it('should create the multi-select filter with a default search term when using "collectionAsync" as an Observable', async () => {
+ const spyCallback = jest.spyOn(filterArguments, 'callback');
+ const mockCollection = ['male', 'female'];
+ mockColumn.filter!.collection = undefined;
+ mockColumn.filter.collectionAsync = of(mockCollection);
+
+ filterArguments.searchTerms = ['female'];
+ await filter.init(filterArguments);
+
+ const filterBtnElm = divContainer.querySelector('.ms-parent.ms-filter.search-filter.filter-gender button.ms-choice') as HTMLButtonElement;
+ const filterListElm = divContainer.querySelectorAll(`[name=filter-gender].ms-drop ul>li input[type=checkbox]`);
+ const filterFilledElms = divContainer.querySelectorAll('.ms-parent.ms-filter.search-filter.filter-gender.filled');
+ const filterOkElm = divContainer.querySelector(`[name=filter-gender].ms-drop .ms-ok-button`) as HTMLButtonElement;
+ filterBtnElm.click();
+ filterOkElm.click();
+
+ expect(filterListElm.length).toBe(2);
+ expect(filterFilledElms.length).toBe(1);
+ expect(filterListElm[1].checked).toBe(true);
+ expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, operator: 'IN', searchTerms: ['female'], shouldTriggerQuery: true });
+ });
+
+ it('should create the multi-select filter with a "collectionAsync" as an Observable and be able to call next on it', async () => {
+ const mockCollection = ['male', 'female'];
+ mockColumn.filter.collectionAsync = of(mockCollection);
+
+ filterArguments.searchTerms = ['female'];
+ await filter.init(filterArguments);
+
+ const filterBtnElm = divContainer.querySelector('.ms-parent.ms-filter.search-filter.filter-gender button.ms-choice');
+ const filterListElm = divContainer.querySelectorAll(`[name=filter-gender].ms-drop ul>li input[type=checkbox]`);
+ filterBtnElm.click();
+
+ expect(filterListElm.length).toBe(2);
+ expect(filterListElm[1].checked).toBe(true);
+
+ // after await (or timeout delay) we'll get the Subject Observable
+ mockCollection.push('other');
+ (mockColumn.filter.collectionAsync as Subject).next(mockCollection);
+
+ const filterUpdatedListElm = divContainer.querySelectorAll(`[name=filter-gender].ms-drop ul>li input[type=checkbox]`);
+ expect(filterUpdatedListElm.length).toBe(3);
+ });
+
+ it('should create the multi-select filter with a "collectionAsync" as an Observable, which has its collection inside an object property, and be able to call next on it', async () => {
+ const mockCollection = { deep: { myCollection: ['male', 'female'] } };
+ mockColumn.filter = {
+ collectionAsync: of(mockCollection),
+ collectionOptions: {
+ collectionInsideObjectProperty: 'deep.myCollection'
+ },
+ customStructure: {
+ value: 'value',
+ label: 'description',
+ },
+ };
+
+ filterArguments.searchTerms = ['female'];
+ await filter.init(filterArguments);
+
+ const filterBtnElm = divContainer.querySelector('.ms-parent.ms-filter.search-filter.filter-gender button.ms-choice');
+ const filterListElm = divContainer.querySelectorAll(`[name=filter-gender].ms-drop ul>li input[type=checkbox]`);
+ filterBtnElm.click();
+
+ expect(filterListElm.length).toBe(2);
+ expect(filterListElm[1].checked).toBe(true);
+
+ // after await (or timeout delay) we'll get the Subject Observable
+ mockCollection.deep.myCollection.push('other');
+ (mockColumn.filter.collectionAsync as Subject).next(mockCollection.deep.myCollection);
+
+ const filterUpdatedListElm = divContainer.querySelectorAll(`[name=filter-gender].ms-drop ul>li input[type=checkbox]`);
+ expect(filterUpdatedListElm.length).toBe(3);
+ });
+
+ it('should throw an error when "collectionAsync" Observable does not return a valid array', (done) => {
+ mockColumn.filter.collectionAsync = of({ hello: 'world' });
+ filter.init(filterArguments).catch((e) => {
+ expect(e.toString()).toContain(`Something went wrong while trying to pull the collection from the "collectionAsync" call in the Filter, the collection is not a valid array.`);
+ done();
+ });
+ });
+ });
});
diff --git a/packages/common/src/filters/autoCompleteFilter.ts b/packages/common/src/filters/autoCompleteFilter.ts
index ca31d1a5f..e5cf00eee 100644
--- a/packages/common/src/filters/autoCompleteFilter.ts
+++ b/packages/common/src/filters/autoCompleteFilter.ts
@@ -20,8 +20,10 @@ import {
} from './../interfaces/index';
import { CollectionService } from '../services/collection.service';
import { collectionObserver, propertyObserver } from '../services/observers';
-import { getDescendantProperty, sanitizeTextByAvailableSanitizer, toKebabCase } from '../services/utilities';
+import { getDescendantProperty, sanitizeTextByAvailableSanitizer, toKebabCase, unsubscribeAll } from '../services/utilities';
import { TranslaterService } from '../services/translater.service';
+import { renderCollectionOptionsAsync } from './filterUtilities';
+import { RxJsFacade, Subscription } from '../services/rxjsFacade';
export class AutoCompleteFilter implements Filter {
protected _autoCompleteOptions!: AutocompleteOption;
@@ -57,11 +59,16 @@ export class AutoCompleteFilter implements Filter {
valueName = 'label';
enableTranslateLabel = false;
+ subscriptions: Subscription[] = [];
/**
* Initialize the Filter
*/
- constructor(protected readonly translaterService: TranslaterService, protected readonly collectionService: CollectionService) { }
+ constructor(
+ protected readonly translaterService: TranslaterService,
+ protected readonly collectionService: CollectionService,
+ protected readonly rxjs?: RxJsFacade
+ ) { }
/** Getter for the Autocomplete Option */
get autoCompleteOptions(): Partial {
@@ -167,7 +174,7 @@ export class AutoCompleteFilter implements Filter {
if (collectionAsync && !this.columnFilter.collection) {
// only read the collectionAsync once (on the 1st load),
// we do this because Http Fetch will throw an error saying body was already read and is streaming is locked
- collectionOutput = this.renderOptionsAsync(collectionAsync);
+ collectionOutput = renderCollectionOptionsAsync(collectionAsync, this.columnDef, this.renderDomElement.bind(this), this.rxjs, this.subscriptions);
resolve(collectionOutput);
} else {
collectionOutput = newCollection;
@@ -209,6 +216,9 @@ export class AutoCompleteFilter implements Filter {
}
this.$filterElm = null;
this._collection = undefined;
+
+ // unsubscribe all the possible Observables if RxJS was used
+ unsubscribeAll(this.subscriptions);
}
/** Set value(s) on the DOM element */
@@ -501,40 +511,4 @@ export class AutoCompleteFilter implements Filter {
.append($liDiv)
.appendTo(ul);
}
-
- protected async renderOptionsAsync(collectionAsync: Promise): Promise {
- let awaitedCollection: any = null;
-
- if (collectionAsync) {
- // wait for the "collectionAsync", once resolved we will save it into the "collection"
- const response: any | any[] = await collectionAsync;
-
- if (Array.isArray(response)) {
- awaitedCollection = response; // from Promise
- } else if (response instanceof Response && typeof response['json'] === 'function') {
- awaitedCollection = await response['json'](); // from Fetch
- } else if (response && response['content']) {
- awaitedCollection = response['content']; // from http-client
- }
-
- if (!Array.isArray(awaitedCollection) && this.collectionOptions?.collectionInsideObjectProperty) {
- const collection = awaitedCollection || response;
- const collectionInsideObjectProperty = this.collectionOptions.collectionInsideObjectProperty;
- awaitedCollection = getDescendantProperty(collection, collectionInsideObjectProperty || '');
- }
-
- if (!Array.isArray(awaitedCollection)) {
- throw new Error('Something went wrong while trying to pull the collection from the "collectionAsync" call in the AutoComplete Filter, the collection is not a valid array.');
- }
-
- // copy over the array received from the async call to the "collection" as the new collection to use
- // this has to be BEFORE the `collectionObserver().subscribe` to avoid going into an infinite loop
- this.columnFilter.collection = awaitedCollection;
-
- // recreate Multiple Select after getting async collection
- this.renderDomElement(awaitedCollection);
- }
-
- return awaitedCollection;
- }
-}
+}
\ No newline at end of file
diff --git a/packages/common/src/filters/filterFactory.ts b/packages/common/src/filters/filterFactory.ts
index 3f528e7eb..a12e2dd15 100644
--- a/packages/common/src/filters/filterFactory.ts
+++ b/packages/common/src/filters/filterFactory.ts
@@ -2,26 +2,31 @@ import { ColumnFilter, Filter } from '../interfaces/index';
import { SlickgridConfig } from '../slickgrid-config';
import { CollectionService } from '../services/collection.service';
import { TranslaterService } from '../services/translater.service';
+import { RxJsFacade } from '../services/rxjsFacade';
export class FilterFactory {
/** The options from the SlickgridConfig */
private _options: any;
- constructor(private config: SlickgridConfig, private readonly translaterService?: TranslaterService, private readonly collectionService?: CollectionService) {
+ constructor(private config: SlickgridConfig, private readonly translaterService?: TranslaterService, private readonly collectionService?: CollectionService, private rxjs?: RxJsFacade) {
this._options = this.config?.options ?? {};
}
+ addRxJsResource(rxjs: RxJsFacade) {
+ this.rxjs = rxjs;
+ }
+
// Uses the User model to create a new User
- createFilter(columnFilter: ColumnFilter | undefined): Filter | undefined {
+ createFilter(columnFilter?: ColumnFilter): Filter | undefined {
let filter: Filter | undefined;
if (columnFilter?.model) {
- filter = typeof columnFilter.model === 'function' ? new columnFilter.model(this.translaterService, this.collectionService) : columnFilter.model;
+ filter = typeof columnFilter.model === 'function' ? new columnFilter.model(this.translaterService, this.collectionService, this.rxjs) : columnFilter.model;
}
// fallback to the default filter
if (!filter && this._options.defaultFilter) {
- filter = new this._options.defaultFilter(this.translaterService, this.collectionService);
+ filter = new this._options.defaultFilter(this.translaterService, this.collectionService, this.rxjs);
}
return filter;
diff --git a/packages/common/src/filters/filterUtilities.ts b/packages/common/src/filters/filterUtilities.ts
index 252cd0d9d..f500b8c30 100644
--- a/packages/common/src/filters/filterUtilities.ts
+++ b/packages/common/src/filters/filterUtilities.ts
@@ -1,5 +1,7 @@
import { OperatorString } from '../enums/operatorString.type';
-import { htmlEncodedStringWithPadding } from '../services/utilities';
+import { Column } from '../interfaces/index';
+import { Observable, RxJsFacade, Subject, Subscription } from '../services/rxjsFacade';
+import { castObservableToPromise, getDescendantProperty, htmlEncodedStringWithPadding } from '../services/utilities';
export function buildSelectOperatorHtmlString(optionValues: Array<{ operator: OperatorString, description: string }>) {
let optionValueString = '';
@@ -9,3 +11,91 @@ export function buildSelectOperatorHtmlString(optionValues: Array<{ operator: Op
return `${optionValueString} `;
}
+
+/**
+ * When user use a CollectionAsync we will use the returned collection to render the filter DOM element
+ * and reinitialize filter collection with this new collection
+ */
+export function renderDomElementFromCollectionAsync(collection: any[], columnDef: Column, renderDomElementCallback: (collection: any) => void) {
+ const columnFilter = columnDef?.filter ?? {};
+ const collectionOptions = columnFilter?.collectionOptions ?? {};
+
+ if (collectionOptions && collectionOptions.collectionInsideObjectProperty) {
+ const collectionInsideObjectProperty = collectionOptions.collectionInsideObjectProperty;
+ collection = getDescendantProperty(collection, collectionInsideObjectProperty as string);
+ }
+ if (!Array.isArray(collection)) {
+ throw new Error(`Something went wrong while trying to pull the collection from the "collectionAsync" call in the Filter, the collection is not a valid array.`);
+ }
+
+ // copy over the array received from the async call to the "collection" as the new collection to use
+ // this has to be BEFORE the `collectionObserver().subscribe` to avoid going into an infinite loop
+ columnFilter.collection = collection;
+
+ // recreate Multiple Select after getting async collection
+ renderDomElementCallback(collection);
+}
+
+export async function renderCollectionOptionsAsync(collectionAsync: Promise | Observable | Subject, columnDef: Column, renderDomElementCallback: (collection: any) => void, rxjs?: RxJsFacade, subscriptions?: Subscription[]): Promise {
+ const columnFilter = columnDef?.filter ?? {};
+ const collectionOptions = columnFilter?.collectionOptions ?? {};
+
+ let awaitedCollection: any = null;
+
+ if (collectionAsync) {
+ const isObservable = rxjs?.isObservable(collectionAsync) ?? false;
+ if (isObservable && rxjs) {
+ awaitedCollection = await castObservableToPromise(rxjs, collectionAsync) as Promise;
+ }
+
+ // wait for the "collectionAsync", once resolved we will save it into the "collection"
+ const response: any | any[] = await collectionAsync;
+
+ if (Array.isArray(response)) {
+ awaitedCollection = response; // from Promise
+ } else if (response instanceof Response && typeof response['json'] === 'function') {
+ awaitedCollection = await response['json'](); // from Fetch
+ } else if (response && response['content']) {
+ awaitedCollection = response['content']; // from http-client
+ }
+
+ if (!Array.isArray(awaitedCollection) && collectionOptions?.collectionInsideObjectProperty) {
+ const collection = awaitedCollection || response;
+ const collectionInsideObjectProperty = collectionOptions.collectionInsideObjectProperty;
+ awaitedCollection = getDescendantProperty(collection, collectionInsideObjectProperty || '');
+ }
+
+ if (!Array.isArray(awaitedCollection)) {
+ throw new Error('Something went wrong while trying to pull the collection from the "collectionAsync" call in the Filter, the collection is not a valid array.');
+ }
+
+ // copy over the array received from the async call to the "collection" as the new collection to use
+ // this has to be BEFORE the `collectionObserver().subscribe` to avoid going into an infinite loop
+ columnFilter.collection = awaitedCollection;
+
+ // recreate Multiple Select after getting async collection
+ renderDomElementCallback(awaitedCollection);
+
+ // because we accept Promises & HttpClient Observable only execute once
+ // we will re-create an RxJs Subject which will replace the "collectionAsync" which got executed once anyway
+ // doing this provide the user a way to call a "collectionAsync.next()"
+ if (isObservable) {
+ createCollectionAsyncSubject(columnDef, renderDomElementCallback, rxjs, subscriptions);
+ }
+ }
+
+ return awaitedCollection;
+}
+
+/** Create or recreate an Observable Subject and reassign it to the "collectionAsync" object so user can call a "collectionAsync.next()" on it */
+export function createCollectionAsyncSubject(columnDef: Column, renderDomElementCallback: (collection: any) => void, rxjs?: RxJsFacade, subscriptions?: Subscription[]) {
+ const columnFilter = columnDef?.filter ?? {};
+ const newCollectionAsync = rxjs?.createSubject();
+ columnFilter.collectionAsync = newCollectionAsync;
+ if (subscriptions && newCollectionAsync) {
+ subscriptions.push(
+ newCollectionAsync.subscribe(collection => renderDomElementFromCollectionAsync(collection, columnDef, renderDomElementCallback))
+ );
+ }
+}
+
diff --git a/packages/common/src/filters/multipleSelectFilter.ts b/packages/common/src/filters/multipleSelectFilter.ts
index d13e9ed35..ab5dac0ef 100644
--- a/packages/common/src/filters/multipleSelectFilter.ts
+++ b/packages/common/src/filters/multipleSelectFilter.ts
@@ -1,12 +1,13 @@
import { SelectFilter } from './selectFilter';
import { CollectionService } from './../services/collection.service';
import { TranslaterService } from '../services/translater.service';
+import { RxJsFacade } from '../services/rxjsFacade';
export class MultipleSelectFilter extends SelectFilter {
/**
* Initialize the Filter
*/
- constructor(protected readonly translaterService: TranslaterService, protected readonly collectionService: CollectionService) {
- super(translaterService, collectionService, true);
+ constructor(protected readonly translaterService: TranslaterService, protected readonly collectionService: CollectionService, protected readonly rxjs: RxJsFacade) {
+ super(translaterService, collectionService, rxjs, true);
}
}
diff --git a/packages/common/src/filters/selectFilter.ts b/packages/common/src/filters/selectFilter.ts
index b2ed7e21c..da52602f0 100644
--- a/packages/common/src/filters/selectFilter.ts
+++ b/packages/common/src/filters/selectFilter.ts
@@ -12,12 +12,13 @@ import {
Locale,
MultipleSelectOption,
SelectOption,
- SlickGrid
+ SlickGrid,
} from './../interfaces/index';
import { CollectionService } from '../services/collection.service';
import { collectionObserver, propertyObserver } from '../services/observers';
-import { getDescendantProperty, getTranslationPrefix, htmlEncode, sanitizeTextByAvailableSanitizer } from '../services/utilities';
-import { TranslaterService } from '../services';
+import { getDescendantProperty, getTranslationPrefix, htmlEncode, sanitizeTextByAvailableSanitizer, unsubscribeAll } from '../services/utilities';
+import { RxJsFacade, Subscription, TranslaterService } from '../services';
+import { renderCollectionOptionsAsync } from './filterUtilities';
export class SelectFilter implements Filter {
protected _isMultipleSelect = true;
@@ -46,11 +47,16 @@ export class SelectFilter implements Filter {
optionLabel!: string;
valueName!: string;
enableTranslateLabel = false;
+ subscriptions: Subscription[] = [];
/**
* Initialize the Filter
*/
- constructor(protected readonly translaterService: TranslaterService, protected readonly collectionService: CollectionService, isMultipleSelect = true) {
+ constructor(
+ protected readonly translaterService: TranslaterService,
+ protected readonly collectionService: CollectionService,
+ protected readonly rxjs?: RxJsFacade,
+ isMultipleSelect = true) {
this._isMultipleSelect = isMultipleSelect;
}
@@ -150,7 +156,7 @@ export class SelectFilter implements Filter {
if (collectionAsync && !this.columnFilter.collection) {
// only read the collectionAsync once (on the 1st load),
// we do this because Http Fetch will throw an error saying body was already read and its streaming is locked
- collectionOutput = this.renderOptionsAsync(collectionAsync);
+ collectionOutput = renderCollectionOptionsAsync(collectionAsync, this.columnDef, this.renderDomElement.bind(this), this.rxjs, this.subscriptions);
resolve(collectionOutput);
} else {
collectionOutput = newCollection;
@@ -195,6 +201,9 @@ export class SelectFilter implements Filter {
$(`[name=${elementClassName}].ms-drop`).remove();
}
this.$filterElm = null;
+
+ // unsubscribe all the possible Observables if RxJS was used
+ unsubscribeAll(this.subscriptions);
}
/**
@@ -522,40 +531,4 @@ export class SelectFilter implements Filter {
this._shouldTriggerQuery = true;
}
}
-
- protected async renderOptionsAsync(collectionAsync: Promise): Promise {
- let awaitedCollection: any = null;
-
- if (collectionAsync) {
- // wait for the "collectionAsync", once resolved we will save it into the "collection"
- const response: any | any[] = await collectionAsync;
-
- if (Array.isArray(response)) {
- awaitedCollection = response; // from Promise
- } else if (response instanceof Response && typeof response['json'] === 'function') {
- awaitedCollection = await response['json'](); // from Fetch
- } else if (response && response['content']) {
- awaitedCollection = response['content']; // from http-client
- }
-
- if (!Array.isArray(awaitedCollection) && this.collectionOptions?.collectionInsideObjectProperty) {
- const collection = awaitedCollection || response;
- const collectionInsideObjectProperty = this.collectionOptions.collectionInsideObjectProperty;
- awaitedCollection = getDescendantProperty(collection, collectionInsideObjectProperty || '');
- }
-
- if (!Array.isArray(awaitedCollection)) {
- throw new Error('Something went wrong while trying to pull the collection from the "collectionAsync" call in the Select Filter, the collection is not a valid array.');
- }
-
- // copy over the array received from the async call to the "collection" as the new collection to use
- // this has to be BEFORE the `collectionObserver().subscribe` to avoid going into an infinite loop
- this.columnFilter.collection = awaitedCollection;
-
- // recreate Multiple Select after getting async collection
- this.renderDomElement(awaitedCollection);
- }
-
- return awaitedCollection;
- }
}
diff --git a/packages/common/src/filters/singleSelectFilter.ts b/packages/common/src/filters/singleSelectFilter.ts
index 1366b476c..7f00f27cd 100644
--- a/packages/common/src/filters/singleSelectFilter.ts
+++ b/packages/common/src/filters/singleSelectFilter.ts
@@ -1,12 +1,13 @@
import { SelectFilter } from './selectFilter';
import { CollectionService } from './../services/collection.service';
import { TranslaterService } from '../services/translater.service';
+import { RxJsFacade } from '../services/rxjsFacade';
export class SingleSelectFilter extends SelectFilter {
/**
* Initialize the Filter
*/
- constructor(protected readonly translaterService: TranslaterService, protected readonly collectionService: CollectionService) {
- super(translaterService, collectionService, false);
+ constructor(protected readonly translaterService: TranslaterService, protected readonly collectionService: CollectionService, protected readonly rxjs: RxJsFacade) {
+ super(translaterService, collectionService, rxjs, false);
}
}
diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts
index 20d014e63..364c78334 100644
--- a/packages/common/src/index.ts
+++ b/packages/common/src/index.ts
@@ -1,6 +1,6 @@
import 'multiple-select-modified';
-import * as BackendUtilities from './services/backend-utilities';
+import * as BackendUtilities from './services/backendUtility.service';
import * as Observers from './services/observers';
import * as ServiceUtilities from './services/utilities';
import * as SortUtilities from './sortComparers/sortUtilities';
diff --git a/packages/common/src/interfaces/backendServiceApi.interface.ts b/packages/common/src/interfaces/backendServiceApi.interface.ts
index 50e107c46..0921857e1 100644
--- a/packages/common/src/interfaces/backendServiceApi.interface.ts
+++ b/packages/common/src/interfaces/backendServiceApi.interface.ts
@@ -1,3 +1,4 @@
+import { Observable } from '../services/rxjsFacade';
import { BackendService } from './backendService.interface';
export interface BackendServiceApi {
@@ -34,13 +35,13 @@ export interface BackendServiceApi {
onError?: (e: any) => void;
/** On init (or on page load), what action to perform? */
- onInit?: (query: string) => Promise;
+ onInit?: (query: string) => Promise | Observable;
/** Before executing the query, what action to perform? For example, start a spinner */
preProcess?: () => void;
- /** On Processing, we get the query back from the service, and we need to provide a Promise. For example: this.http.get(myGraphqlUrl) */
- process: (query: string) => Promise;
+ /** On Processing, we get the query back from the service, and we need to provide a Promise/Observable. For example: this.http.get(myGraphqlUrl) */
+ process: (query: string) => Promise | Observable;
/** After executing the query, what action to perform? For example, stop the spinner */
postProcess?: (response: any) => void;
diff --git a/packages/common/src/interfaces/columnEditor.interface.ts b/packages/common/src/interfaces/columnEditor.interface.ts
index d5641352d..64fdda433 100644
--- a/packages/common/src/interfaces/columnEditor.interface.ts
+++ b/packages/common/src/interfaces/columnEditor.interface.ts
@@ -1,4 +1,5 @@
import { FieldType } from '../enums/index';
+import { Observable } from '../services/rxjsFacade';
import {
CollectionCustomStructure,
CollectionFilterBy,
@@ -26,7 +27,7 @@ export interface ColumnEditor {
callbacks?: any;
/** A collection of items/options that will be loaded asynchronously (commonly used with AutoComplete & Single/Multi-Select Editors) */
- collectionAsync?: Promise;
+ collectionAsync?: Promise | Observable;
/**
* A collection of items/options (commonly used with AutoComplete & Single/Multi-Select Editors)
diff --git a/packages/common/src/interfaces/columnFilter.interface.ts b/packages/common/src/interfaces/columnFilter.interface.ts
index c8077a4da..20d85bc85 100644
--- a/packages/common/src/interfaces/columnFilter.interface.ts
+++ b/packages/common/src/interfaces/columnFilter.interface.ts
@@ -9,6 +9,7 @@ import {
MultipleSelectOption,
} from './index';
import { SearchTerm } from '../enums/searchTerm.type';
+import { Observable, Subject } from '../services/rxjsFacade';
export interface ColumnFilter {
/** Do we want to bypass the Backend Query? Commonly used with an OData Backend Service, if we want to filter without calling the regular OData query. */
@@ -48,7 +49,7 @@ export interface ColumnFilter {
model?: any;
/** A collection of items/options that will be loaded asynchronously (commonly used with a Select/Multi-Select Filter) */
- collectionAsync?: Promise;
+ collectionAsync?: Promise | Observable | Subject;
/**
* A collection of items/options (commonly used with a Select/Multi-Select Filter)
diff --git a/packages/common/src/interfaces/externalResource.interface.ts b/packages/common/src/interfaces/externalResource.interface.ts
index 533cadc78..e4406af72 100644
--- a/packages/common/src/interfaces/externalResource.interface.ts
+++ b/packages/common/src/interfaces/externalResource.interface.ts
@@ -2,8 +2,11 @@ import { SlickGrid } from './slickGrid.interface';
import { ContainerService } from '../services';
export interface ExternalResource {
+ /** optionally provide the Service class name of the resource to make it easier to find even with minified code */
+ className?: string;
+
/** Initialize the External Resource (Component or Service) */
- init: (grid: SlickGrid, container: ContainerService) => void;
+ init?: (grid: SlickGrid, container: ContainerService) => void;
/** Dispose method */
dispose?: () => void;
diff --git a/packages/common/src/interfaces/rowDetailViewOption.interface.ts b/packages/common/src/interfaces/rowDetailViewOption.interface.ts
index 8acd250b3..e52503fdb 100644
--- a/packages/common/src/interfaces/rowDetailViewOption.interface.ts
+++ b/packages/common/src/interfaces/rowDetailViewOption.interface.ts
@@ -1,3 +1,4 @@
+import { Observable, Subject } from '../services/rxjsFacade';
import { SlickGrid } from './slickGrid.interface';
export interface RowDetailViewOption {
@@ -72,7 +73,7 @@ export interface RowDetailViewOption {
postTemplate?: (item: any) => string;
/** Async server function call */
- process: (item: any) => Promise;
+ process: (item: any) => Promise | Observable | Subject;
/** Override the logic for showing (or not) the expand icon (use case example: only every 2nd row is expandable) */
expandableOverride?: (row: number, dataContext: any, grid: SlickGrid) => boolean;
diff --git a/packages/common/src/interfaces/subscription.interface.ts b/packages/common/src/interfaces/subscription.interface.ts
index 8f4f75f64..f537b0689 100644
--- a/packages/common/src/interfaces/subscription.interface.ts
+++ b/packages/common/src/interfaces/subscription.interface.ts
@@ -1,4 +1,4 @@
-export interface Subscription {
+export interface EventSubscription {
/** Disposes the subscription. */
dispose?: () => void;
diff --git a/packages/common/src/services/__tests__/backend-utilities.spec.ts b/packages/common/src/services/__tests__/backend-utilities.spec.ts
index 7aecfd011..d9840c4bd 100644
--- a/packages/common/src/services/__tests__/backend-utilities.spec.ts
+++ b/packages/common/src/services/__tests__/backend-utilities.spec.ts
@@ -1,6 +1,9 @@
+import 'jest-extended';
+import { of, Subject, throwError } from 'rxjs';
+
import { BackendServiceApi, GridOption } from '../../interfaces/index';
-import main, { executeBackendProcessesCallback, onBackendError, refreshBackendDataset } from '../../services/backend-utilities';
-// import { GraphqlService } from '../../services/graphql.service';
+import { BackendUtilityService } from '../backendUtility.service';
+import { RxJsResourceStub } from '../../../../../test/rxjsResourceStub';
jest.mock('flatpickr', () => { });
@@ -11,8 +14,10 @@ const graphqlServiceMock = {
updateSorters: jest.fn(),
} as unknown;
-describe('backend-utilities', () => {
+describe('Backend Utility Service', () => {
let gridOptionMock: GridOption;
+ let rxjsResourceStub: RxJsResourceStub;
+ let service: BackendUtilityService;
beforeEach(() => {
gridOptionMock = {
@@ -30,6 +35,8 @@ describe('backend-utilities', () => {
totalItems: 0
}
} as GridOption;
+ rxjsResourceStub = new RxJsResourceStub();
+ service = new BackendUtilityService(rxjsResourceStub);
});
describe('executeBackendProcessesCallback method', () => {
@@ -37,7 +44,7 @@ describe('backend-utilities', () => {
const now = new Date();
gridOptionMock.backendServiceApi!.internalPostProcess = jest.fn();
const spy = jest.spyOn(gridOptionMock.backendServiceApi as BackendServiceApi, 'internalPostProcess');
- executeBackendProcessesCallback(now, { data: {} }, gridOptionMock.backendServiceApi as BackendServiceApi, 0);
+ service.executeBackendProcessesCallback(now, { data: {} }, gridOptionMock.backendServiceApi as BackendServiceApi, 0);
expect(spy).toHaveBeenCalled();
});
@@ -59,7 +66,7 @@ describe('backend-utilities', () => {
gridOptionMock.pagination = { totalItems: 1, pageSizes: [10, 25], pageSize: 10 };
const spy = jest.spyOn(gridOptionMock.backendServiceApi as BackendServiceApi, 'postProcess');
- executeBackendProcessesCallback(now, mockResult, gridOptionMock.backendServiceApi as BackendServiceApi, 1);
+ service.executeBackendProcessesCallback(now, mockResult, gridOptionMock.backendServiceApi as BackendServiceApi, 1);
expect(spy).toHaveBeenCalledWith(expectaction);
});
@@ -70,29 +77,24 @@ describe('backend-utilities', () => {
gridOptionMock.backendServiceApi!.onError = jest.fn();
const spy = jest.spyOn(gridOptionMock.backendServiceApi as BackendServiceApi, 'onError');
- onBackendError('some error', gridOptionMock.backendServiceApi);
+ service.onBackendError('some error', gridOptionMock.backendServiceApi);
expect(spy).toHaveBeenCalled();
});
it('should throw back the error when callback was provided', () => {
gridOptionMock.backendServiceApi!.onError = undefined;
- expect(() => onBackendError('some error', gridOptionMock.backendServiceApi)).toThrow();
+ expect(() => service.onBackendError('some error', gridOptionMock.backendServiceApi)).toThrow();
});
});
describe('refreshBackendDataset method', () => {
- let executeSpy;
-
- beforeAll(() => {
- executeSpy = jest.spyOn(main, 'executeBackendCallback');
- });
-
it('should call "executeBackendCallback" after calling the "refreshBackendDataset" method with Pagination', () => {
const query = `query { users (first:20,offset:0) { totalCount, nodes { id,name,gender,company } } }`;
const querySpy = jest.spyOn(gridOptionMock.backendServiceApi!.service, 'buildQuery').mockReturnValue(query);
+ const executeSpy = jest.spyOn(service, 'executeBackendCallback');
- refreshBackendDataset(gridOptionMock);
+ service.refreshBackendDataset(gridOptionMock);
expect(querySpy).toHaveBeenCalled();
expect(executeSpy).toHaveBeenCalledWith(gridOptionMock.backendServiceApi as BackendServiceApi, query, null, expect.toBeDate(), gridOptionMock.pagination!.totalItems);
@@ -102,8 +104,9 @@ describe('backend-utilities', () => {
gridOptionMock.enablePagination = false;
const query = `query { users { id,name,gender,company } }`;
const querySpy = jest.spyOn(gridOptionMock.backendServiceApi!.service, 'buildQuery').mockReturnValue(query);
+ const executeSpy = jest.spyOn(service, 'executeBackendCallback');
- refreshBackendDataset(gridOptionMock);
+ service.refreshBackendDataset(gridOptionMock);
expect(querySpy).toHaveBeenCalled();
expect(executeSpy).toHaveBeenCalledWith(gridOptionMock.backendServiceApi as BackendServiceApi, query, null, expect.toBeDate(), gridOptionMock.pagination!.totalItems);
@@ -113,11 +116,57 @@ describe('backend-utilities', () => {
gridOptionMock.enablePagination = true;
try {
gridOptionMock.backendServiceApi = undefined;
- refreshBackendDataset(undefined);
+ service.refreshBackendDataset(undefined);
} catch (e) {
expect(e.toString()).toContain('BackendServiceApi requires at least a "process" function and a "service" defined');
done();
}
});
});
+
+ describe('executeBackendCallback method', () => {
+ it('should expect that executeBackendProcessesCallback will be called after the process Observable resolves', (done) => {
+ const subject = new Subject();
+ const now = new Date();
+ const query = `query { users (first:20,offset:0) { totalCount, nodes { id,name,gender,company } } }`;
+ const processResult = {
+ data: { users: { nodes: [] }, pageInfo: { hasNextPage: true }, totalCount: 0 },
+ metrics: { startTime: now, endTime: now, executionTime: 0, totalItemCount: 0 }
+ };
+
+ const nextSpy = jest.spyOn(subject, 'next');
+ const processSpy = jest.spyOn(gridOptionMock.backendServiceApi, 'process').mockReturnValue(of(processResult));
+ const executeProcessesSpy = jest.spyOn(service, 'executeBackendProcessesCallback');
+
+ service.addRxJsResource(rxjsResourceStub);
+ service.executeBackendCallback(gridOptionMock.backendServiceApi, query, {}, now, 10, null, subject);
+
+ setTimeout(() => {
+ expect(nextSpy).toHaveBeenCalled();
+ expect(processSpy).toHaveBeenCalled();
+ expect(executeProcessesSpy).toHaveBeenCalledWith(now, processResult, gridOptionMock.backendServiceApi, 10);
+ done();
+ });
+ });
+
+ it('should expect that onBackendError will be called after the process Observable throws an error', (done) => {
+ const errorExpected = 'observable error';
+ const subject = new Subject();
+ const now = new Date();
+ service.onBackendError = jest.fn();
+ const query = `query { users (first:20,offset:0) { totalCount, nodes { id,name,gender,company } } }`;
+ const nextSpy = jest.spyOn(subject, 'next');
+ const processSpy = jest.spyOn(gridOptionMock.backendServiceApi, 'process').mockReturnValue(throwError(errorExpected));
+
+ service.addRxJsResource(rxjsResourceStub);
+ service.executeBackendCallback(gridOptionMock.backendServiceApi, query, {}, now, 10, null, subject);
+
+ setTimeout(() => {
+ expect(nextSpy).toHaveBeenCalled();
+ expect(processSpy).toHaveBeenCalled();
+ expect(service.onBackendError).toHaveBeenCalledWith(errorExpected, gridOptionMock.backendServiceApi);
+ done();
+ });
+ });
+ });
});
diff --git a/packages/common/src/services/__tests__/filter.service.spec.ts b/packages/common/src/services/__tests__/filter.service.spec.ts
index b864b32a0..a80fab1b6 100644
--- a/packages/common/src/services/__tests__/filter.service.spec.ts
+++ b/packages/common/src/services/__tests__/filter.service.spec.ts
@@ -1,7 +1,7 @@
// import 3rd party lib multiple-select for the tests
import 'multiple-select-modified';
-
import 'jest-extended';
+import { of, throwError } from 'rxjs';
import { FieldType } from '../../enums/index';
import {
@@ -25,14 +25,11 @@ import { FilterFactory } from '../../filters/filterFactory';
import { getParsedSearchTermsByFieldType } from '../../filter-conditions';
import { SlickgridConfig } from '../../slickgrid-config';
import { SharedService } from '../shared.service';
-import * as utilities from '../../services/backend-utilities';
+import { BackendUtilityService } from '../backendUtility.service';
import { CollectionService } from '../collection.service';
import { TranslateServiceStub } from '../../../../../test/translateServiceStub';
import { PubSubService } from '../pubSub.service';
-
-const mockRefreshBackendDataset = jest.fn();
-// @ts-ignore:2540
-utilities.refreshBackendDataset = mockRefreshBackendDataset;
+import { RxJsResourceStub } from '../../../../../test/rxjsResourceStub';
jest.mock('flatpickr', () => { });
declare const Slick: SlickNamespace;
@@ -113,10 +110,12 @@ const pubSubServiceStub = {
describe('FilterService', () => {
let service: FilterService;
+ let backendUtilityService: BackendUtilityService;
let collectionService: CollectionService;
let sharedService: SharedService;
let slickgridConfig: SlickgridConfig;
let slickgridEventHandler: SlickEventHandler;
+ let rxjsResourceStub: RxJsResourceStub;
let translateService: TranslateServiceStub;
beforeEach(() => {
@@ -129,8 +128,10 @@ describe('FilterService', () => {
slickgridConfig = new SlickgridConfig();
translateService = new TranslateServiceStub();
collectionService = new CollectionService(translateService);
+ rxjsResourceStub = new RxJsResourceStub();
+ backendUtilityService = new BackendUtilityService();
const filterFactory = new FilterFactory(slickgridConfig, translateService, collectionService);
- service = new FilterService(filterFactory, pubSubServiceStub, sharedService);
+ service = new FilterService(filterFactory, pubSubServiceStub, sharedService, backendUtilityService, rxjsResourceStub);
slickgridEventHandler = service.eventHandler;
});
@@ -525,6 +526,26 @@ describe('FilterService', () => {
done();
});
});
+
+ it('should execute the "onError" method when the Observable throws an error', (done) => {
+ const errorExpected = 'observable error';
+ const spyProcess = jest.fn();
+ gridOptionMock.backendServiceApi!.process = () => of(spyProcess);
+ gridOptionMock.backendServiceApi!.onError = () => jest.fn();
+ const pubSubSpy = jest.spyOn(pubSubServiceStub, 'publish');
+ const spyOnError = jest.spyOn(gridOptionMock.backendServiceApi as BackendServiceApi, 'onError');
+ jest.spyOn(gridOptionMock.backendServiceApi, 'process').mockReturnValue(throwError(errorExpected));
+
+ backendUtilityService.addRxJsResource(rxjsResourceStub);
+ service.addRxJsResource(rxjsResourceStub);
+ service.clearFilters();
+
+ setTimeout(() => {
+ expect(pubSubSpy).toHaveBeenCalledWith(`onFilterCleared`, true);
+ expect(spyOnError).toHaveBeenCalledWith(errorExpected);
+ done();
+ });
+ });
});
});
@@ -1102,6 +1123,7 @@ describe('FilterService', () => {
const emitSpy = jest.spyOn(service, 'emitFilterChanged');
const backendUpdateSpy = jest.spyOn(backendServiceStub, 'updateFilters');
const backendProcessSpy = jest.spyOn(backendServiceStub, 'processOnFilterChanged');
+ const refreshBackendSpy = jest.spyOn(backendUtilityService, 'refreshBackendDataset');
service.init(gridStub);
service.bindBackendOnFilter(gridStub);
@@ -1118,7 +1140,7 @@ describe('FilterService', () => {
isActive: { columnId: 'isActive', columnDef: mockColumn2, searchTerms: [false], operator: 'EQ', parsedSearchTerms: false, type: FieldType.boolean }
});
expect(clearSpy).toHaveBeenCalledWith(false);
- expect(mockRefreshBackendDataset).toHaveBeenCalledWith(gridOptionMock);
+ expect(refreshBackendSpy).toHaveBeenCalledWith(gridOptionMock);
});
it('should expect filters to be sent to the backend when using "bindBackendOnFilter" without triggering a filter changed event neither a backend query when both flag arguments are set to false', () => {
@@ -1131,6 +1153,7 @@ describe('FilterService', () => {
const emitSpy = jest.spyOn(service, 'emitFilterChanged');
const backendUpdateSpy = jest.spyOn(backendServiceStub, 'updateFilters');
const backendProcessSpy = jest.spyOn(backendServiceStub, 'processOnFilterChanged');
+ const refreshBackendSpy = jest.spyOn(backendUtilityService, 'refreshBackendDataset');
service.init(gridStub);
service.bindBackendOnFilter(gridStub);
@@ -1140,7 +1163,7 @@ describe('FilterService', () => {
expect(backendProcessSpy).not.toHaveBeenCalled();
expect(emitSpy).not.toHaveBeenCalled();
- expect(mockRefreshBackendDataset).not.toHaveBeenCalled();
+ expect(refreshBackendSpy).not.toHaveBeenCalled();
expect(backendUpdateSpy).toHaveBeenCalledWith(mockNewFilters, true);
expect(service.getColumnFilters()).toEqual({
firstName: { columnId: 'firstName', columnDef: mockColumn1, searchTerms: ['Jane'], operator: 'StartsWith', parsedSearchTerms: ['Jane'], type: FieldType.string },
@@ -1199,6 +1222,7 @@ describe('FilterService', () => {
const emitSpy = jest.spyOn(service, 'emitFilterChanged');
const backendUpdateSpy = jest.spyOn(backendServiceStub, 'updateFilters');
const backendProcessSpy = jest.spyOn(backendServiceStub, 'processOnFilterChanged');
+ const refreshBackendSpy = jest.spyOn(backendUtilityService, 'refreshBackendDataset');
service.init(gridStub);
service.bindBackendOnFilter(gridStub);
@@ -1210,7 +1234,7 @@ describe('FilterService', () => {
expect(backendProcessSpy).not.toHaveBeenCalled();
expect(backendUpdateSpy).toHaveBeenCalledWith(expectation, true);
expect(service.getColumnFilters()).toEqual(expectation);
- expect(mockRefreshBackendDataset).toHaveBeenCalledWith(gridOptionMock);
+ expect(refreshBackendSpy).toHaveBeenCalledWith(gridOptionMock);
});
it('should expect filter to be sent to the backend when using "bindBackendOnFilter" without triggering a filter changed event neither a backend query when both flag arguments are set to false', () => {
@@ -1225,6 +1249,7 @@ describe('FilterService', () => {
const emitSpy = jest.spyOn(service, 'emitFilterChanged');
const backendUpdateSpy = jest.spyOn(backendServiceStub, 'updateFilters');
const backendProcessSpy = jest.spyOn(backendServiceStub, 'processOnFilterChanged');
+ const refreshBackendSpy = jest.spyOn(backendUtilityService, 'refreshBackendDataset');
service.init(gridStub);
service.bindBackendOnFilter(gridStub);
@@ -1234,7 +1259,7 @@ describe('FilterService', () => {
expect(backendProcessSpy).not.toHaveBeenCalled();
expect(emitSpy).not.toHaveBeenCalled();
- expect(mockRefreshBackendDataset).not.toHaveBeenCalled();
+ expect(refreshBackendSpy).not.toHaveBeenCalled();
expect(backendUpdateSpy).toHaveBeenCalledWith(expectation, true);
expect(service.getColumnFilters()).toEqual(expectation);
});
diff --git a/packages/common/src/services/__tests__/pagination.service.spec.ts b/packages/common/src/services/__tests__/pagination.service.spec.ts
index 79a91792c..e0d794cda 100644
--- a/packages/common/src/services/__tests__/pagination.service.spec.ts
+++ b/packages/common/src/services/__tests__/pagination.service.spec.ts
@@ -1,9 +1,11 @@
import 'jest-extended';
+import { of, throwError } from 'rxjs';
import { PaginationService } from './../pagination.service';
import { SharedService } from '../shared.service';
import { Column, SlickDataView, GridOption, SlickGrid, SlickNamespace, BackendServiceApi, Pagination } from '../../interfaces/index';
-import * as utilities from '../backend-utilities';
+import { BackendUtilityService } from '../backendUtility.service';
+import { RxJsResourceStub } from '../../../../../test/rxjsResourceStub';
declare const Slick: SlickNamespace;
@@ -18,13 +20,12 @@ jest.mock('../pubSub.service', () => ({
PubSubService: () => mockPubSub
}));
-const mockExecuteBackendProcess = jest.fn();
-// @ts-ignore:2540
-utilities.executeBackendProcessesCallback = mockExecuteBackendProcess;
-
-const mockBackendError = jest.fn();
-// @ts-ignore:2540
-utilities.onBackendError = mockBackendError;
+const backendUtilityServiceStub = {
+ executeBackendProcessesCallback: jest.fn(),
+ executeBackendCallback: jest.fn(),
+ onBackendError: jest.fn(),
+ refreshBackendDataset: jest.fn(),
+} as unknown as BackendUtilityService;
const dataviewStub = {
onPagingInfoChanged: new Slick.Event(),
@@ -43,13 +44,6 @@ const mockBackendService = {
processOnPaginationChanged: jest.fn(),
};
-// const pubSubServiceStub = {
-// publish: jest.fn(),
-// subscribe: jest.fn(),
-// unsubscribe: jest.fn(),
-// unsubscribeAll: jest.fn(),
-// } as PubSubService;
-
const mockGridOption = {
enableAutoResize: true,
enablePagination: true,
@@ -84,10 +78,12 @@ const gridStub = {
describe('PaginationService', () => {
let service: PaginationService;
let sharedService: SharedService;
+ let rxjsResourceStub: RxJsResourceStub;
beforeEach(() => {
sharedService = new SharedService();
- service = new PaginationService(mockPubSub, sharedService);
+ rxjsResourceStub = new RxJsResourceStub();
+ service = new PaginationService(mockPubSub, sharedService, backendUtilityServiceStub, rxjsResourceStub);
jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(mockGridOption);
});
@@ -375,20 +371,38 @@ describe('PaginationService', () => {
const postSpy = jest.fn();
mockGridOption.backendServiceApi!.process = postSpy;
jest.spyOn(mockBackendService, 'processOnPaginationChanged').mockReturnValue('backend query');
- const promise = new Promise((_resolve, reject) => setTimeout(() => reject(mockError), 1));
- jest.spyOn(mockGridOption.backendServiceApi as BackendServiceApi, 'process').mockReturnValue(promise);
+ jest.spyOn(mockGridOption.backendServiceApi as BackendServiceApi, 'process').mockReturnValue(Promise.reject(mockError));
+ const backendErrorSpy = jest.spyOn(backendUtilityServiceStub, 'onBackendError');
try {
service.init(gridStub, mockGridOption.pagination as Pagination, mockGridOption.backendServiceApi);
await service.processOnPageChanged(1);
} catch (e) {
- expect(mockBackendError).toHaveBeenCalledWith(mockError, mockGridOption.backendServiceApi);
+ expect(backendErrorSpy).toHaveBeenCalledWith(mockError, mockGridOption.backendServiceApi);
+ }
+ });
+
+ it('should execute "process" method and catch error when process Observable fails', async (done) => {
+ const mockError = 'observable error';
+ const postSpy = jest.fn();
+ mockGridOption.backendServiceApi!.process = postSpy;
+ jest.spyOn(mockBackendService, 'processOnPaginationChanged').mockReturnValue('backend query');
+ jest.spyOn(mockGridOption.backendServiceApi as BackendServiceApi, 'process').mockReturnValue(throwError(mockError));
+ const backendErrorSpy = jest.spyOn(backendUtilityServiceStub, 'onBackendError');
+
+ try {
+ service.init(gridStub, mockGridOption.pagination as Pagination, mockGridOption.backendServiceApi);
+ await service.processOnPageChanged(1);
+ } catch (e) {
+ expect(backendErrorSpy).toHaveBeenCalledWith(mockError, mockGridOption.backendServiceApi);
+ done();
}
});
- it('should execute "process" method when defined', (done) => {
+ it('should execute "process" method when defined as a Promise', (done) => {
const postSpy = jest.fn();
mockGridOption.backendServiceApi!.process = postSpy;
+ const backendExecuteSpy = jest.spyOn(backendUtilityServiceStub, 'executeBackendProcessesCallback');
jest.spyOn(mockBackendService, 'processOnPaginationChanged').mockReturnValue('backend query');
const now = new Date();
const processResult = { users: [{ name: 'John' }], metrics: { startTime: now, endTime: now, executionTime: 0, totalItemCount: 0 } };
@@ -400,11 +414,31 @@ describe('PaginationService', () => {
setTimeout(() => {
expect(postSpy).toHaveBeenCalled();
- expect(mockExecuteBackendProcess).toHaveBeenCalledWith(expect.toBeDate(), processResult, mockGridOption.backendServiceApi as BackendServiceApi, 85);
+ expect(backendExecuteSpy).toHaveBeenCalledWith(expect.toBeDate(), processResult, mockGridOption.backendServiceApi as BackendServiceApi, 85);
done();
}, 10);
});
+ it('should execute "process" method when defined as an Observable', (done) => {
+ const postSpy = jest.fn();
+ mockGridOption.backendServiceApi.process = postSpy;
+ const backendExecuteSpy = jest.spyOn(backendUtilityServiceStub, 'executeBackendProcessesCallback');
+ jest.spyOn(mockBackendService, 'processOnPaginationChanged').mockReturnValue('backend query');
+ const now = new Date();
+ const processResult = { users: [{ name: 'John' }], metrics: { startTime: now, endTime: now, executionTime: 0, totalItemCount: 0 } };
+ jest.spyOn(mockGridOption.backendServiceApi as BackendServiceApi, 'process').mockReturnValue(of(processResult));
+
+ service.addRxJsResource(rxjsResourceStub);
+ service.init(gridStub, mockGridOption.pagination as Pagination, mockGridOption.backendServiceApi);
+ service.processOnPageChanged(1);
+
+ setTimeout(() => {
+ expect(postSpy).toHaveBeenCalled();
+ expect(backendExecuteSpy).toHaveBeenCalledWith(expect.toBeDate(), processResult, mockGridOption.backendServiceApi as BackendServiceApi, 85);
+ done();
+ });
+ });
+
it('should call "setPagingOptions" from the DataView and trigger "onPaginationChanged" when using a Local Grid', () => {
const pubSubSpy = jest.spyOn(mockPubSub, 'publish');
const setPagingSpy = jest.spyOn(dataviewStub, 'setPagingOptions');
diff --git a/packages/common/src/services/__tests__/rxjsFacade.spec.ts b/packages/common/src/services/__tests__/rxjsFacade.spec.ts
new file mode 100644
index 000000000..34bbe4867
--- /dev/null
+++ b/packages/common/src/services/__tests__/rxjsFacade.spec.ts
@@ -0,0 +1,67 @@
+import { Observable, RxJsFacade, Subject, Subscription } from '../rxjsFacade';
+
+describe('RxJsFacade Service', () => {
+ it('should display a not implemented when calling "EMPTY" getter', () => {
+ expect(() => RxJsFacade.prototype.EMPTY).toThrow('RxJS Facade "EMPTY" constant must be implemented');
+ });
+
+ it('should display a not implemented when calling "createObservable" method', () => {
+ expect(() => RxJsFacade.prototype.createObservable()).toThrow('RxJS Facade "createObservable" method must be implemented');
+ });
+
+ it('should display a not implemented when calling "createSubject" method', () => {
+ expect(() => RxJsFacade.prototype.createSubject()).toThrow('RxJS Facade "createSubject" method must be implemented');
+ });
+
+ it('should display a not implemented when calling "firstValueFrom" method', () => {
+ expect(() => RxJsFacade.prototype.firstValueFrom({} as any)).toThrow('RxJS Facade "firstValueFrom" method must be implemented');
+ });
+
+ it('should display a not implemented when calling "iif" method', () => {
+ expect(() => RxJsFacade.prototype.iif(() => false)).toThrow('RxJS Facade "iif" method must be implemented');
+ });
+
+ it('should always return False when calling "isObservable" method', () => {
+ expect(RxJsFacade.prototype.isObservable({})).toBe(false);
+ });
+
+ it('should display a not implemented when calling "takeUntil" method', () => {
+ expect(() => RxJsFacade.prototype.takeUntil({} as any)).toThrow('RxJS Facade "takeUntil" method must be implemented');
+ });
+});
+
+describe('Subject Service', () => {
+ it('should display a not implemented when calling "next" method', () => {
+ expect(() => Subject.prototype.next({} as any)).toThrow('RxJS Subject "next" method must be implemented');
+ });
+
+ it('should display a not implemented when calling "unsubscribe" method', () => {
+ expect(() => Subject.prototype.unsubscribe()).toThrow('RxJS Subject "unsubscribe" method must be implemented');
+ });
+});
+
+describe('Observable Service', () => {
+ it('should display a not implemented when calling "constructor"', () => {
+ // @ts-ignore
+ expect(() => new Observable()).toThrow('RxJS Observable Facade "constructor" method must be implemented');
+ });
+
+ it('should display a not implemented when calling "subscribe" method', () => {
+ expect(() => Observable.prototype.subscribe()).toThrow('RxJS Observable Facade "subscribe" method must be implemented');
+ });
+
+ it('should display a not implemented when calling "pipe" method', () => {
+ expect(() => Observable.prototype.pipe({})).toThrow('RxJS Observable Facade "pipe" method must be implemented');
+ });
+});
+
+describe('Subscription Service', () => {
+ it('should display a not implemented when calling "next" method', () => {
+ // @ts-ignore
+ expect(() => new Subscription()).toThrow('RxJS Subscription Facade "constructor" method must be implemented');
+ });
+
+ it('should display a not implemented when calling "pipe" method', () => {
+ expect(() => Subscription.prototype.unsubscribe()).toThrow('RxJS Subscription Facade "unsubscribe" method must be implemented');
+ });
+});
diff --git a/packages/common/src/services/__tests__/sort.service.spec.ts b/packages/common/src/services/__tests__/sort.service.spec.ts
index 7b753774c..6d41498d0 100644
--- a/packages/common/src/services/__tests__/sort.service.spec.ts
+++ b/packages/common/src/services/__tests__/sort.service.spec.ts
@@ -1,3 +1,5 @@
+import { of, throwError } from 'rxjs';
+
import { EmitterType, FieldType, } from '../../enums/index';
import {
BackendService,
@@ -16,15 +18,13 @@ import {
} from '../../interfaces/index';
import { SortComparers } from '../../sortComparers';
import { SortService } from '../sort.service';
-import * as utilities from '../../services/backend-utilities';
+import { BackendUtilityService } from '../backendUtility.service';
import { PubSubService } from '../pubSub.service';
import { SharedService } from '../shared.service';
+import { RxJsResourceStub } from '../../../../../test/rxjsResourceStub';
declare const Slick: SlickNamespace;
-const mockRefreshBackendDataset = jest.fn();
-// @ts-ignore:2540
-utilities.refreshBackendDataset = mockRefreshBackendDataset;
const gridOptionMock = {
enablePagination: true,
@@ -88,14 +88,19 @@ const pubSubServiceStub = {
} as PubSubService;
describe('SortService', () => {
+ let backendUtilityService: BackendUtilityService;
let sharedService: SharedService;
let service: SortService;
+ let rxjsResourceStub: RxJsResourceStub;
let slickgridEventHandler: SlickEventHandler;
beforeEach(() => {
+ backendUtilityService = new BackendUtilityService();
sharedService = new SharedService();
+ rxjsResourceStub = new RxJsResourceStub();
sharedService.dataView = dataViewStub;
- service = new SortService(sharedService, pubSubServiceStub);
+
+ service = new SortService(sharedService, pubSubServiceStub, backendUtilityService, rxjsResourceStub);
slickgridEventHandler = service.eventHandler;
});
@@ -477,6 +482,25 @@ describe('SortService', () => {
done();
}, 0);
});
+
+ it('should execute the "onError" method when the Observable throws an error', (done) => {
+ const spyProcess = jest.fn();
+ const errorExpected = 'observable error';
+ gridOptionMock.backendServiceApi.process = () => of(spyProcess);
+ gridOptionMock.backendServiceApi.onError = (e) => jest.fn();
+ const spyOnError = jest.spyOn(gridOptionMock.backendServiceApi, 'onError');
+ jest.spyOn(gridOptionMock.backendServiceApi, 'process').mockReturnValue(throwError(errorExpected));
+
+ backendUtilityService.addRxJsResource(rxjsResourceStub);
+ service.addRxJsResource(rxjsResourceStub);
+ service.bindBackendOnSort(gridStub);
+ service.onBackendSortChanged(undefined, { multiColumnSort: true, sortCols: [], grid: gridStub });
+
+ setTimeout(() => {
+ expect(spyOnError).toHaveBeenCalledWith(errorExpected);
+ done();
+ });
+ });
});
describe('getCurrentColumnSorts method', () => {
@@ -907,6 +931,7 @@ describe('SortService', () => {
};
const emitSpy = jest.spyOn(service, 'emitSortChanged');
const backendUpdateSpy = jest.spyOn(backendServiceStub, 'updateSorters');
+ const refreshBackendSpy = jest.spyOn(backendUtilityService, 'refreshBackendDataset');
service.bindLocalOnSort(gridStub);
service.updateSorting(mockNewSorters);
@@ -914,7 +939,7 @@ describe('SortService', () => {
expect(emitSpy).toHaveBeenCalledWith('remote');
expect(service.getCurrentLocalSorters()).toEqual([]);
expect(backendUpdateSpy).toHaveBeenCalledWith(undefined, mockNewSorters);
- expect(mockRefreshBackendDataset).toHaveBeenCalledWith(gridOptionMock);
+ expect(refreshBackendSpy).toHaveBeenCalledWith(gridOptionMock);
});
it('should expect sorters to be sent to the backend when using "bindBackendOnSort" without triggering a sort changed event neither a backend query when both flag arguments are set to false', () => {
@@ -924,13 +949,14 @@ describe('SortService', () => {
};
const emitSpy = jest.spyOn(service, 'emitSortChanged');
const backendUpdateSpy = jest.spyOn(backendServiceStub, 'updateSorters');
+ const refreshBackendSpy = jest.spyOn(backendUtilityService, 'refreshBackendDataset');
service.bindBackendOnSort(gridStub);
service.updateSorting(mockNewSorters, false, false);
expect(emitSpy).not.toHaveBeenCalled();
expect(backendUpdateSpy).toHaveBeenCalledWith(undefined, mockNewSorters);
- expect(mockRefreshBackendDataset).not.toHaveBeenCalled();
+ expect(refreshBackendSpy).not.toHaveBeenCalled();
});
});
diff --git a/packages/common/src/services/__tests__/utilities.spec.ts b/packages/common/src/services/__tests__/utilities.spec.ts
index 4afc7f7a1..967c94640 100644
--- a/packages/common/src/services/__tests__/utilities.spec.ts
+++ b/packages/common/src/services/__tests__/utilities.spec.ts
@@ -1,11 +1,14 @@
import 'jest-extended';
+import { of } from 'rxjs';
import { FieldType, OperatorType } from '../../enums/index';
-import { GridOption } from '../../interfaces/index';
+import { GridOption, Subscription } from '../../interfaces/index';
+import { RxJsResourceStub } from '../../../../../test/rxjsResourceStub';
import {
addToArrayWhenNotExists,
addWhiteSpaces,
arrayRemoveItemByIndex,
+ castObservableToPromise,
convertHierarchicalViewToParentChildArray,
convertParentChildArrayToHierarchicalView,
decimalFormatted,
@@ -36,6 +39,7 @@ import {
toCamelCase,
toKebabCase,
toSnakeCase,
+ unsubscribeAll,
uniqueArray,
uniqueObjectArray,
} from '../utilities';
@@ -173,6 +177,32 @@ describe('Service/Utilies', () => {
});
});
+ describe('castObservableToPromise method', () => {
+ let rxjs: RxJsResourceStub;
+ beforeEach(() => {
+ rxjs = new RxJsResourceStub();
+ });
+
+ it('should throw an error when argument provided is not a Promise neither an Observable', async () => {
+ expect(() => castObservableToPromise(rxjs, null)).toThrowError('Something went wrong,');
+ });
+
+ it('should return original Promise when argument is already a Promise', async () => {
+ const promise = new Promise((resolve) => setTimeout(() => resolve('hello'), 1));
+ const castPromise = castObservableToPromise(rxjs, promise);
+ expect(promise).toBe(castPromise);
+ });
+
+ it('should be able to cast an Observable to a Promise', () => {
+ const inputArray = ['hello', 'world'];
+ const observable = of(inputArray);
+
+ castObservableToPromise(rxjs, observable).then((outputArray) => {
+ expect(outputArray).toBe(inputArray);
+ });
+ });
+ });
+
describe('convertHierarchicalViewToParentChildArray method', () => {
let mockColumns;
@@ -1451,6 +1481,35 @@ describe('Service/Utilies', () => {
});
});
+ describe('unsubscribeAll method', () => {
+ it('should return original array when array of subscriptions is empty', () => {
+ const output = unsubscribeAll([]);
+ expect(output).toEqual([]);
+ });
+
+ it('should be able to unsubscribe all Observables', () => {
+ const subscriptions: Subscription[] = [];
+ const observable1 = of([1, 2]);
+ const observable2 = of([1, 2]);
+ subscriptions.push(observable1.subscribe(), observable2.subscribe());
+ const output = unsubscribeAll(subscriptions);
+ expect(output).toHaveLength(0);
+ });
+
+ it('should be able to unsubscribe all PubSub events or anything that has an unsubscribe method', () => {
+ const mockUnsubscribe1 = jest.fn();
+ const mockUnsubscribe2 = jest.fn();
+ const mockSubscription1 = { unsubscribe: mockUnsubscribe1 };
+ const mockSubscription2 = { unsubscribe: mockUnsubscribe2 };
+ const mockSubscriptions = [mockSubscription1, mockSubscription2];
+
+ unsubscribeAll(mockSubscriptions);
+
+ expect(mockUnsubscribe1).toHaveBeenCalledTimes(1);
+ expect(mockUnsubscribe2).toHaveBeenCalledTimes(1);
+ });
+ });
+
describe('uniqueArray method', () => {
it('should return original value when input is not an array', () => {
const output1 = uniqueArray(null as any);
diff --git a/packages/common/src/services/backend-utilities.ts b/packages/common/src/services/backend-utilities.ts
deleted file mode 100644
index c7a12489e..000000000
--- a/packages/common/src/services/backend-utilities.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import { EmitterType } from '../enums/emitterType.enum';
-import { BackendServiceApi, GridOption } from '../interfaces/index';
-
-// construct a main object that will be exported and used by unit tests
-const main: any = {};
-
-/** Execute the Backend Processes Callback, that could come from an Observable or a Promise callback */
-main.executeBackendProcessesCallback = function exeBackendProcessesCallback(startTime: Date, processResult: any, backendApi: BackendServiceApi, totalItems: number): any {
- const endTime = new Date();
-
- // define what our internal Post Process callback, only available for GraphQL Service for now
- // it will basically refresh the Dataset & Pagination removing the need for the user to always create his own PostProcess every time
- if (processResult && backendApi && backendApi.internalPostProcess) {
- backendApi.internalPostProcess(processResult);
- }
-
- // send the response process to the postProcess callback
- if (backendApi.postProcess !== undefined) {
- if (processResult instanceof Object) {
- processResult.metrics = {
- startTime,
- endTime,
- executionTime: endTime.valueOf() - startTime.valueOf(),
- itemCount: totalItems,
- totalItemCount: totalItems,
- };
- }
- backendApi.postProcess(processResult);
- }
-};
-
-/** On a backend service api error, we will run the "onError" if there is 1 provided or just throw back the error when nothing is provided */
-main.onBackendError = function backendError(e: any, backendApi: BackendServiceApi) {
- if (backendApi && backendApi.onError) {
- backendApi.onError(e);
- } else {
- throw e;
- }
-};
-
-/**
- * Execute the backend callback, which are mainly the "process" & "postProcess" methods.
- * Also note that "preProcess" was executed prior to this callback
- */
-main.executeBackendCallback = function exeBackendCallback(backendServiceApi: BackendServiceApi, query: string, args: any, startTime: Date, totalItems: number, emitActionChangedCallback: (type: EmitterType) => void) {
- if (backendServiceApi) {
- // emit an onFilterChanged event when it's not called by a clear filter
- if (args && !args.clearFilterTriggered && !args.clearSortTriggered) {
- emitActionChangedCallback(EmitterType.remote);
- }
-
- // the processes can be Observables (like HttpClient) or Promises
- const process = backendServiceApi.process(query);
- if (process instanceof Promise && process.then) {
- process.then((processResult: any) => main.executeBackendProcessesCallback(startTime, processResult, backendServiceApi, totalItems))
- .catch((error: any) => main.onBackendError(error, backendServiceApi));
- }
- }
-};
-
-/** Refresh the dataset through the Backend Service */
-main.refreshBackendDataset = function refreshBackend(gridOptions: GridOption) {
- let query = '';
- const backendApi = gridOptions && gridOptions.backendServiceApi;
-
- if (!backendApi || !backendApi.service || !backendApi.process) {
- throw new Error(`BackendServiceApi requires at least a "process" function and a "service" defined`);
- }
-
- if (backendApi.service) {
- query = backendApi.service.buildQuery();
- }
-
- if (query && query !== '') {
- // keep start time & end timestamps & return it after process execution
- const startTime = new Date();
-
- if (backendApi.preProcess) {
- backendApi.preProcess();
- }
-
- const totalItems = gridOptions && gridOptions.pagination && gridOptions.pagination.totalItems;
- main.executeBackendCallback(backendApi, query, null, startTime, totalItems);
- }
-};
-
-// export all methods & the main so that it works in all modules but also in Jest unit test
-// export every method as independent constant so that it still works whenever this is used in other modules
-export const executeBackendProcessesCallback = main.executeBackendProcessesCallback;
-export const onBackendError = main.onBackendError;
-export const executeBackendCallback = main.executeBackendCallback;
-export const refreshBackendDataset = main.refreshBackendDataset;
-
-export default main;
diff --git a/packages/common/src/services/backendUtility.service.ts b/packages/common/src/services/backendUtility.service.ts
new file mode 100644
index 000000000..486dd8786
--- /dev/null
+++ b/packages/common/src/services/backendUtility.service.ts
@@ -0,0 +1,107 @@
+import { EmitterType } from '../enums/emitterType.enum';
+import { BackendServiceApi, GridOption } from '../interfaces/index';
+import { Observable, RxJsFacade, Subject } from './rxjsFacade';
+
+export class BackendUtilityService {
+ constructor(private rxjs?: RxJsFacade) { }
+
+ addRxJsResource(rxjs: RxJsFacade) {
+ this.rxjs = rxjs;
+ }
+
+ /** Execute the Backend Processes Callback, that could come from an Observable or a Promise callback */
+ executeBackendProcessesCallback(startTime: Date, processResult: any, backendApi: BackendServiceApi, totalItems: number): any {
+ const endTime = new Date();
+
+ // define what our internal Post Process callback, only available for GraphQL Service for now
+ // it will basically refresh the Dataset & Pagination removing the need for the user to always create his own PostProcess every time
+ if (processResult && backendApi && backendApi.internalPostProcess) {
+ backendApi.internalPostProcess(processResult);
+ }
+
+ // send the response process to the postProcess callback
+ if (backendApi.postProcess !== undefined) {
+ if (processResult instanceof Object) {
+ processResult.metrics = {
+ startTime,
+ endTime,
+ executionTime: endTime.valueOf() - startTime.valueOf(),
+ itemCount: totalItems,
+ totalItemCount: totalItems,
+ };
+ }
+ backendApi.postProcess(processResult);
+ }
+ }
+
+ /** On a backend service api error, we will run the "onError" if there is 1 provided or just throw back the error when nothing is provided */
+ onBackendError(e: any, backendApi: BackendServiceApi) {
+ if (backendApi?.onError) {
+ backendApi.onError(e);
+ } else {
+ throw e;
+ }
+ }
+
+ /**
+ * Execute the backend callback, which are mainly the "process" & "postProcess" methods.
+ * Also note that "preProcess" was executed prior to this callback
+ */
+ executeBackendCallback(backendServiceApi: BackendServiceApi, query: string, args: any, startTime: Date, totalItems: number, emitActionChangedCallback?: (type: EmitterType) => void, httpCancelRequests$?: Subject) {
+ if (backendServiceApi) {
+ // emit an onFilterChanged event when it's not called by a clear filter
+ if (args && !args.clearFilterTriggered && !args.clearSortTriggered && emitActionChangedCallback) {
+ emitActionChangedCallback(EmitterType.remote);
+ }
+
+ // the processes can be Observables (like HttpClient) or Promises
+ const process = backendServiceApi.process(query);
+ if (process instanceof Promise && process.then) {
+ process.then((processResult: any) => this.executeBackendProcessesCallback(startTime, processResult, backendServiceApi, totalItems))
+ .catch((error: any) => this.onBackendError(error, backendServiceApi));
+ } else if (this.rxjs?.isObservable(process)) {
+ const rxjs = this.rxjs as RxJsFacade;
+
+ // this will abort any previous HTTP requests, that were previously hooked in the takeUntil, before sending a new request
+ if (rxjs.isObservable(httpCancelRequests$)) {
+ httpCancelRequests$!.next();
+ }
+
+ (process as unknown as Observable)
+ // the following takeUntil, will potentially be used later to cancel any pending http request (takeUntil another rx, that would be httpCancelRequests$, completes)
+ // but make sure the observable is actually defined with the iif condition check before piping it to the takeUntil
+ .pipe(rxjs.takeUntil(rxjs.iif(() => rxjs.isObservable(httpCancelRequests$), httpCancelRequests$, rxjs.EMPTY)))
+ .subscribe(
+ (processResult: any) => this.executeBackendProcessesCallback(startTime, processResult, backendServiceApi, totalItems),
+ (error: any) => this.onBackendError(error, backendServiceApi)
+ );
+ }
+ }
+ }
+
+ /** Refresh the dataset through the Backend Service */
+ refreshBackendDataset(gridOptions: GridOption) {
+ let query = '';
+ const backendApi = gridOptions?.backendServiceApi;
+
+ if (!backendApi || !backendApi.service || !backendApi.process) {
+ throw new Error(`BackendServiceApi requires at least a "process" function and a "service" defined`);
+ }
+
+ if (backendApi.service) {
+ query = backendApi.service.buildQuery();
+ }
+
+ if (query && query !== '') {
+ // keep start time & end timestamps & return it after process execution
+ const startTime = new Date();
+
+ if (backendApi.preProcess) {
+ backendApi.preProcess();
+ }
+
+ const totalItems = gridOptions?.pagination?.totalItems ?? 0;
+ this.executeBackendCallback(backendApi, query, null, startTime, totalItems);
+ }
+ }
+}
diff --git a/packages/common/src/services/filter.service.ts b/packages/common/src/services/filter.service.ts
index 2c7681605..efa7fa47d 100644
--- a/packages/common/src/services/filter.service.ts
+++ b/packages/common/src/services/filter.service.ts
@@ -29,10 +29,11 @@ import {
SlickGrid,
SlickNamespace,
} from './../interfaces/index';
-import { executeBackendCallback, refreshBackendDataset } from './backend-utilities';
+import { BackendUtilityService } from './backendUtility.service';
import { deepCopy, getDescendantProperty, mapOperatorByFieldType } from './utilities';
import { PubSubService } from '../services/pubSub.service';
import { SharedService } from './shared.service';
+import { RxJsFacade, Subject } from './rxjsFacade';
// using external non-typed js libraries
declare const Slick: SlickNamespace;
@@ -58,10 +59,14 @@ export class FilterService {
protected _grid!: SlickGrid;
protected _onSearchChange: SlickEvent | null;
protected _tmpPreFilteredData?: number[];
+ protected httpCancelRequests$?: Subject; // this will be used to cancel any pending http request
- constructor(protected filterFactory: FilterFactory, protected pubSubService: PubSubService, protected sharedService: SharedService) {
+ constructor(protected filterFactory: FilterFactory, protected pubSubService: PubSubService, protected sharedService: SharedService, protected backendUtilities?: BackendUtilityService, protected rxjs?: RxJsFacade) {
this._onSearchChange = new Slick.Event();
this._eventHandler = new Slick.EventHandler();
+ if (this.rxjs) {
+ this.httpCancelRequests$ = this.rxjs.createSubject();
+ }
}
/** Getter of the SlickGrid Event Handler */
@@ -94,6 +99,10 @@ export class FilterService {
return (this._grid?.getData && this._grid.getData()) as SlickDataView;
}
+ addRxJsResource(rxjs: RxJsFacade) {
+ this.rxjs = rxjs;
+ }
+
/**
* Initialize the Service
* @param grid
@@ -111,6 +120,9 @@ export class FilterService {
if (this._eventHandler && this._eventHandler.unsubscribeAll) {
this._eventHandler.unsubscribeAll();
}
+ if (this.httpCancelRequests$ && this.rxjs?.isObservable(this.httpCancelRequests$)) {
+ this.httpCancelRequests$.next(); // this cancels any pending http requests
+ }
this.disposeColumnFilters();
this._onSearchChange = null;
}
@@ -271,13 +283,13 @@ export class FilterService {
}
// when using backend service, we need to query only once so it's better to do it here
- const backendApi = this._gridOptions && this._gridOptions.backendServiceApi;
+ const backendApi = this._gridOptions?.backendServiceApi;
if (backendApi && triggerChange) {
const callbackArgs = { clearFilterTriggered: true, shouldTriggerQuery: triggerChange, grid: this._grid, columnFilters: this._columnFilters };
const queryResponse = backendApi.service.processOnFilterChanged(undefined, callbackArgs as FilterChangedArgs);
const query = queryResponse as string;
- const totalItems = this._gridOptions && this._gridOptions.pagination && this._gridOptions.pagination.totalItems || 0;
- executeBackendCallback(backendApi, query, callbackArgs, new Date(), totalItems, this.emitFilterChanged.bind(this));
+ const totalItems = this._gridOptions?.pagination?.totalItems ?? 0;
+ this.backendUtilities?.executeBackendCallback(backendApi, query, callbackArgs, new Date(), totalItems, this.emitFilterChanged.bind(this));
}
// emit an event when filters are all cleared
@@ -639,7 +651,7 @@ export class FilterService {
if (args?.shouldTriggerQuery) {
const query = await backendApi.service.processOnFilterChanged(event, args);
const totalItems = this._gridOptions && this._gridOptions.pagination && this._gridOptions.pagination.totalItems || 0;
- executeBackendCallback(backendApi, query, args, startTime, totalItems, this.emitFilterChanged.bind(this));
+ this.backendUtilities?.executeBackendCallback(backendApi, query, args, startTime, totalItems, this.emitFilterChanged.bind(this), this.httpCancelRequests$);
}
}
@@ -790,7 +802,7 @@ export class FilterService {
if (backendApiService && backendApiService.updateFilters) {
backendApiService.updateFilters(filters, true);
if (triggerBackendQuery) {
- refreshBackendDataset(this._gridOptions);
+ this.backendUtilities?.refreshBackendDataset(this._gridOptions);
}
}
}
@@ -833,7 +845,7 @@ export class FilterService {
if (backendApiService && backendApiService.updateFilters) {
backendApiService.updateFilters(this._columnFilters, true);
if (triggerBackendQuery) {
- refreshBackendDataset(this._gridOptions);
+ this.backendUtilities?.refreshBackendDataset(this._gridOptions);
}
}
} else {
diff --git a/packages/common/src/services/gridState.service.ts b/packages/common/src/services/gridState.service.ts
index 1b634b596..4524ec289 100644
--- a/packages/common/src/services/gridState.service.ts
+++ b/packages/common/src/services/gridState.service.ts
@@ -11,14 +11,14 @@ import {
CurrentPagination,
CurrentRowSelection,
CurrentSorter,
- SlickDataView,
+ EventSubscription,
+ GetSlickEventType,
GridOption,
GridState,
+ SlickDataView,
+ SlickEventHandler,
SlickGrid,
- Subscription,
SlickNamespace,
- GetSlickEventType,
- SlickEventHandler,
} from '../interfaces/index';
import { ExtensionService } from './extension.service';
import { FilterService } from './filter.service';
@@ -34,7 +34,7 @@ export class GridStateService {
private _columns: Column[] = [];
private _currentColumns: CurrentColumn[] = [];
private _grid!: SlickGrid;
- private _subscriptions: Subscription[] = [];
+ private _subscriptions: EventSubscription[] = [];
private _selectedRowDataContextIds: Array | undefined = []; // used with row selection
private _selectedFilteredRowDataContextIds: Array | undefined = []; // used with row selection
private _wasRecheckedAfterPageChange = true; // used with row selection & pagination
diff --git a/packages/common/src/services/index.ts b/packages/common/src/services/index.ts
index 3e8be239b..b7f5e3417 100644
--- a/packages/common/src/services/index.ts
+++ b/packages/common/src/services/index.ts
@@ -1,4 +1,4 @@
-export * from './backend-utilities';
+export * from './backendUtility.service';
export * from './bindingEvent.service';
export * from './collection.service';
export * from './container.service';
@@ -12,6 +12,7 @@ export * from './gridState.service';
export * from './groupingAndColspan.service';
export * from './pagination.service';
export * from './pubSub.service';
+export * from './rxjsFacade';
export * from './shared.service';
export * from './sort.service';
export * from './textExport.service';
diff --git a/packages/common/src/services/pagination.service.ts b/packages/common/src/services/pagination.service.ts
index 66f3f3d77..64e631779 100644
--- a/packages/common/src/services/pagination.service.ts
+++ b/packages/common/src/services/pagination.service.ts
@@ -3,6 +3,7 @@ import { dequal } from 'dequal';
import {
BackendServiceApi,
CurrentPagination,
+ EventSubscription,
GetSlickEventType,
Pagination,
ServicePagination,
@@ -10,11 +11,11 @@ import {
SlickEventHandler,
SlickGrid,
SlickNamespace,
- Subscription
} from '../interfaces/index';
-import { executeBackendProcessesCallback, onBackendError } from './backend-utilities';
+import { BackendUtilityService } from './backendUtility.service';
import { SharedService } from './shared.service';
import { PubSubService } from './pubSub.service';
+import { Observable, RxJsFacade } from './rxjsFacade';
// using external non-typed js libraries
declare const Slick: SlickNamespace;
@@ -32,13 +33,13 @@ export class PaginationService {
private _totalItems = 0;
private _availablePageSizes: number[] = [];
private _paginationOptions!: Pagination;
- private _subscriptions: Subscription[] = [];
+ private _subscriptions: EventSubscription[] = [];
/** SlickGrid Grid object */
grid!: SlickGrid;
/** Constructor */
- constructor(private pubSubService: PubSubService, private sharedService: SharedService) { }
+ constructor(private pubSubService: PubSubService, private sharedService: SharedService, private backendUtilities?: BackendUtilityService, private rxjs?: RxJsFacade) { }
/** Getter of SlickGrid DataView object */
get dataView(): SlickDataView | undefined {
@@ -87,6 +88,10 @@ export class PaginationService {
}
}
+ addRxJsResource(rxjs: RxJsFacade) {
+ this.rxjs = rxjs;
+ }
+
init(grid: SlickGrid, paginationOptions: Pagination, backendServiceApi?: BackendServiceApi) {
this._availablePageSizes = paginationOptions.pageSizes;
this.grid = grid;
@@ -326,11 +331,11 @@ export class PaginationService {
const startTime = new Date();
// run any pre-process, if defined, for example a spinner
- if (this._backendServiceApi && this._backendServiceApi.preProcess) {
+ if (this._backendServiceApi?.preProcess) {
this._backendServiceApi.preProcess();
}
- if (this._backendServiceApi && this._backendServiceApi.process) {
+ if (this._backendServiceApi?.process) {
const query = this._backendServiceApi.service.processOnPaginationChanged(event, { newPage: pageNumber, pageSize: itemsPerPage });
// the processes can be Promises
@@ -338,14 +343,27 @@ export class PaginationService {
if (process instanceof Promise) {
process
.then((processResult: any) => {
- executeBackendProcessesCallback(startTime, processResult, this._backendServiceApi, this._totalItems);
+ this.backendUtilities?.executeBackendProcessesCallback(startTime, processResult, this._backendServiceApi as BackendServiceApi, this._totalItems);
resolve(this.getFullPagination());
})
.catch((error) => {
- onBackendError(error, this._backendServiceApi);
+ this.backendUtilities?.onBackendError(error, this._backendServiceApi as BackendServiceApi);
reject(process);
});
+ } else if (this.rxjs?.isObservable(process)) {
+ this._subscriptions.push(
+ (process as Observable).subscribe(
+ (processResult: any) => {
+ resolve(this.backendUtilities?.executeBackendProcessesCallback(startTime, processResult, this._backendServiceApi as BackendServiceApi, this._totalItems));
+ },
+ (error: any) => {
+ this.backendUtilities?.onBackendError(error, this._backendServiceApi as BackendServiceApi);
+ reject(process);
+ }
+ )
+ );
}
+
this.pubSubService.publish(`onPaginationRefreshed`, this.getFullPagination());
this.pubSubService.publish(`onPaginationChanged`, this.getFullPagination());
}
diff --git a/packages/common/src/services/pubSub.service.ts b/packages/common/src/services/pubSub.service.ts
index a72652ca9..914ff6e27 100644
--- a/packages/common/src/services/pubSub.service.ts
+++ b/packages/common/src/services/pubSub.service.ts
@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
-import { Subscription } from '../interfaces/index';
+import { EventSubscription } from '../interfaces/index';
export abstract class PubSubService {
/**
@@ -18,7 +18,7 @@ export abstract class PubSubService {
* @return possibly a Subscription
*/
// eslint-disable-next-line @typescript-eslint/ban-types
- subscribe(_eventName: string | Function, _callback: (data: T) => void): Subscription | any {
+ subscribe(_eventName: string | Function, _callback: (data: T) => void): EventSubscription | any {
throw new Error('PubSubService "subscribe" method must be implemented');
}
@@ -30,7 +30,7 @@ export abstract class PubSubService {
* @return possibly a Subscription
*/
// eslint-disable-next-line @typescript-eslint/ban-types
- subscribeEvent?(_eventName: string | Function, _callback: (event: CustomEventInit) => void): Subscription | any {
+ subscribeEvent?(_eventName: string | Function, _callback: (event: CustomEventInit) => void): EventSubscription | any {
throw new Error('PubSubService "subscribeEvent" method must be implemented');
}
@@ -44,7 +44,7 @@ export abstract class PubSubService {
}
/** Unsubscribes all subscriptions that currently exists */
- unsubscribeAll(_subscriptions?: Subscription[]): void {
+ unsubscribeAll(_subscriptions?: EventSubscription[]): void {
throw new Error('PubSubService "unsubscribeAll" method must be implemented');
}
}
diff --git a/packages/common/src/services/rxjsFacade.ts b/packages/common/src/services/rxjsFacade.ts
new file mode 100644
index 000000000..b6f14f0f1
--- /dev/null
+++ b/packages/common/src/services/rxjsFacade.ts
@@ -0,0 +1,101 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+
+//
+// -----------------------------------------------------------------------------
+// THIS IS JUST AN EMPTY SHELL, A FACADE TO RxJs without making it a dependency
+// -----------------------------------------------------------------------------
+
+/**
+ * A simple empty shell, a Facade to RxJS to make Slickgrid-Universal usable with RxJS without installing RxJS.
+ * Its sole purpose is to provide access, as an Interface, to use RxJS with Slickgrid-Universal without adding it as a dependency.
+ * The developer who will want to use RxJS will simply have to use the extra `rxjs-observable` package to get going.
+ *
+ * That external `rsjs-observable` package simply implements this RxJsFacade
+ * and is just a very simple and basic RxJS Wrapper package (which will depend on the real RxJS package)
+ */
+export abstract class RxJsFacade {
+ /**
+ * The same Observable instance returned by any call to without a scheduler.
+ * This returns the EMPTY constant from RxJS
+ */
+ get EMPTY(): Observable {
+ throw new Error('RxJS Facade "EMPTY" constant must be implemented');
+ }
+
+ /** Simple method to create an Observable */
+ createObservable(): Observable {
+ throw new Error('RxJS Facade "createObservable" method must be implemented');
+ }
+
+ /** Simple method to create a Subject */
+ createSubject(): Subject {
+ throw new Error('RxJS Facade "createSubject" method must be implemented');
+ }
+
+ firstValueFrom(source: Observable): Promise {
+ throw new Error('RxJS Facade "firstValueFrom" method must be implemented');
+ }
+
+ /** Decides at subscription time which Observable will actually be subscribed. */
+ iif(condition: () => boolean, trueResult?: any, falseResult?: any): Observable {
+ throw new Error('RxJS Facade "iif" method must be implemented');
+ }
+
+ /** Tests to see if the object is an RxJS Observable */
+ isObservable(obj: any): boolean {
+ return false;
+ }
+
+ /** Emits the values emitted by the source Observable until a `notifier` Observable emits a value. */
+ takeUntil(notifier: Observable): any {
+ throw new Error('RxJS Facade "takeUntil" method must be implemented');
+ }
+}
+
+/** A representation of any set of values over any amount of time. This is the most basic building block of RxJS. */
+export abstract class Observable {
+ /** Observable constructor, you can provide a subscribe function that is called when the Observable is initially subscribed to. */
+ constructor(subscribe?: (this: Observable, subscriber: any) => any) {
+ throw new Error('RxJS Observable Facade "constructor" method must be implemented');
+ }
+
+ /** Subscribe to the Observable */
+ subscribe(next?: (value: T) => void, error?: (error: any) => void, complete?: () => void): Subscription {
+ throw new Error('RxJS Observable Facade "subscribe" method must be implemented');
+ }
+
+ /** Pipe an operator function to the Observable */
+ pipe(fns?: any): any {
+ throw new Error('RxJS Observable Facade "pipe" method must be implemented');
+ }
+}
+
+/**
+ * A Subject is a special type of Observable that allows values to be
+ * multicasted to many Observers. Subjects are like EventEmitters.
+ */
+export abstract class Subject extends Observable {
+ next(value: T): void {
+ throw new Error('RxJS Subject "next" method must be implemented');
+ }
+
+ unsubscribe(): void {
+ throw new Error('RxJS Subject "unsubscribe" method must be implemented');
+ }
+}
+
+/**
+ * A Subject is a special type of Observable that allows values to be
+ * multicasted to many Observers. Subjects are like EventEmitters.
+ */
+export abstract class Subscription {
+ /** A function describing how to perform the disposal of resources when the `unsubscribe` method is called. */
+ constructor(unsubscribe?: () => void) {
+ throw new Error('RxJS Subscription Facade "constructor" method must be implemented');
+ }
+
+ /** Disposes the resources held by the subscription. */
+ unsubscribe(): void {
+ throw new Error('RxJS Subscription Facade "unsubscribe" method must be implemented');
+ }
+}
diff --git a/packages/common/src/services/shared.service.ts b/packages/common/src/services/shared.service.ts
index 4564807c4..b6481e377 100644
--- a/packages/common/src/services/shared.service.ts
+++ b/packages/common/src/services/shared.service.ts
@@ -59,11 +59,11 @@ export class SharedService {
this._frozenVisibleColumnId = columnId;
}
- /** Setter to keep the frozen column id for reference if we ever show/hide column from ColumnPicker/GridMenu afterward */
+ /** Setter to know if the columns were ever reordered or not since the grid was created. */
get hasColumnsReordered(): boolean {
return this._hasColumnsReordered;
}
- /** Getter to keep the frozen column id for reference if we ever show/hide column from ColumnPicker/GridMenu afterward */
+ /** Getter to know if the columns were ever reordered or not since the grid was created. */
set hasColumnsReordered(isColumnReordered: boolean) {
this._hasColumnsReordered = isColumnReordered;
}
diff --git a/packages/common/src/services/sort.service.ts b/packages/common/src/services/sort.service.ts
index 7b1869e42..63f20b26d 100644
--- a/packages/common/src/services/sort.service.ts
+++ b/packages/common/src/services/sort.service.ts
@@ -20,11 +20,12 @@ import {
SortDirectionNumber,
SortDirectionString,
} from '../enums/index';
-import { executeBackendCallback, refreshBackendDataset } from './backend-utilities';
+import { BackendUtilityService } from './backendUtility.service';
import { getDescendantProperty, convertHierarchicalViewToParentChildArray } from './utilities';
import { sortByFieldType } from '../sortComparers/sortUtilities';
import { PubSubService } from './pubSub.service';
import { SharedService } from './shared.service';
+import { RxJsFacade, Subject } from './rxjsFacade';
// using external non-typed js libraries
declare const Slick: SlickNamespace;
@@ -35,9 +36,13 @@ export class SortService {
private _dataView!: SlickDataView;
private _grid!: SlickGrid;
private _isBackendGrid = false;
+ private httpCancelRequests$?: Subject; // this will be used to cancel any pending http request
- constructor(private sharedService: SharedService, private pubSubService: PubSubService) {
+ constructor(private sharedService: SharedService, private pubSubService: PubSubService, private backendUtilities?: BackendUtilityService, private rxjs?: RxJsFacade) {
this._eventHandler = new Slick.EventHandler();
+ if (this.rxjs) {
+ this.httpCancelRequests$ = this.rxjs.createSubject();
+ }
}
/** Getter of the SlickGrid Event Handler */
@@ -55,6 +60,10 @@ export class SortService {
return (this._grid && this._grid.getColumns) ? this._grid.getColumns() : [];
}
+ addRxJsResource(rxjs: RxJsFacade) {
+ this.rxjs = rxjs;
+ }
+
/**
* Bind a backend sort (single/multi) hook to the grid
* @param grid SlickGrid Grid object
@@ -305,6 +314,9 @@ export class SortService {
if (this._eventHandler && this._eventHandler.unsubscribeAll) {
this._eventHandler.unsubscribeAll();
}
+ if (this.httpCancelRequests$ && this.rxjs?.isObservable(this.httpCancelRequests$)) {
+ this.httpCancelRequests$.next(); // this cancels any pending http requests
+ }
}
/** Process the initial sort, typically it will sort ascending by the column that has the Tree Data unless user specifies a different initialSort */
@@ -355,7 +367,7 @@ export class SortService {
// query backend
const query = backendApi.service.processOnSortChanged(event, args);
const totalItems = gridOptions && gridOptions.pagination && gridOptions.pagination.totalItems || 0;
- executeBackendCallback(backendApi, query, args, startTime, totalItems, this.emitSortChanged.bind(this));
+ this.backendUtilities?.executeBackendCallback(backendApi, query, args, startTime, totalItems, this.emitSortChanged.bind(this), this.httpCancelRequests$);
}
/** When a Sort Changes on a Local grid (JSON dataset) */
@@ -504,7 +516,7 @@ export class SortService {
if (backendApiService && backendApiService.updateSorters) {
backendApiService.updateSorters(undefined, sorters);
if (triggerBackendQuery) {
- refreshBackendDataset(this._gridOptions);
+ this.backendUtilities?.refreshBackendDataset(this._gridOptions);
}
}
} else {
diff --git a/packages/common/src/services/utilities.ts b/packages/common/src/services/utilities.ts
index 99bd1d838..70f1b4bde 100644
--- a/packages/common/src/services/utilities.ts
+++ b/packages/common/src/services/utilities.ts
@@ -4,7 +4,8 @@ const DOMPurify = DOMPurify_; // patch to fix rollup to work
const moment = (moment_ as any)['default'] || moment_; // patch to fix rollup "moment has no default export" issue, document here https://github.com/rollup/rollup/issues/670
import { FieldType, OperatorString, OperatorType } from '../enums/index';
-import { GridOption } from '../interfaces/index';
+import { EventSubscription, GridOption } from '../interfaces/index';
+import { Observable, RxJsFacade, Subject, Subscription } from './rxjsFacade';
/**
* Add an item to an array only when the item does not exists, when the item is an object we will be using their "id" to compare
@@ -48,6 +49,28 @@ export function arrayRemoveItemByIndex(array: T[], index: number): T[] {
return array.filter((_el: T, i: number) => index !== i);
}
+/**
+ * Try casting an input of type Promise | Observable into a Promise type.
+ * @param object which could be of type Promise or Observable
+ * @param fromServiceName string representing the caller service name and will be used if we throw a casting problem error
+ */
+export function castObservableToPromise(rxjs: RxJsFacade, input: Promise | Observable | Subject, fromServiceName = ''): Promise {
+ let promise: any = input;
+
+ if (input instanceof Promise) {
+ // if it's already a Promise then return it
+ return input;
+ } else if (rxjs.isObservable(input)) {
+ promise = rxjs.firstValueFrom(input);
+ }
+
+ if (!(promise instanceof Promise)) {
+ throw new Error(`Something went wrong, Slickgrid-Universal ${fromServiceName} is not able to convert the Observable into a Promise.`);
+ }
+
+ return promise;
+}
+
/**
* Convert a flat array (with "parentId" references) into a hierarchical dataset structure (where children are array(s) inside their parent objects)
* @param flatArray input array (flat dataset)
@@ -972,6 +995,24 @@ export function toSnakeCase(inputStr: string): string {
return inputStr;
}
+/**
+ * Unsubscribe all Subscriptions
+ * It will return an empty array if it all went well
+ * @param subscriptions
+ */
+export function unsubscribeAll(subscriptions: Array): Array {
+ if (Array.isArray(subscriptions)) {
+ while (subscriptions.length > 0) {
+ const subscription = subscriptions.pop();
+ if (subscription?.unsubscribe) {
+ subscription.unsubscribe();
+ }
+ }
+ }
+
+ return subscriptions;
+}
+
/**
* Takes an input array and makes sure the array has unique values by removing duplicates
* @param array input with possible duplicates
diff --git a/packages/excel-export/src/excelExport.service.ts b/packages/excel-export/src/excelExport.service.ts
index 782f7154e..3b13ac16a 100644
--- a/packages/excel-export/src/excelExport.service.ts
+++ b/packages/excel-export/src/excelExport.service.ts
@@ -59,7 +59,7 @@ export class ExcelExportService implements ExternalResource, BaseExcelExportServ
private _workbook!: ExcelWorkbook;
/** ExcelExportService class name which is use to find service instance in the external registered services */
- className = 'ExcelExportService';
+ readonly className = 'ExcelExportService';
constructor() { }
diff --git a/packages/graphql/src/interfaces/graphqlServiceApi.interface.ts b/packages/graphql/src/interfaces/graphqlServiceApi.interface.ts
index 3e0e7d7cf..d9c4481e8 100644
--- a/packages/graphql/src/interfaces/graphqlServiceApi.interface.ts
+++ b/packages/graphql/src/interfaces/graphqlServiceApi.interface.ts
@@ -1,4 +1,4 @@
-import { BackendServiceApi } from '@slickgrid-universal/common';
+import { BackendServiceApi, Observable } from '@slickgrid-universal/common';
import { GraphqlResult } from './graphqlResult.interface';
import { GraphqlPaginatedResult } from './graphqlPaginatedResult.interface';
@@ -13,10 +13,10 @@ export interface GraphqlServiceApi extends BackendServiceApi {
service: GraphqlService;
/** On init (or on page load), what action to perform? */
- onInit?: (query: string) => Promise;
+ onInit?: (query: string) => Promise | Observable;
- /** On Processing, we get the query back from the service, and we need to provide a Promise. For example: this.http.get(myGraphqlUrl) */
- process: (query: string) => Promise;
+ /** On Processing, we get the query back from the service, and we need to provide a Promise/Observable. For example: this.http.get(myGraphqlUrl) */
+ process: (query: string) => Promise | Observable;
/** After executing the query, what action to perform? For example, stop the spinner */
postProcess?: (response: GraphqlResult | GraphqlPaginatedResult) => void;
diff --git a/packages/rxjs-observable/README.md b/packages/rxjs-observable/README.md
new file mode 100644
index 000000000..9ea05d424
--- /dev/null
+++ b/packages/rxjs-observable/README.md
@@ -0,0 +1,52 @@
+[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
+[![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/)
+[![lerna](https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg)](https://lerna.js.org/)
+[![npm](https://img.shields.io/npm/v/@slickgrid-universal/rxjs-observable.svg?color=forest)](https://www.npmjs.com/package/@slickgrid-universal/rxjs-observable)
+[![npm](https://img.shields.io/npm/dy/@slickgrid-universal/rxjs-observable?color=forest)](https://www.npmjs.com/package/@slickgrid-universal/rxjs-observable)
+
+[![Actions Status](https://github.com/ghiscoding/slickgrid-universal/workflows/CI%20Build/badge.svg)](https://github.com/ghiscoding/slickgrid-universal/actions)
+[![Cypress.io](https://img.shields.io/badge/tested%20with-Cypress-04C38E.svg)](https://www.cypress.io/)
+[![jest](https://jestjs.io/img/jest-badge.svg)](https://github.com/facebook/jest)
+[![codecov](https://codecov.io/gh/ghiscoding/slickgrid-universal/branch/master/graph/badge.svg)](https://codecov.io/gh/ghiscoding/slickgrid-universal)
+
+## RxJS Observable Wrapper
+#### @slickgrid-universal/rxjs-observable
+
+An RxJS Observable Service Wrapper to make it possible to use RxJS with Slickgrid-Universal (with a Backend Service like OData/GraphQL). By default any Backend Service will be using Promises unless we use this RxJS Observable package.
+
+This package is simply a bridge, a facade, to make it possible to use RxJS without adding RxJS to the `@slickgrid-universal/common` list of dependencies, so RxJS is a dependency of this package without being a dependency of the common (core) package, This will avoid adding dependencies not everyone need and won't clutter clutterin the common package (the common package will simply use an empty shell, an empty interface, without requiring to install RxJS at all, we do however have full unit tests coverage for all of that).
+
+### External Dependencies
+- [RxJS 7](https://github.com/ReactiveX/RxJS)
+
+### Installation
+Follow the instruction provided in the main [README](https://github.com/ghiscoding/slickgrid-universal#installation), you can see a demo by looking at the [GitHub Demo](https://ghiscoding.github.io/slickgrid-universal) page and click on "Export to CSV" from the Grid Menu (aka hamburger menu).
+
+### Usage
+In order to use the Service, you will need to register it in your grid options via the `registerExternalResources` as shown below and of course install RxJS itself (this package requires RxJS 7).
+
+##### ViewModel
+```ts
+import { GridOdataService } from '@slickgrid-universal/odata';
+import { RxJsResource } from '@slickgrid-universal/rxjs-observable';
+
+export class MyExample {
+ gridOptions: GridOption;
+
+ prepareGrid {
+ this.gridOptions = {
+ // ...
+ registerExternalResources: [new RxJsResource()],
+
+ // you will most probably use it with a Backend Service, for example with OData
+ backendServiceApi: {
+ service: new GridOdataService(),
+ preProcess: () => this.displaySpinner(true),
+
+ // assuming your Http call is with an RxJS Observable
+ process: (query) => this.getAllCustomers$(query),
+ } as OdataServiceApi,
+ };
+ }
+}
+```
diff --git a/packages/rxjs-observable/package.json b/packages/rxjs-observable/package.json
new file mode 100644
index 000000000..a7bf031cd
--- /dev/null
+++ b/packages/rxjs-observable/package.json
@@ -0,0 +1,50 @@
+{
+ "name": "@slickgrid-universal/rxjs-observable",
+ "version": "0.11.2",
+ "description": "RxJS Observable Wrapper",
+ "main": "dist/commonjs/index.js",
+ "browser": "src/index.ts",
+ "module": "dist/esm/index.js",
+ "types": "dist/commonjs/index.d.ts",
+ "typings": "dist/commonjs/index.d.ts",
+ "publishConfig": {
+ "access": "public"
+ },
+ "directories": {
+ "src": "src"
+ },
+ "scripts": {
+ "build": "cross-env tsc --build",
+ "postbuild": "npm-run-all bundle:commonjs",
+ "build:watch": "cross-env tsc --incremental --watch",
+ "dev": "run-s build sass:build sass:copy",
+ "dev:watch": "run-p build:watch",
+ "bundle": "run-p bundle:commonjs bundle:esm",
+ "bundle:commonjs": "tsc --project tsconfig.bundle.json --outDir dist/commonjs --module commonjs",
+ "bundle:esm": "cross-env tsc --project tsconfig.bundle.json --outDir dist/esm --module esnext --target es2018",
+ "prebundle": "npm-run-all delete:dist",
+ "delete:dist": "cross-env rimraf --maxBusyTries=10 dist",
+ "package:add-browser-prop": "cross-env node ../change-package-browser.js --add-browser=true --folder-name=empty-warning-component",
+ "package:remove-browser-prop": "cross-env node ../change-package-browser.js --remove-browser=true --folder-name=empty-warning-component"
+ },
+ "author": "Ghislain B.",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.15.0",
+ "npm": ">=6.14.8"
+ },
+ "browserslist": [
+ "last 2 version",
+ "> 1%",
+ "not dead"
+ ],
+ "dependencies": {
+ "@slickgrid-universal/common": "^0.11.2",
+ "rxjs": "next"
+ },
+ "devDependencies": {
+ "cross-env": "^7.0.3",
+ "npm-run-all": "^4.1.5",
+ "rimraf": "^3.0.2"
+ }
+}
\ No newline at end of file
diff --git a/packages/rxjs-observable/src/.npmignore b/packages/rxjs-observable/src/.npmignore
new file mode 100644
index 000000000..083a3140f
--- /dev/null
+++ b/packages/rxjs-observable/src/.npmignore
@@ -0,0 +1,2 @@
+index.ts
+**/*.*
diff --git a/packages/rxjs-observable/src/index.spec.ts b/packages/rxjs-observable/src/index.spec.ts
new file mode 100644
index 000000000..7617e9210
--- /dev/null
+++ b/packages/rxjs-observable/src/index.spec.ts
@@ -0,0 +1,11 @@
+import * as entry from './index';
+
+describe('Testing library entry point', () => {
+ it('should have an index entry point defined', () => {
+ expect(entry).toBeTruthy();
+ });
+
+ it('should have all exported object defined', () => {
+ expect(typeof entry.RxJsResource).toBe('function');
+ });
+});
diff --git a/packages/rxjs-observable/src/index.ts b/packages/rxjs-observable/src/index.ts
new file mode 100644
index 000000000..20ec82fde
--- /dev/null
+++ b/packages/rxjs-observable/src/index.ts
@@ -0,0 +1 @@
+export * from './rxjs.resource';
diff --git a/packages/rxjs-observable/src/rxjs-resource.spec.ts b/packages/rxjs-observable/src/rxjs-resource.spec.ts
new file mode 100644
index 000000000..debea7a2c
--- /dev/null
+++ b/packages/rxjs-observable/src/rxjs-resource.spec.ts
@@ -0,0 +1,49 @@
+import { EMPTY, isObservable, Observable, Subject } from 'rxjs';
+import { RxJsResource } from './rxjs.resource';
+
+describe('RxJs Resource', () => {
+ let service: RxJsResource;
+
+ beforeEach(() => {
+ service = new RxJsResource();
+ });
+
+ it('should create the service', () => {
+ expect(service).toBeTruthy();
+ expect(service.className).toBe('RxJsResource');
+ });
+
+ it('should be able to create an RxJS Observable', () => {
+ const observable = service.createObservable();
+ expect(observable instanceof Observable).toBeTruthy();
+ expect(service.isObservable(observable)).toBeTruthy();
+ });
+
+ it('should be able to create an RxJS Subject', () => {
+ const subject = service.createSubject();
+ expect(subject instanceof Subject).toBeTruthy();
+ });
+
+ it('should be able to retrieve the RxJS EMPTY constant from the service getter', () => {
+ expect(service.EMPTY).toEqual(EMPTY);
+ });
+
+ it('should be able to execute the "iif" method and expect an Observable returned', () => {
+ const observable = service.createObservable();
+ const iifOutput = service.iif(() => isObservable(observable));
+ expect(iifOutput instanceof Observable).toBeTruthy();
+ });
+
+ it('should be able to execute the "firstValueFrom" method and expect an Observable returned', () => {
+ const observable = service.createObservable();
+ const output = service.firstValueFrom(observable);
+ expect(output instanceof Promise).toBeTruthy();
+ });
+
+ it('should be able to execute the "takeUntil" method and expect an Observable returned', () => {
+ const observable = service.createObservable();
+ const iifOutput = service.iif(() => isObservable(observable));
+ const output = observable.pipe(service.takeUntil(iifOutput));
+ expect(output instanceof Observable).toBeTruthy();
+ });
+});
diff --git a/packages/rxjs-observable/src/rxjs.resource.ts b/packages/rxjs-observable/src/rxjs.resource.ts
new file mode 100644
index 000000000..16fd9c902
--- /dev/null
+++ b/packages/rxjs-observable/src/rxjs.resource.ts
@@ -0,0 +1,43 @@
+import { RxJsFacade } from '@slickgrid-universal/common';
+import { EMPTY, iif, isObservable, firstValueFrom, Observable, Subject } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
+
+export class RxJsResource implements RxJsFacade {
+ readonly className = 'RxJsResource';
+
+ /**
+ * The same Observable instance returned by any call to without a scheduler.
+ * This returns the EMPTY constant from RxJS
+ */
+ get EMPTY(): Observable {
+ return EMPTY;
+ }
+
+ /** Simple method to create an Observable */
+ createObservable(): Observable {
+ return new Observable();
+ }
+
+ /** Simple method to create an Subject */
+ createSubject(): Subject {
+ return new Subject();
+ }
+
+ firstValueFrom(source: Observable): Promise {
+ return firstValueFrom(source);
+ }
+
+ iif(condition: () => boolean, trueResult?: any, falseResult?: any): Observable {
+ return iif(condition, trueResult, falseResult);
+ }
+
+ /** Tests to see if the object is an RxJS Observable */
+ isObservable(obj: any): boolean {
+ return isObservable(obj);
+ }
+
+ /** Emits the values emitted by the source Observable until a `notifier` Observable emits a value. */
+ takeUntil(notifier: Observable): any {
+ return takeUntil(notifier);
+ }
+}
\ No newline at end of file
diff --git a/packages/rxjs-observable/tsconfig.bundle.json b/packages/rxjs-observable/tsconfig.bundle.json
new file mode 100644
index 000000000..7c6f72a9a
--- /dev/null
+++ b/packages/rxjs-observable/tsconfig.bundle.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../tsconfig.bundle.json",
+ "compilerOptions": {
+ "typeRoots": [
+ "../typings",
+ "../../node_modules/@types"
+ ],
+ "outDir": "dist/commonjs"
+ },
+ "include": [
+ "../typings",
+ "**/*"
+ ]
+}
\ No newline at end of file
diff --git a/packages/rxjs-observable/tsconfig.json b/packages/rxjs-observable/tsconfig.json
new file mode 100644
index 000000000..277dfa292
--- /dev/null
+++ b/packages/rxjs-observable/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "extends": "../tsconfig.base.json",
+ "compileOnSave": false,
+ "compilerOptions": {
+ "rootDir": "src",
+ "declarationDir": "dist/esm",
+ "outDir": "dist/esm",
+ "typeRoots": [
+ "typings"
+ ]
+ },
+ "exclude": [
+ "dist",
+ "node_modules",
+ "**/*.spec.ts"
+ ],
+ "filesGlob": [
+ "./src/**/*.ts"
+ ],
+ "include": [
+ "src/**/*.ts",
+ "typings/**/*.ts"
+ ]
+}
\ No newline at end of file
diff --git a/packages/text-export/src/textExport.service.ts b/packages/text-export/src/textExport.service.ts
index 3fa290e3c..4984f7c08 100644
--- a/packages/text-export/src/textExport.service.ts
+++ b/packages/text-export/src/textExport.service.ts
@@ -57,7 +57,7 @@ export class TextExportService implements ExternalResource, BaseTextExportServic
private _translaterService: TranslaterService | undefined;
/** ExcelExportService class name which is use to find service instance in the external registered services */
- className = 'TextExportService';
+ readonly className = 'TextExportService';
constructor() { }
diff --git a/packages/vanilla-bundle/dist-grid-bundle-zip/slickgrid-vanilla-bundle.zip b/packages/vanilla-bundle/dist-grid-bundle-zip/slickgrid-vanilla-bundle.zip
index ef6add617..28da1215c 100644
Binary files a/packages/vanilla-bundle/dist-grid-bundle-zip/slickgrid-vanilla-bundle.zip and b/packages/vanilla-bundle/dist-grid-bundle-zip/slickgrid-vanilla-bundle.zip differ
diff --git a/packages/vanilla-bundle/package.json b/packages/vanilla-bundle/package.json
index 0727d57f5..b6a3e82e0 100644
--- a/packages/vanilla-bundle/package.json
+++ b/packages/vanilla-bundle/package.json
@@ -63,6 +63,6 @@
"npm-run-all": "^4.1.5",
"rimraf": "^3.0.2",
"ts-loader": "^8.0.18",
- "webpack": "^5.27.0"
+ "webpack": "^5.28.0"
}
}
\ No newline at end of file
diff --git a/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid.spec.ts b/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid.spec.ts
index 062458a63..a498a12ba 100644
--- a/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid.spec.ts
+++ b/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid.spec.ts
@@ -1,5 +1,7 @@
import 'jest-extended';
+import { of, throwError } from 'rxjs';
import {
+ BackendUtilityService,
Column,
CollectionService,
ColumnFilters,
@@ -41,7 +43,6 @@ import {
} from '@slickgrid-universal/common';
import { GraphqlService, GraphqlPaginatedResult, GraphqlServiceApi, GraphqlServiceOption } from '@slickgrid-universal/graphql';
import { SlickCompositeEditorComponent } from '@slickgrid-universal/composite-editor-component';
-import * as backendUtilities from '@slickgrid-universal/common/dist/commonjs/services/backend-utilities';
import * as utilities from '@slickgrid-universal/common/dist/commonjs/services/utilities';
import * as slickVanillaUtilities from '../slick-vanilla-utilities';
@@ -53,17 +54,12 @@ import { HttpStub } from '../../../../../test/httpClientStub';
import { MockSlickEvent, MockSlickEventHandler } from '../../../../../test/mockSlickEvent';
import { ResizerService } from '../../services/resizer.service';
import { UniversalContainerService } from '../../services/universalContainer.service';
+import { RxJsResourceStub } from '../../../../../test/rxjsResourceStub';
jest.mock('../../services/textExport.service');
-const mockExecuteBackendProcess = jest.fn();
-const mockRefreshBackendDataset = jest.fn();
-const mockBackendError = jest.fn();
const mockConvertParentChildArray = jest.fn();
const mockAutoAddCustomEditorFormatter = jest.fn();
-(backendUtilities.executeBackendProcessesCallback as any) = mockExecuteBackendProcess;
-(backendUtilities.refreshBackendDataset as any) = mockRefreshBackendDataset;
-(backendUtilities.onBackendError as any) = mockBackendError;
(slickVanillaUtilities.autoAddEditorFormatterToColumnsWithEditor as any) = mockAutoAddCustomEditorFormatter;
declare const Slick: any;
@@ -102,6 +98,15 @@ const mockGraphqlService = {
updatePagination: jest.fn(),
} as unknown as GraphqlService;
+const backendUtilityServiceStub = {
+ addRxJsResource: jest.fn(),
+ executeBackendProcessesCallback: jest.fn(),
+ executeBackendCallback: jest.fn(),
+ onBackendError: jest.fn(),
+ refreshBackendDataset: jest.fn(),
+} as unknown as BackendUtilityService;
+
+
const collectionServiceStub = {
filterCollection: jest.fn(),
singleFilterCollection: jest.fn(),
@@ -109,6 +114,7 @@ const collectionServiceStub = {
} as unknown as CollectionService;
const filterServiceStub = {
+ addRxJsResource: jest.fn(),
clearFilters: jest.fn(),
dispose: jest.fn(),
init: jest.fn(),
@@ -143,6 +149,7 @@ const gridStateServiceStub = {
const paginationServiceStub = {
totalItems: 0,
+ addRxJsResource: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
getFullPagination: jest.fn(),
@@ -161,6 +168,7 @@ Object.defineProperty(paginationServiceStub, 'totalItems', {
});
const sortServiceStub = {
+ addRxJsResource: jest.fn(),
bindBackendOnSort: jest.fn(),
bindLocalOnSort: jest.fn(),
dispose: jest.fn(),
@@ -323,6 +331,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', ()
dataset,
undefined,
{
+ backendUtilityService: backendUtilityServiceStub,
collectionService: collectionServiceStub,
eventPubSubService,
extensionService: extensionServiceStub,
@@ -702,6 +711,25 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', ()
});
});
+ it('should be able to load async editors with an Observable', (done) => {
+ const mockCollection = ['male', 'female'];
+ const mockColDefs = [{ id: 'gender', field: 'gender', editor: { model: Editors.text, collectionAsync: of(mockCollection) } }] as Column[];
+ const getColSpy = jest.spyOn(mockGrid, 'getColumns').mockReturnValue(mockColDefs);
+
+ const rxjsMock = new RxJsResourceStub();
+ component.gridOptions = { registerExternalResources: [rxjsMock] } as unknown as GridOption;
+ component.initialization(divContainer, slickEventHandler);
+ component.columnDefinitions = mockColDefs;
+
+ setTimeout(() => {
+ expect(getColSpy).toHaveBeenCalled();
+ expect(component.columnDefinitions[0].editor!.collection).toEqual(mockCollection);
+ expect(component.columnDefinitions[0].internalColumnEditor!.collection).toEqual(mockCollection);
+ expect(component.columnDefinitions[0].internalColumnEditor!.model).toEqual(Editors.text);
+ done();
+ });
+ });
+
it('should throw an error when Fetch Promise response bodyUsed is true', (done) => {
const consoleSpy = jest.spyOn(global.console, 'warn').mockReturnValue();
const mockCollection = ['male', 'female'];
@@ -921,6 +949,24 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', ()
expect(component.registeredResources[0] instanceof TextExportService).toBeTrue();
});
+ it('should add RxJS resource to all necessary Services when RxJS external resource is registered', () => {
+ const rxjsMock = new RxJsResourceStub();
+ const backendUtilitySpy = jest.spyOn(backendUtilityServiceStub, 'addRxJsResource');
+ const filterServiceSpy = jest.spyOn(filterServiceStub, 'addRxJsResource');
+ const sortServiceSpy = jest.spyOn(sortServiceStub, 'addRxJsResource');
+ const paginationServiceSpy = jest.spyOn(paginationServiceStub, 'addRxJsResource');
+
+ component.gridOptions = { registerExternalResources: [rxjsMock] } as unknown as GridOption;
+ component.initialization(divContainer, slickEventHandler);
+
+ expect(backendUtilitySpy).toHaveBeenCalled();
+ expect(filterServiceSpy).toHaveBeenCalled();
+ expect(sortServiceSpy).toHaveBeenCalled();
+ expect(paginationServiceSpy).toHaveBeenCalled();
+ expect(component.registeredResources.length).toBe(4); // RxJsResourceStub, GridService, GridStateService, SlickEmptyCompositeEditorComponent
+ expect(component.registeredResources[0] instanceof RxJsResourceStub).toBeTrue();
+ });
+
it('should destroy customElement and its DOM element when requested', () => {
const spy = jest.spyOn(component, 'emptyGridContainerElm');
@@ -1145,7 +1191,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', ()
expect(refreshSpy).toHaveBeenCalledWith(mockData);
});
- it('should execute the process method on initialization when "executeProcessCommandOnInit" is set as a backend service options with Pagination enabled', (done) => {
+ it('should execute the process method on initialization when "executeProcessCommandOnInit" is set as a backend service options with a Promise and Pagination enabled', (done) => {
const now = new Date();
const query = `query { users (first:20,offset:0) { totalCount, nodes { id,name,gender,company } } }`;
const processResult = {
@@ -1155,6 +1201,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', ()
const promise = new Promise(resolve => setTimeout(() => resolve(processResult), 1));
const processSpy = jest.spyOn(component.gridOptions.backendServiceApi, 'process').mockReturnValue(promise);
jest.spyOn(component.gridOptions.backendServiceApi.service, 'buildQuery').mockReturnValue(query);
+ const backendExecuteSpy = jest.spyOn(backendUtilityServiceStub, 'executeBackendProcessesCallback');
component.gridOptions.backendServiceApi.service.options = { executeProcessCommandOnInit: true };
component.initialization(divContainer, slickEventHandler);
@@ -1162,7 +1209,31 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', ()
expect(processSpy).toHaveBeenCalled();
setTimeout(() => {
- expect(mockExecuteBackendProcess).toHaveBeenCalledWith(expect.toBeDate(), processResult, component.gridOptions.backendServiceApi, 0);
+ expect(backendExecuteSpy).toHaveBeenCalledWith(expect.toBeDate(), processResult, component.gridOptions.backendServiceApi, 0);
+ done();
+ }, 5);
+ });
+
+ it('should execute the process method on initialization when "executeProcessCommandOnInit" is set as a backend service options with an Observable and Pagination enabled', (done) => {
+ const now = new Date();
+ const rxjsMock = new RxJsResourceStub();
+ const query = `query { users (first:20,offset:0) { totalCount, nodes { id,name,gender,company } } }`;
+ const processResult = {
+ data: { users: { nodes: [] }, pageInfo: { hasNextPage: true }, totalCount: 0 },
+ metrics: { startTime: now, endTime: now, executionTime: 0, totalItemCount: 0 }
+ };
+ const processSpy = jest.spyOn((component.gridOptions as any).backendServiceApi, 'process').mockReturnValue(of(processResult));
+ jest.spyOn((component.gridOptions as any).backendServiceApi.service, 'buildQuery').mockReturnValue(query);
+ const backendExecuteSpy = jest.spyOn(backendUtilityServiceStub, 'executeBackendProcessesCallback');
+
+ component.gridOptions.registerExternalResources = [rxjsMock];
+ component.gridOptions.backendServiceApi.service.options = { executeProcessCommandOnInit: true };
+ component.initialization(divContainer, slickEventHandler);
+
+ expect(processSpy).toHaveBeenCalled();
+
+ setTimeout(() => {
+ expect(backendExecuteSpy).toHaveBeenCalledWith(expect.toBeDate(), processResult, component.gridOptions.backendServiceApi, 0);
done();
}, 5);
});
@@ -1177,6 +1248,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', ()
const promise = new Promise(resolve => setTimeout(() => resolve(processResult), 1));
const processSpy = jest.spyOn(component.gridOptions.backendServiceApi, 'process').mockReturnValue(promise);
jest.spyOn(component.gridOptions.backendServiceApi.service, 'buildQuery').mockReturnValue(query);
+ const backendExecuteSpy = jest.spyOn(backendUtilityServiceStub, 'executeBackendProcessesCallback');
component.gridOptions.backendServiceApi.service.options = { executeProcessCommandOnInit: true };
component.initialization(divContainer, slickEventHandler);
@@ -1184,7 +1256,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', ()
expect(processSpy).toHaveBeenCalled();
setTimeout(() => {
- expect(mockExecuteBackendProcess).toHaveBeenCalledWith(expect.toBeDate(), processResult, component.gridOptions.backendServiceApi, 0);
+ expect(backendExecuteSpy).toHaveBeenCalledWith(expect.toBeDate(), processResult, component.gridOptions.backendServiceApi, 0);
done();
}, 5);
});
@@ -1206,6 +1278,26 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', ()
done();
});
});
+
+ it('should throw an error when the process method on initialization when "executeProcessCommandOnInit" is set as a backend service options from an Observable', (done) => {
+ const mockError = { error: '404' };
+ const rxjsMock = new RxJsResourceStub();
+ const query = `query { users (first:20,offset:0) { totalCount, nodes { id,name,gender,company } } }`;
+ const processSpy = jest.spyOn((component.gridOptions as any).backendServiceApi, 'process').mockReturnValue(throwError(mockError));
+ jest.spyOn((component.gridOptions as any).backendServiceApi.service, 'buildQuery').mockReturnValue(query);
+ const backendErrorSpy = jest.spyOn(backendUtilityServiceStub, 'onBackendError');
+
+ component.gridOptions.registerExternalResources = [rxjsMock];
+ component.gridOptions.backendServiceApi.service.options = { executeProcessCommandOnInit: true };
+ component.initialization(divContainer, slickEventHandler);
+
+ expect(processSpy).toHaveBeenCalled();
+
+ setTimeout(() => {
+ expect(backendErrorSpy).toHaveBeenCalledWith(mockError, component.gridOptions.backendServiceApi);
+ done();
+ });
+ });
});
describe('bindDifferentHooks private method called by "attached"', () => {
@@ -1833,6 +1925,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', ()
});
it('should call the backend service API to refresh the dataset', (done) => {
+ const backendRefreshSpy = jest.spyOn(backendUtilityServiceStub, 'refreshBackendDataset');
component.gridOptions.enablePagination = true;
component.gridOptions.backendServiceApi = {
service: mockGraphqlService as any,
@@ -1844,7 +1937,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', ()
eventPubSubService.publish('onPaginationVisibilityChanged', { visible: false });
setTimeout(() => {
- expect(mockRefreshBackendDataset).toHaveBeenCalled();
+ expect(backendRefreshSpy).toHaveBeenCalled();
expect(component.showPagination).toBeFalsy();
done();
});
@@ -1980,6 +2073,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor with
dataset,
hierarchicalDataset,
{
+ backendUtilityService: backendUtilityServiceStub,
collectionService: collectionServiceStub,
eventPubSubService,
extensionService: extensionServiceStub,
@@ -2051,6 +2145,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor with
dataset,
null,
{
+ backendUtilityService: backendUtilityServiceStub,
collectionService: collectionServiceStub,
eventPubSubService,
extensionService: extensionServiceStub,
diff --git a/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts b/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts
index 59c9aa849..69a8be62d 100644
--- a/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts
+++ b/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts
@@ -9,6 +9,7 @@ import 'slickgrid/plugins/slick.resizer';
import {
AutoCompleteEditor,
BackendServiceApi,
+ BackendServiceOption,
Column,
ColumnEditor,
DataViewOption,
@@ -17,23 +18,19 @@ import {
EventNamingStyle,
GlobalGridOptions,
GridOption,
+ GridStateType,
Metrics,
MetricTexts,
+ Pagination,
+ SelectEditor,
+ ServicePagination,
SlickDataView,
SlickEventHandler,
SlickGrid,
SlickGroupItemMetadataProvider,
SlickNamespace,
- TreeDataOption,
- executeBackendProcessesCallback,
- GridStateType,
- BackendServiceOption,
- onBackendError,
- refreshBackendDataset,
- Pagination,
- SelectEditor,
- ServicePagination,
Subscription,
+ TreeDataOption,
// extensions
AutoTooltipExtension,
@@ -52,6 +49,7 @@ import {
RowSelectionExtension,
// services
+ BackendUtilityService,
CollectionService,
ExtensionService,
FilterFactory,
@@ -60,8 +58,10 @@ import {
GridService,
GridStateService,
GroupingAndColspanService,
+ Observable,
PaginationService,
RowMoveManagerExtension,
+ RxJsFacade,
SharedService,
SortService,
SlickgridConfig,
@@ -125,14 +125,17 @@ export class SlickVanillaGridBundle {
extensionUtility!: ExtensionUtility;
// services
+ backendUtilityService!: BackendUtilityService;
collectionService!: CollectionService;
extensionService!: ExtensionService;
+ filterFactory!: FilterFactory;
filterService!: FilterService;
gridEventService!: GridEventService;
gridService!: GridService;
gridStateService!: GridStateService;
groupingService!: GroupingAndColspanService;
paginationService!: PaginationService;
+ rxjs?: RxJsFacade;
sharedService!: SharedService;
sortService!: SortService;
translaterService: TranslaterService | undefined;
@@ -266,6 +269,7 @@ export class SlickVanillaGridBundle {
dataset?: any[],
hierarchicalDataset?: any[],
services?: {
+ backendUtilityService?: BackendUtilityService,
collectionService?: CollectionService,
eventPubSubService?: EventPubSubService,
extensionService?: ExtensionService,
@@ -277,6 +281,7 @@ export class SlickVanillaGridBundle {
groupingAndColspanService?: GroupingAndColspanService,
paginationService?: PaginationService,
resizerService?: ResizerService,
+ rxjs?: RxJsFacade,
sharedService?: SharedService,
sortService?: SortService,
treeDataService?: TreeDataService,
@@ -313,17 +318,18 @@ export class SlickVanillaGridBundle {
this._eventPubSubService = services?.eventPubSubService ?? new EventPubSubService(gridParentContainerElm);
this._eventPubSubService.eventNamingStyle = this._gridOptions?.eventNamingStyle ?? EventNamingStyle.camelCase;
- this.gridEventService = services?.gridEventService ?? new GridEventService();
const slickgridConfig = new SlickgridConfig();
+ this.backendUtilityService = services?.backendUtilityService ?? new BackendUtilityService();
+ this.gridEventService = services?.gridEventService ?? new GridEventService();
this.sharedService = services?.sharedService ?? new SharedService();
this.collectionService = services?.collectionService ?? new CollectionService(this.translaterService);
this.extensionUtility = services?.extensionUtility ?? new ExtensionUtility(this.sharedService, this.translaterService);
- const filterFactory = new FilterFactory(slickgridConfig, this.translaterService, this.collectionService);
- this.filterService = services?.filterService ?? new FilterService(filterFactory, this._eventPubSubService, this.sharedService);
+ this.filterFactory = new FilterFactory(slickgridConfig, this.translaterService, this.collectionService);
+ this.filterService = services?.filterService ?? new FilterService(this.filterFactory, this._eventPubSubService, this.sharedService, this.backendUtilityService);
this.resizerService = services?.resizerService ?? new ResizerService(this._eventPubSubService);
- this.sortService = services?.sortService ?? new SortService(this.sharedService, this._eventPubSubService);
+ this.sortService = services?.sortService ?? new SortService(this.sharedService, this._eventPubSubService, this.backendUtilityService);
this.treeDataService = services?.treeDataService ?? new TreeDataService(this.sharedService);
- this.paginationService = services?.paginationService ?? new PaginationService(this._eventPubSubService, this.sharedService);
+ this.paginationService = services?.paginationService ?? new PaginationService(this._eventPubSubService, this.sharedService, this.backendUtilityService);
// extensions
const autoTooltipExtension = new AutoTooltipExtension(this.sharedService);
@@ -333,7 +339,7 @@ export class SlickVanillaGridBundle {
const columnPickerExtension = new ColumnPickerExtension(this.extensionUtility, this.sharedService);
const checkboxExtension = new CheckboxSelectorExtension(this.sharedService);
const draggableGroupingExtension = new DraggableGroupingExtension(this.extensionUtility, this.sharedService);
- const gridMenuExtension = new GridMenuExtension(this.extensionUtility, this.filterService, this.sharedService, this.sortService, this.translaterService);
+ const gridMenuExtension = new GridMenuExtension(this.extensionUtility, this.filterService, this.sharedService, this.sortService, this.backendUtilityService, this.translaterService);
const groupItemMetaProviderExtension = new GroupItemMetaProviderExtension(this.sharedService);
const headerButtonExtension = new HeaderButtonExtension(this.extensionUtility, this.sharedService);
const headerMenuExtension = new HeaderMenuExtension(this.extensionUtility, this.filterService, this._eventPubSubService, this.sharedService, this.sortService, this.translaterService);
@@ -494,6 +500,10 @@ export class SlickVanillaGridBundle {
this._eventPubSubService.publish('onDataviewCreated', this.dataView);
}
+ // get any possible Services that user want to register which don't require SlickGrid to be instantiated
+ // RxJS Resource is in this lot because it has to be registered before anything else and doesn't require SlickGrid to be initialized
+ this.preRegisterResources();
+
// for convenience to the user, we provide the property "editor" as an Slickgrid-Universal editor complex object
// however "editor" is used internally by SlickGrid for it's own Editor Factory
// so in our lib we will swap "editor" and copy it into a new property called "internalColumnEditor"
@@ -529,6 +539,9 @@ export class SlickVanillaGridBundle {
this.sharedService.frozenVisibleColumnId = this._columnDefinitions[frozenColumnIndex]?.id ?? '';
}
+ // get any possible Services that user want to register
+ this.registerResources();
+
// initialize the SlickGrid grid
this.slickGrid.init();
@@ -613,67 +626,12 @@ export class SlickVanillaGridBundle {
this.gridEventService.bindOnCellChange(this.slickGrid);
this.gridEventService.bindOnClick(this.slickGrid);
- // get any possible Services that user want to register
- this._registeredResources = this.gridOptions.registerExternalResources || [];
-
- // when using Salesforce, we want the Export to CSV always enabled without registering it
- if (this.gridOptions.enableTextExport && this.gridOptions.useSalesforceDefaultGridOptions) {
- const textExportService = new TextExportService();
- this._registeredResources.push(textExportService);
- }
-
- // at this point, we consider all the registered services as external services, anything else registered afterward aren't external
- if (Array.isArray(this._registeredResources)) {
- this.sharedService.externalRegisteredResources = this._registeredResources;
- }
-
- // push all other Services that we want to be registered
- this._registeredResources.push(this.gridService, this.gridStateService);
-
- // when using Grouping/DraggableGrouping/Colspan register its Service
- if (this.gridOptions.createPreHeaderPanel && !this.gridOptions.enableDraggableGrouping) {
- this._registeredResources.push(this.groupingService);
- }
-
- // when using Tree Data View, register its Service
- if (this.gridOptions.enableTreeData) {
- this._registeredResources.push(this.treeDataService);
- }
-
- // when user enables translation, we need to translate Headers on first pass & subsequently in the bindDifferentHooks
- if (this.gridOptions.enableTranslate) {
- this.extensionService.translateColumnHeaders();
- }
-
- // also initialize (render) the empty warning component
- this.slickEmptyWarning = new SlickEmptyWarningComponent();
- this._registeredResources.push(this.slickEmptyWarning);
-
- // also initialize (render) the pagination component when using the salesforce default options
- // however before adding a new instance, just make sure there isn't one that might have been loaded by calling "registerExternalResources"
- if (this.gridOptions.enableCompositeEditor && this.gridOptions.useSalesforceDefaultGridOptions) {
- if (!this._registeredResources.some((resource => resource instanceof SlickCompositeEditorComponent))) {
- this.slickCompositeEditor = new SlickCompositeEditorComponent();
- this._registeredResources.push(this.slickCompositeEditor);
- }
- }
-
// bind the Backend Service API callback functions only after the grid is initialized
// because the preProcess() and onInit() might get triggered
if (this.gridOptions?.backendServiceApi) {
this.bindBackendCallbackFunctions(this.gridOptions);
}
- // bind & initialize all Components/Services that were tagged as enabled
- // register all services by executing their init method and providing them with the Grid object
- if (Array.isArray(this._registeredResources)) {
- for (const resource of this._registeredResources) {
- if (typeof resource.init === 'function') {
- resource.init(this.slickGrid, this.universalContainerService);
- }
- }
- }
-
// publish & dispatch certain events
this._eventPubSubService.publish('onGridCreated', this.slickGrid);
@@ -945,6 +903,7 @@ export class SlickVanillaGridBundle {
// wrap this inside a setTimeout to avoid timing issue since the gridOptions needs to be ready before running this onInit
setTimeout(() => {
+ const backendUtilityService = this.backendUtilityService as BackendUtilityService;
// keep start time & end timestamps & return it after process execution
const startTime = new Date();
@@ -954,11 +913,18 @@ export class SlickVanillaGridBundle {
}
// the processes can be a Promise (like Http)
- if (process instanceof Promise && process.then) {
- const totalItems = this.gridOptions && this.gridOptions.pagination && this.gridOptions.pagination.totalItems || 0;
+ const totalItems = this.gridOptions?.pagination?.totalItems ?? 0;
+ if (process instanceof Promise) {
process
- .then((processResult: any) => executeBackendProcessesCallback(startTime, processResult, backendApi, totalItems))
- .catch((error) => onBackendError(error, backendApi));
+ .then((processResult: any) => backendUtilityService.executeBackendProcessesCallback(startTime, processResult, backendApi, totalItems))
+ .catch((error) => backendUtilityService.onBackendError(error, backendApi));
+ } else if (process && this.rxjs?.isObservable(process)) {
+ this.subscriptions.push(
+ (process as Observable).subscribe(
+ (processResult: any) => backendUtilityService.executeBackendProcessesCallback(startTime, processResult, backendApi, totalItems),
+ (error: any) => backendUtilityService.onBackendError(error, backendApi)
+ )
+ );
}
});
}
@@ -1178,7 +1144,7 @@ export class SlickVanillaGridBundle {
this._eventPubSubService.subscribe('onPaginationVisibilityChanged', (visibility: { visible: boolean }) => {
this.showPagination = visibility?.visible ?? false;
if (this.gridOptions?.backendServiceApi) {
- refreshBackendDataset();
+ this.backendUtilityService?.refreshBackendDataset(this.gridOptions);
}
})
);
@@ -1198,7 +1164,7 @@ export class SlickVanillaGridBundle {
const collectionAsync = (column?.editor as ColumnEditor).collectionAsync;
(column?.editor as ColumnEditor).disabled = true; // disable the Editor DOM element, we'll re-enable it after receiving the collection with "updateEditorCollection()"
- if (collectionAsync) {
+ if (collectionAsync instanceof Promise) {
// wait for the "collectionAsync", once resolved we will save it into the "collection"
// the collectionAsync can be of 3 types HttpClient, HttpFetch or a Promise
collectionAsync.then((response: any | any[]) => {
@@ -1216,6 +1182,13 @@ export class SlickVanillaGridBundle {
this.updateEditorCollection(column, response['content']); // from http-client
}
});
+ } else if (this.rxjs?.isObservable(collectionAsync)) {
+ // wrap this inside a setTimeout to avoid timing issue since updateEditorCollection requires to call SlickGrid getColumns() method
+ setTimeout(() => {
+ this.subscriptions.push(
+ (collectionAsync as Observable).subscribe((resolvedCollection) => this.updateEditorCollection(column, resolvedCollection))
+ );
+ });
}
}
@@ -1297,6 +1270,86 @@ export class SlickVanillaGridBundle {
}
}
+ /** Pre-Register any Resource that don't require SlickGrid to be instantiated (for example RxJS Resource) */
+ private preRegisterResources() {
+ this._registeredResources = this.gridOptions.registerExternalResources || [];
+
+ // bind & initialize all Components/Services that were tagged as enabled
+ // register all services by executing their init method and providing them with the Grid object
+ if (Array.isArray(this._registeredResources)) {
+ for (const resource of this._registeredResources) {
+ if (resource?.className === 'RxJsResource') {
+ this.registerRxJsResource(resource as RxJsFacade);
+ }
+ }
+ }
+ }
+
+ private registerResources() {
+ // when using Salesforce, we want the Export to CSV always enabled without registering it
+ if (this.gridOptions.enableTextExport && this.gridOptions.useSalesforceDefaultGridOptions) {
+ const textExportService = new TextExportService();
+ this._registeredResources.push(textExportService);
+ }
+
+ // at this point, we consider all the registered services as external services, anything else registered afterward aren't external
+ if (Array.isArray(this._registeredResources)) {
+ this.sharedService.externalRegisteredResources = this._registeredResources;
+ }
+
+ // push all other Services that we want to be registered
+ this._registeredResources.push(this.gridService, this.gridStateService);
+
+ // when using Grouping/DraggableGrouping/Colspan register its Service
+ if (this.gridOptions.createPreHeaderPanel && !this.gridOptions.enableDraggableGrouping) {
+ this._registeredResources.push(this.groupingService);
+ }
+
+ // when using Tree Data View, register its Service
+ if (this.gridOptions.enableTreeData) {
+ this._registeredResources.push(this.treeDataService);
+ }
+
+ // when user enables translation, we need to translate Headers on first pass & subsequently in the bindDifferentHooks
+ if (this.gridOptions.enableTranslate) {
+ this.extensionService.translateColumnHeaders();
+ }
+
+ // also initialize (render) the empty warning component
+ this.slickEmptyWarning = new SlickEmptyWarningComponent();
+ this._registeredResources.push(this.slickEmptyWarning);
+
+ // also initialize (render) the pagination component when using the salesforce default options
+ // however before adding a new instance, just make sure there isn't one that might have been loaded by calling "registerExternalResources"
+ if (this.gridOptions.enableCompositeEditor && this.gridOptions.useSalesforceDefaultGridOptions) {
+ if (!this._registeredResources.some((resource => resource instanceof SlickCompositeEditorComponent))) {
+ this.slickCompositeEditor = new SlickCompositeEditorComponent();
+ this._registeredResources.push(this.slickCompositeEditor);
+ }
+ }
+
+ // bind & initialize all Components/Services that were tagged as enabled
+ // register all services by executing their init method and providing them with the Grid object
+ if (Array.isArray(this._registeredResources)) {
+ for (const resource of this._registeredResources) {
+ if (typeof resource.init === 'function') {
+ resource.init(this.slickGrid, this.universalContainerService);
+ }
+ }
+ }
+ }
+
+ /** Register the RxJS Resource in all necessary services which uses */
+ private registerRxJsResource(resource: RxJsFacade) {
+ this.rxjs = resource;
+ this.backendUtilityService.addRxJsResource(this.rxjs);
+ this.filterFactory.addRxJsResource(this.rxjs);
+ this.filterService.addRxJsResource(this.rxjs);
+ this.sortService.addRxJsResource(this.rxjs);
+ this.paginationService.addRxJsResource(this.rxjs);
+ this.universalContainerService.registerInstance('RxJsResource', this.rxjs);
+ }
+
/**
* For convenience to the user, we provide the property "editor" as an Slickgrid-Universal editor complex object
* however "editor" is used internally by SlickGrid for it's own Editor Factory
diff --git a/packages/vanilla-bundle/src/services/eventPubSub.service.ts b/packages/vanilla-bundle/src/services/eventPubSub.service.ts
index 2d531db06..00da0ef1b 100644
--- a/packages/vanilla-bundle/src/services/eventPubSub.service.ts
+++ b/packages/vanilla-bundle/src/services/eventPubSub.service.ts
@@ -1,4 +1,4 @@
-import { EventNamingStyle, PubSubService, Subscription, titleCase, toKebabCase } from '@slickgrid-universal/common';
+import { EventNamingStyle, EventSubscription, PubSubService, titleCase, toKebabCase } from '@slickgrid-universal/common';
interface PubSubEvent {
name: string;
@@ -50,7 +50,7 @@ export class EventPubSubService implements PubSubService {
this._elementSource.removeEventListener(eventNameByConvention, listener);
}
- unsubscribeAll(subscriptions?: Subscription[]) {
+ unsubscribeAll(subscriptions?: EventSubscription[]) {
if (Array.isArray(subscriptions)) {
for (const subscription of subscriptions) {
if (subscription?.dispose) {
diff --git a/test/cypress/integration/example15.spec.js b/test/cypress/integration/example15.spec.js
new file mode 100644
index 000000000..9e4f3f26c
--- /dev/null
+++ b/test/cypress/integration/example15.spec.js
@@ -0,0 +1,735 @@
+///
+
+describe('Example 15 - OData Grid using RxJS', () => {
+ const GRID_ROW_HEIGHT = 33;
+
+ beforeEach(() => {
+ // create a console.log spy for later use
+ cy.window().then((win) => {
+ cy.spy(win.console, 'log');
+ });
+ });
+
+ it('should display Example title', () => {
+ cy.visit(`${Cypress.config('baseExampleUrl')}/example15`);
+ cy.get('h3').should('contain', 'Example 15 - Grid with OData Backend Service using RxJS Observables');
+ });
+
+ describe('when "enableCount" is set', () => {
+ it('should have default OData query', () => {
+ cy.get('[data-test=alert-odata-query]').should('exist');
+ cy.get('[data-test=alert-odata-query]').should('contain', 'OData Query');
+
+ // wait for the query to finish
+ cy.get('[data-test=status]').should('contain', 'finished');
+
+ cy.get('[data-test=odata-query-result]')
+ .should(($span) => {
+ expect($span.text()).to.eq(`$inlinecount=allpages&$top=20&$skip=20&$orderby=Name asc&$filter=(Gender eq 'male')`);
+ });
+ });
+
+ it('should change Pagination to next page', () => {
+ cy.get('.icon-seek-next').click();
+
+ // wait for the query to finish
+ cy.get('[data-test=status]').should('contain', 'finished');
+
+ cy.get('[data-test=page-number-input]')
+ .invoke('val')
+ .then(pageNumber => expect(pageNumber).to.eq('3'));
+
+ cy.get('[data-test=page-count]')
+ .contains('3');
+
+ cy.get('[data-test=item-from]')
+ .contains('41');
+
+ cy.get('[data-test=item-to]')
+ .contains('50');
+
+ cy.get('[data-test=total-items]')
+ .contains('50');
+
+ cy.get('[data-test=odata-query-result]')
+ .should(($span) => {
+ expect($span.text()).to.eq(`$inlinecount=allpages&$top=20&$skip=40&$orderby=Name asc&$filter=(Gender eq 'male')`);
+ });
+
+ cy.window().then((win) => {
+ expect(win.console.log).to.have.callCount(1);
+ expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { newValues: { pageNumber: 3, pageSize: 20 }, type: 'pagination' });
+ });
+ });
+
+ it('should change Pagination to first page with 10 items', () => {
+ cy.get('#items-per-page-label').select('10');
+
+ // wait for the query to start and finish
+ cy.get('[data-test=status]').should('contain', 'loading...');
+ cy.get('[data-test=status]').should('contain', 'finished');
+
+ cy.get('[data-test=page-number-input]')
+ .invoke('val')
+ .then(pageNumber => expect(pageNumber).to.eq('1'));
+
+ cy.get('[data-test=page-count]')
+ .contains('5');
+
+ cy.get('[data-test=item-from]')
+ .contains('1');
+
+ cy.get('[data-test=item-to]')
+ .contains('10');
+
+ cy.get('[data-test=total-items]')
+ .contains('50');
+
+ cy.get('[data-test=odata-query-result]')
+ .should(($span) => {
+ expect($span.text()).to.eq(`$inlinecount=allpages&$top=10&$orderby=Name asc&$filter=(Gender eq 'male')`);
+ });
+
+ cy.window().then((win) => {
+ expect(win.console.log).to.have.callCount(1);
+ expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { newValues: { pageNumber: 1, pageSize: 10 }, type: 'pagination' });
+ });
+ });
+
+ it('should change Pagination to last page', () => {
+ cy.get('.icon-seek-end').click();
+
+ // wait for the query to finish
+ cy.get('[data-test=status]').should('contain', 'finished');
+
+ cy.get('[data-test=page-number-input]')
+ .invoke('val')
+ .then(pageNumber => expect(pageNumber).to.eq('5'));
+
+ cy.get('[data-test=page-count]')
+ .contains('5');
+
+ cy.get('[data-test=item-from]')
+ .contains('41');
+
+ cy.get('[data-test=item-to]')
+ .contains('50');
+
+ cy.get('[data-test=total-items]')
+ .contains('50');
+
+ cy.get('[data-test=odata-query-result]')
+ .should(($span) => {
+ expect($span.text()).to.eq(`$inlinecount=allpages&$top=10&$skip=40&$orderby=Name asc&$filter=(Gender eq 'male')`);
+ });
+
+ cy.window().then((win) => {
+ expect(win.console.log).to.have.callCount(1);
+ expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { newValues: { pageNumber: 5, pageSize: 10 }, type: 'pagination' });
+ });
+ });
+
+ it('should change Pagination to first page using the external button', () => {
+ cy.get('[data-test=goto-first-page')
+ .click();
+
+ // wait for the query to finish
+ cy.get('[data-test=status]').should('contain', 'finished');
+
+ cy.get('[data-test=page-number-input]')
+ .invoke('val')
+ .then(pageNumber => expect(pageNumber).to.eq('1'));
+
+ cy.get('[data-test=page-count]')
+ .contains('5');
+
+ cy.get('[data-test=item-from]')
+ .contains('1');
+
+ cy.get('[data-test=item-to]')
+ .contains('10');
+
+ cy.get('[data-test=total-items]')
+ .contains('50');
+
+ cy.get('[data-test=odata-query-result]')
+ .should(($span) => {
+ expect($span.text()).to.eq(`$inlinecount=allpages&$top=10&$orderby=Name asc&$filter=(Gender eq 'male')`);
+ });
+
+ cy.window().then((win) => {
+ expect(win.console.log).to.have.callCount(1);
+ expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { newValues: { pageNumber: 1, pageSize: 10 }, type: 'pagination' });
+ });
+ });
+
+ it('should change Pagination to last page using the external button', () => {
+ cy.get('[data-test=goto-last-page')
+ .click();
+
+ // wait for the query to finish
+ cy.get('[data-test=status]').should('contain', 'finished');
+
+ cy.get('[data-test=page-number-input]')
+ .invoke('val')
+ .then(pageNumber => expect(pageNumber).to.eq('5'));
+
+ cy.get('[data-test=page-count]')
+ .contains('5');
+
+ cy.get('[data-test=item-from]')
+ .contains('41');
+
+ cy.get('[data-test=item-to]')
+ .contains('50');
+
+ cy.get('[data-test=total-items]')
+ .contains('50');
+
+ cy.get('[data-test=odata-query-result]')
+ .should(($span) => {
+ expect($span.text()).to.eq(`$inlinecount=allpages&$top=10&$skip=40&$orderby=Name asc&$filter=(Gender eq 'male')`);
+ });
+
+ cy.window().then((win) => {
+ expect(win.console.log).to.have.callCount(1);
+ expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { newValues: { pageNumber: 5, pageSize: 10 }, type: 'pagination' });
+ });
+ });
+
+ it('should Clear all Filters and expect to go back to first page', () => {
+ cy.get('.grid15')
+ .find('button.slick-gridmenu-button')
+ .trigger('click')
+ .click();
+
+ cy.get(`.slick-gridmenu:visible`)
+ .find('.slick-gridmenu-item')
+ .first()
+ .find('span')
+ .contains('Clear all Filters')
+ .click();
+
+ // wait for the query to finish
+ cy.get('[data-test=status]').should('contain', 'finished');
+
+ cy.get('[data-test=page-number-input]')
+ .invoke('val')
+ .then(pageNumber => expect(pageNumber).to.eq('1'));
+
+ cy.get('[data-test=page-count]')
+ .contains('10');
+
+ cy.get('[data-test=item-from]')
+ .contains('1');
+
+ cy.get('[data-test=item-to]')
+ .contains('10');
+
+ cy.get('[data-test=total-items]')
+ .contains('100');
+
+ cy.get('[data-test=odata-query-result]')
+ .should(($span) => {
+ expect($span.text()).to.eq(`$inlinecount=allpages&$top=10&$orderby=Name asc`);
+ });
+
+ cy.window().then((win) => {
+ expect(win.console.log).to.have.callCount(2);
+ expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { newValues: [], type: 'filter' });
+ expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { newValues: { pageNumber: 1, pageSize: 10 }, type: 'pagination' });
+ });
+ });
+
+ it('should Clear all Sorting', () => {
+ cy.get('.grid15')
+ .find('button.slick-gridmenu-button')
+ .trigger('click')
+ .click();
+
+ cy.get(`.slick-gridmenu:visible`)
+ .find('.slick-gridmenu-item:nth(1)')
+ .find('span')
+ .contains('Clear all Sorting')
+ .click();
+
+ // wait for the query to finish
+ cy.get('[data-test=status]').should('contain', 'finished');
+
+ cy.get('[data-test=odata-query-result]')
+ .should(($span) => {
+ expect($span.text()).to.eq(`$inlinecount=allpages&$top=10`);
+ });
+
+ cy.window().then((win) => {
+ expect(win.console.log).to.have.callCount(1);
+ expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { newValues: [], type: 'sorter' });
+ });
+ });
+
+ it('should use "substringof" when OData version is set to 2', () => {
+ cy.get('.search-filter.filter-name')
+ .find('input')
+ .type('John');
+
+ // wait for the query to finish
+ cy.get('[data-test=status]').should('contain', 'finished');
+
+ cy.get('[data-test=odata-query-result]')
+ .should(($span) => {
+ expect($span.text()).to.eq(`$inlinecount=allpages&$top=10&$filter=(substringof('John', Name))`);
+ });
+
+ cy.get('.grid15')
+ .find('.slick-row')
+ .should('have.length', 1);
+ });
+
+ it('should use "contains" when OData version is set to 4', () => {
+ cy.get('[data-test=version4]')
+ .click();
+
+ cy.get('.search-filter.filter-name')
+ .find('input')
+ .type('John');
+
+ // wait for the query to finish
+ cy.get('[data-test=status]').should('contain', 'finished');
+
+ cy.get('[data-test=odata-query-result]')
+ .should(($span) => {
+ expect($span.text()).to.eq(`$count=true&$top=10&$filter=(contains(Name, 'John'))`);
+ });
+
+ cy.get('.grid15')
+ .find('.slick-row')
+ .should('have.length', 1);
+ });
+
+ it('should click on Set Dynamic Filter and expect query and filters to be changed', () => {
+ cy.get('[data-test=set-dynamic-filter]')
+ .click();
+
+ cy.get('.search-filter.filter-name select')
+ .should('have.value', 'a*');
+
+ cy.get('.search-filter.filter-name')
+ .find('input')
+ .invoke('val')
+ .then(text => expect(text).to.eq('A'));
+
+ // wait for the query to finish
+ cy.get('[data-test=status]').should('contain', 'finished');
+
+ cy.get('[data-test=odata-query-result]')
+ .should(($span) => {
+ expect($span.text()).to.eq(`$count=true&$top=10&$filter=(startswith(Name, 'A'))`);
+ });
+
+ cy.get('.grid15')
+ .find('.slick-row')
+ .should('have.length', 5);
+ });
+ });
+
+ describe('when "enableCount" is unchecked (not set)', () => {
+ it('should Clear all Filters, set 20 items per page & uncheck "enableCount"', () => {
+ cy.get('.grid15')
+ .find('button.slick-gridmenu-button')
+ .trigger('click')
+ .click();
+
+ cy.get(`.slick-gridmenu:visible`)
+ .find('.slick-gridmenu-item')
+ .first()
+ .find('span')
+ .contains('Clear all Filters')
+ .click();
+
+ cy.get('#items-per-page-label').select('20');
+
+ cy.get('[data-test=enable-count]').click();
+
+ // wait for the query to finish
+ cy.get('[data-test=status]').should('contain', 'finished');
+
+ cy.get('[data-test=odata-query-result]')
+ .should(($span) => {
+ expect($span.text()).to.eq(`$top=20`);
+ });
+ });
+
+ it('should change Pagination to next page', () => {
+ cy.get('.icon-seek-next').click();
+
+ // wait for the query to finish
+ cy.get('[data-test=status]').should('contain', 'finished');
+
+ cy.get('[data-test=odata-query-result]')
+ .should(($span) => {
+ expect($span.text()).to.eq(`$top=20&$skip=20`);
+ });
+ });
+
+ it('should change Pagination to first page with 10 items', () => {
+ cy.get('#items-per-page-label').select('10');
+
+ // wait for the query to start and finish
+ cy.get('[data-test=status]').should('contain', 'loading...');
+ cy.get('[data-test=status]').should('contain', 'finished');
+
+ cy.get('[data-test=odata-query-result]')
+ .should(($span) => {
+ expect($span.text()).to.eq(`$top=10`);
+ });
+ });
+
+ it('should change Pagination to last page', () => {
+ cy.get('.icon-seek-end').click();
+
+ // wait for the query to finish
+ cy.get('[data-test=status]').should('contain', 'finished');
+
+ cy.get('[data-test=odata-query-result]')
+ .should(($span) => {
+ expect($span.text()).to.eq(`$top=10&$skip=90`);
+ });
+ });
+
+ it('should click on "Name" column to sort it Ascending', () => {
+ cy.get('.slick-header-columns')
+ .children('.slick-header-column:nth(1)')
+ .click();
+
+ cy.get('.slick-header-columns')
+ .children('.slick-header-column:nth(1)')
+ .find('.slick-sort-indicator.slick-sort-indicator-asc')
+ .should('be.visible');
+
+ // wait for the query to finish
+ cy.get('[data-test=status]').should('contain', 'finished');
+
+ cy.get('[data-test=odata-query-result]')
+ .should(($span) => {
+ expect($span.text()).to.eq(`$top=10&$skip=90&$orderby=Name asc`);
+ });
+ });
+
+ it('should Clear all Sorting', () => {
+ cy.get('.grid15')
+ .find('button.slick-gridmenu-button')
+ .trigger('click')
+ .click();
+
+ cy.get(`.slick-gridmenu:visible`)
+ .find('.slick-gridmenu-item:nth(1)')
+ .find('span')
+ .contains('Clear all Sorting')
+ .click();
+
+ // wait for the query to finish
+ cy.get('[data-test=status]').should('contain', 'finished');
+
+ cy.get('[data-test=odata-query-result]')
+ .should(($span) => {
+ expect($span.text()).to.eq(`$top=10&$skip=90`);
+ });
+ });
+
+ it('should click on Set Dynamic Filter and expect query and filters to be changed', () => {
+ cy.get('[data-test=set-dynamic-filter]')
+ .click();
+
+ cy.get('.search-filter.filter-name select')
+ .should('have.value', 'a*');
+
+ cy.get('.search-filter.filter-name')
+ .find('input')
+ .invoke('val')
+ .then(text => expect(text).to.eq('A'));
+
+ // wait for the query to finish
+ cy.get('[data-test=status]').should('contain', 'finished');
+
+ cy.get('[data-test=odata-query-result]')
+ .should(($span) => {
+ expect($span.text()).to.eq(`$top=10&$filter=(startswith(Name, 'A'))`);
+ });
+
+ cy.get('.grid15')
+ .find('.slick-row')
+ .should('have.length', 5);
+ });
+
+ it('should use "substringof" when OData version is set to 2', () => {
+ cy.get('[data-test=version2]')
+ .click();
+
+ cy.get('.search-filter.filter-name')
+ .find('input')
+ .type('John');
+
+ // wait for the query to finish
+ cy.get('[data-test=status]').should('contain', 'finished');
+
+ cy.get('[data-test=odata-query-result]')
+ .should(($span) => {
+ expect($span.text()).to.eq(`$top=10&$filter=(substringof('John', Name))`);
+ });
+
+ cy.get('.grid15')
+ .find('.slick-row')
+ .should('have.length', 1);
+ });
+
+ it('should use "contains" when OData version is set to 4', () => {
+ cy.get('[data-test=version4]')
+ .click();
+
+ cy.get('.search-filter.filter-name')
+ .find('input')
+ .type('John');
+
+ // wait for the query to finish
+ cy.get('[data-test=status]').should('contain', 'finished');
+
+ cy.get('[data-test=odata-query-result]')
+ .should(($span) => {
+ expect($span.text()).to.eq(`$top=10&$filter=(contains(Name, 'John'))`);
+ });
+
+ cy.get('.grid15')
+ .find('.slick-row')
+ .should('have.length', 1);
+ });
+ });
+
+ describe('General Pagination Behaviors', () => {
+ it('should type a filter which returns an empty dataset', () => {
+ cy.get('.search-filter.filter-name')
+ .find('input')
+ .clear()
+ .type('xy');
+
+ cy.get('[data-test=odata-query-result]')
+ .should(($span) => {
+ expect($span.text()).to.eq(`$top=10&$filter=(contains(Name, 'xy'))`);
+ });
+
+ // wait for the query to finish
+ cy.get('[data-test=status]').should('contain', 'finished');
+
+ cy.get('.slick-empty-data-warning:visible')
+ .contains('No data to display.');
+ });
+
+ it('should display page 0 of 0 but hide pagination from/to numbers when filtered data "xy" returns an empty dataset', () => {
+ cy.get('[data-test=page-count]')
+ .contains('0');
+
+ cy.get('[data-test=item-from]')
+ .should('not.be.visible');
+
+ cy.get('[data-test=item-to]')
+ .should('not.be.visible');
+
+ cy.get('[data-test=total-items]')
+ .contains('0');
+
+ cy.get('[data-test=odata-query-result]')
+ .should(($span) => {
+ expect($span.text()).to.eq(`$top=10&$filter=(contains(Name, 'xy'))`);
+ });
+
+ cy.get('[data-test=page-number-input]')
+ .invoke('val')
+ .then(pageNumber => expect(pageNumber).to.eq('0'));
+ });
+
+ it('should erase part of the filter so that it filters with "x"', () => {
+ cy.get('.search-filter.filter-name')
+ .find('input')
+ .type('{backspace}');
+
+ cy.get('[data-test=odata-query-result]')
+ .should(($span) => {
+ expect($span.text()).to.eq(`$top=10&$filter=(contains(Name, 'x'))`);
+ });
+
+ // wait for the query to finish
+ cy.get('[data-test=status]').should('contain', 'finished');
+
+ cy.get('.slick-empty-data-warning')
+ .contains('No data to display.')
+ .should('not.be.visible');
+
+ cy.window().then((win) => {
+ // expect(win.console.log).to.have.callCount(2);
+ expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { newValues: [{ columnId: 'name', operator: 'Contains', searchTerms: ['x'] }], type: 'filter' });
+ // expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { newValues: { pageNumber: 1, pageSize: 10 }, type: 'pagination' });
+ });
+ });
+
+ it('should display page 1 of 1 with 2 items after erasing part of the filter to be "x" which should return 1 page', () => {
+ cy.wait(50);
+
+ cy.get('[data-test=page-count]')
+ .contains('1');
+
+ cy.get('[data-test=item-from]')
+ .contains('1');
+
+ cy.get('[data-test=item-to]')
+ .contains('2');
+
+ cy.get('[data-test=total-items]')
+ .contains('2');
+
+ cy.get('[data-test=page-number-input]')
+ .invoke('val')
+ .then(pageNumber => expect(pageNumber).to.eq('1'));
+ });
+ });
+
+ describe('Set Dynamic Sorting', () => {
+ it('should click on "Set Filters Dynamically" then on "Set Sorting Dynamically"', () => {
+ cy.get('[data-test=set-dynamic-filter]')
+ .click();
+
+ // wait for the query to finish
+ cy.get('[data-test=status]').should('contain', 'loading');
+ cy.get('[data-test=status]').should('contain', 'finished');
+
+ cy.get('[data-test=set-dynamic-sorting]')
+ .click();
+
+ cy.get('[data-test=status]').should('contain', 'loading');
+ cy.get('[data-test=status]').should('contain', 'finished');
+ });
+
+ it('should expect the grid to be sorted by "Name" descending', () => {
+ cy.get('.grid15')
+ .get('.slick-header-column:nth(1)')
+ .find('.slick-sort-indicator-desc')
+ .should('have.length', 1);
+
+ cy.get('.slick-row')
+ .first()
+ .children('.slick-cell:nth(1)')
+ .should('contain', 'Ayers Hood');
+
+ cy.get('.slick-row')
+ .last()
+ .children('.slick-cell:nth(1)')
+ .should('contain', 'Alexander Foley');
+
+ cy.get('[data-test=odata-query-result]')
+ .should(($span) => {
+ expect($span.text()).to.eq(`$top=10&$orderby=Name desc&$filter=(startswith(Name, 'A'))`);
+ });
+ });
+ });
+
+ describe('Editors & Filters with RxJS Observable', () => {
+ it('should open the "Gender" filter and expect to find 3 options in its list ([blank], male, female)', () => {
+ const expectedOptions = ['', 'male', 'female'];
+ cy.get('.ms-filter.filter-gender:visible').click();
+
+ cy.get('[name="filter-gender"].ms-drop')
+ .find('li:visible')
+ .should('have.length', 3);
+
+ cy.get('[name="filter-gender"].ms-drop')
+ .find('li:visible')
+ .each(($li, index) => expect($li.text()).to.eq(expectedOptions[index]));
+
+ cy.get('.grid15')
+ .find('.slick-row')
+ .should('have.length', 5);
+ });
+
+ it('should select "male" Gender and expect only 4 rows left in the grid', () => {
+ cy.get('[name="filter-gender"].ms-drop')
+ .find('li:visible:nth(1)')
+ .contains('male')
+ .click();
+
+ cy.get('.grid15')
+ .find('.slick-row')
+ .should('have.length', 4);
+ });
+
+ it('should be able to open "Gender" on the first row and expect to find 2 options the editor list (male, female)', () => {
+ const expectedOptions = ['male', 'female'];
+
+ cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(2)`)
+ .should('contain', 'male')
+ .click();
+
+ cy.get('[name="editor-gender"].ms-drop')
+ .find('li:visible')
+ .should('have.length', 2);
+
+ cy.get('[name="editor-gender"].ms-drop')
+ .find('li:visible')
+ .each(($li, index) => expect($li.text()).to.eq(expectedOptions[index]));
+ });
+
+ it('should click on "Add Other Gender via RxJS" button', () => {
+ cy.get('[data-test="add-gender-button"]').should('not.be.disabled');
+ cy.get('[data-test="add-gender-button"]').click();
+ cy.get('[data-test="add-gender-button"]').should('be.disabled');
+ });
+
+ it('should open the "Gender" editor on the first row and expect to find 1 more option the editor list (male, female, other)', () => {
+ const expectedOptions = ['male', 'female', 'other'];
+
+ cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(2)`)
+ .should('contain', 'male')
+ .click();
+
+ cy.get('[name="editor-gender"].ms-drop')
+ .find('li:visible')
+ .should('have.length', 3);
+
+ cy.get('[name="editor-gender"].ms-drop')
+ .find('li:visible')
+ .each(($li, index) => expect($li.text()).to.eq(expectedOptions[index]));
+ });
+
+ it('should be able to change the Gender editor on the first row to the new option "other"', () => {
+ cy.get('[name="editor-gender"].ms-drop')
+ .find('li:visible:nth(2)')
+ .contains('other')
+ .click();
+ });
+
+ it('should open Gender filter and now expect to see 1 more option in its list ([blank], male, female, other)', () => {
+ const expectedOptions = ['', 'male', 'female', 'other'];
+ cy.get('.ms-filter.filter-gender:visible').click();
+
+ cy.get('[name="filter-gender"].ms-drop')
+ .find('li:visible')
+ .should('have.length', 4);
+
+ cy.get('[name="filter-gender"].ms-drop')
+ .find('li:visible')
+ .each(($li, index) => expect($li.text()).to.eq(expectedOptions[index]));
+ });
+
+ it('should choose "other" form the Gender filter and expect 1 row left in the grid', () => {
+ cy.get('[name="filter-gender"].ms-drop')
+ .find('li:visible:nth(3)')
+ .contains('other')
+ .click();
+
+ cy.get('.grid15')
+ .find('.slick-row')
+ .should('have.length', 1);
+
+ cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(1)`).should('contain', 'Ayers Hood');
+ cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(2)`).should('contain', 'other');
+ cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(3)`).should('contain', 'Accuprint');
+ });
+ });
+});
diff --git a/test/rxjsResourceStub.ts b/test/rxjsResourceStub.ts
new file mode 100644
index 000000000..1adace899
--- /dev/null
+++ b/test/rxjsResourceStub.ts
@@ -0,0 +1,48 @@
+import { RxJsFacade } from '@slickgrid-universal/common';
+import { EMPTY, iif, isObservable, firstValueFrom, Observable, Subject } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
+
+export class RxJsResourceStub implements RxJsFacade {
+ readonly className = 'RxJsResource';
+
+ /**
+ * The same Observable instance returned by any call to without a scheduler.
+ * This returns the EMPTY constant from RxJS
+ */
+ get EMPTY(): Observable {
+ return EMPTY;
+ }
+
+ /** Simple method to create an Observable */
+ createObservable(): Observable {
+ return new Observable();
+ }
+
+ /** Simple method to create an Subject */
+ createSubject(): Subject {
+ return new Subject();
+ }
+
+ /** he same Observable instance returned by any call to without a scheduler. It is preferrable to use this over empty() */
+ empty(): Observable {
+ return EMPTY;
+ }
+
+ firstValueFrom(source: Observable): Promise {
+ return firstValueFrom(source);
+ }
+
+ iif(condition: () => boolean, trueResult?: any, falseResult?: any): Observable {
+ return iif(condition, trueResult, falseResult);
+ }
+
+ /** Tests to see if the object is an RxJS Observable */
+ isObservable(obj: any): boolean {
+ return isObservable(obj);
+ }
+
+ /** Emits the values emitted by the source Observable until a `notifier` Observable emits a value. */
+ takeUntil(notifier: Observable): any {
+ return takeUntil(notifier);
+ }
+}
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index bbf994f97..db6c96515 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -17,21 +17,21 @@
"@babel/highlight" "^7.12.13"
"@babel/compat-data@^7.13.8":
- version "7.13.8"
- resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.8.tgz#5b783b9808f15cef71547f1b691f34f8ff6003a6"
- integrity sha512-EaI33z19T4qN3xLXsGf48M2cDqa6ei9tPZlfLdb2HC+e/cFtREiRd8hdSqDbwdLB0/+gLwqJmCYASH0z2bUdog==
+ version "7.13.11"
+ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.11.tgz#9c8fe523c206979c9a81b1e12fe50c1254f1aa35"
+ integrity sha512-BwKEkO+2a67DcFeS3RLl0Z3Gs2OvdXewuWjc1Hfokhb5eQWP9YRYH1/+VrVZvql2CfjOiNGqSAFOYt4lsqTHzg==
"@babel/core@^7.1.0", "@babel/core@^7.7.5":
- version "7.13.8"
- resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.8.tgz#c191d9c5871788a591d69ea1dc03e5843a3680fb"
- integrity sha512-oYapIySGw1zGhEFRd6lzWNLWFX2s5dA/jm+Pw/+59ZdXtjyIuwlXbrId22Md0rgZVop+aVoqow2riXhBLNyuQg==
+ version "7.13.10"
+ resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.10.tgz#07de050bbd8193fcd8a3c27918c0890613a94559"
+ integrity sha512-bfIYcT0BdKeAZrovpMqX2Mx5NrgAckGbwT982AkdS5GNfn3KMGiprlBAtmBcFZRUmpaufS6WZFP8trvx8ptFDw==
dependencies:
"@babel/code-frame" "^7.12.13"
- "@babel/generator" "^7.13.0"
- "@babel/helper-compilation-targets" "^7.13.8"
+ "@babel/generator" "^7.13.9"
+ "@babel/helper-compilation-targets" "^7.13.10"
"@babel/helper-module-transforms" "^7.13.0"
- "@babel/helpers" "^7.13.0"
- "@babel/parser" "^7.13.4"
+ "@babel/helpers" "^7.13.10"
+ "@babel/parser" "^7.13.10"
"@babel/template" "^7.12.13"
"@babel/traverse" "^7.13.0"
"@babel/types" "^7.13.0"
@@ -43,7 +43,7 @@
semver "^6.3.0"
source-map "^0.5.0"
-"@babel/generator@^7.13.0":
+"@babel/generator@^7.13.0", "@babel/generator@^7.13.9":
version "7.13.9"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.13.9.tgz#3a7aa96f9efb8e2be42d38d80e2ceb4c64d8de39"
integrity sha512-mHOOmY0Axl/JCTkxTU6Lf5sWOg/v8nUa+Xkt4zMTftX0wqmb6Sh7J8gvcehBw7q0AhrhAR+FDacKjCZ2X8K+Sw==
@@ -52,10 +52,10 @@
jsesc "^2.5.1"
source-map "^0.5.0"
-"@babel/helper-compilation-targets@^7.13.8":
- version "7.13.8"
- resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.8.tgz#02bdb22783439afb11b2f009814bdd88384bd468"
- integrity sha512-pBljUGC1y3xKLn1nrx2eAhurLMA8OqBtBP/JwG4U8skN7kf8/aqwwxpV1N6T0e7r6+7uNitIa/fUxPFagSXp3A==
+"@babel/helper-compilation-targets@^7.13.10":
+ version "7.13.10"
+ resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.10.tgz#1310a1678cb8427c07a753750da4f8ce442bdd0c"
+ integrity sha512-/Xju7Qg1GQO4mHZ/Kcs6Au7gfafgZnwm+a7sy/ow/tV1sHeraRUHbjdat8/UvDor4Tez+siGKDk6zIKtCPKVJA==
dependencies:
"@babel/compat-data" "^7.13.8"
"@babel/helper-validator-option" "^7.12.17"
@@ -153,28 +153,28 @@
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz#d1fbf012e1a79b7eebbfdc6d270baaf8d9eb9831"
integrity sha512-TopkMDmLzq8ngChwRlyjR6raKD6gMSae4JdYDB8bByKreQgG0RBTuKe9LRxW3wFtUnjxOPRKBDwEH6Mg5KeDfw==
-"@babel/helpers@^7.13.0":
- version "7.13.0"
- resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.13.0.tgz#7647ae57377b4f0408bf4f8a7af01c42e41badc0"
- integrity sha512-aan1MeFPxFacZeSz6Ld7YZo5aPuqnKlD7+HZY75xQsueczFccP9A7V05+oe0XpLwHK3oLorPe9eaAUljL7WEaQ==
+"@babel/helpers@^7.13.10":
+ version "7.13.10"
+ resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.13.10.tgz#fd8e2ba7488533cdeac45cc158e9ebca5e3c7df8"
+ integrity sha512-4VO883+MWPDUVRF3PhiLBUFHoX/bsLTGFpFK/HqvvfBZz2D57u9XzPVNFVBTc0PW/CWR9BXTOKt8NF4DInUHcQ==
dependencies:
"@babel/template" "^7.12.13"
"@babel/traverse" "^7.13.0"
"@babel/types" "^7.13.0"
"@babel/highlight@^7.10.4", "@babel/highlight@^7.12.13":
- version "7.13.8"
- resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.13.8.tgz#10b2dac78526424dfc1f47650d0e415dfd9dc481"
- integrity sha512-4vrIhfJyfNf+lCtXC2ck1rKSzDwciqF7IWFhXXrSOUC2O5DrVp+w4c6ed4AllTxhTkUP5x2tYj41VaxdVMMRDw==
+ version "7.13.10"
+ resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.13.10.tgz#a8b2a66148f5b27d666b15d81774347a731d52d1"
+ integrity sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==
dependencies:
"@babel/helper-validator-identifier" "^7.12.11"
chalk "^2.0.0"
js-tokens "^4.0.0"
-"@babel/parser@^7.1.0", "@babel/parser@^7.12.13", "@babel/parser@^7.13.0", "@babel/parser@^7.13.4":
- version "7.13.9"
- resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.9.tgz#ca34cb95e1c2dd126863a84465ae8ef66114be99"
- integrity sha512-nEUfRiARCcaVo3ny3ZQjURjHQZUo/JkEw7rLlSZy/psWGnvwXFtPcr6jb7Yb41DVW5LTe6KRq9LGleRNsg1Frw==
+"@babel/parser@^7.1.0", "@babel/parser@^7.12.13", "@babel/parser@^7.13.0", "@babel/parser@^7.13.10":
+ version "7.13.11"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.11.tgz#f93ebfc99d21c1772afbbaa153f47e7ce2f50b88"
+ integrity sha512-PhuoqeHoO9fc4ffMEVk4qb/w/s2iOSWohvbHxLtxui0eBg3Lg5gN1U8wp1V1u61hOWkPQJJyJzGH6Y+grwkq8Q==
"@babel/plugin-syntax-async-generators@^7.8.4":
version "7.8.4"
@@ -1402,10 +1402,10 @@
is-plain-object "^5.0.0"
universal-user-agent "^6.0.0"
-"@octokit/openapi-types@^5.3.0":
- version "5.3.0"
- resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-5.3.0.tgz#29e3faa119da90082dc653ea74c8bb345d197bf7"
- integrity sha512-5q2qBz4iZ0xS/DEJ0ROusFbN4cVlbJE9GvOByen+mv7artuGXfVhONqcuRd7jYN2glTmCnzcZw+X6LrjRVqs0A==
+"@octokit/openapi-types@^5.3.2":
+ version "5.3.2"
+ resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-5.3.2.tgz#b8ac43c5c3d00aef61a34cf744e315110c78deb4"
+ integrity sha512-NxF1yfYOUO92rCx3dwvA2onF30Vdlg7YUkMVXkeptqpzA3tRLplThhFleV/UKWFgh7rpKu1yYRbvNDUtzSopKA==
"@octokit/plugin-enterprise-rest@^6.0.1":
version "6.0.1"
@@ -1494,11 +1494,11 @@
"@types/node" ">= 8"
"@octokit/types@^6.0.3", "@octokit/types@^6.7.1":
- version "6.12.0"
- resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.12.0.tgz#8376fd60edfd5d1eebfeedb994c6bcb5b862c7a1"
- integrity sha512-KwOf16soD7aDEEi/PgNeJlHzjZPfrmmNy+7WezSdrpnqZ7YImBJcNnX9+5RUHt1MnA4h8oISRHTqaZDGsX9DRQ==
+ version "6.12.2"
+ resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.12.2.tgz#5b44add079a478b8eb27d78cf384cc47e4411362"
+ integrity sha512-kCkiN8scbCmSq+gwdJV0iLgHc0O/GTPY1/cffo9kECu1MvatLPh9E+qFhfRIktKfHEA6ZYvv6S1B4Wnv3bi3pA==
dependencies:
- "@octokit/openapi-types" "^5.3.0"
+ "@octokit/openapi-types" "^5.3.2"
"@samverschueren/stream-to-observable@^0.3.0":
version "0.3.1"
@@ -1539,9 +1539,9 @@
integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==
"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7":
- version "7.1.12"
- resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.12.tgz#4d8e9e51eb265552a7e4f1ff2219ab6133bdfb2d"
- integrity sha512-wMTHiiTiBAAPebqaPiPDLFA4LYPKr6Ph0Xq/6rq1Ur3v66HXyG+clfR9CNETkD7MQS8ZHvpQOtA53DLws5WAEQ==
+ version "7.1.14"
+ resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.14.tgz#faaeefc4185ec71c389f4501ee5ec84b170cc402"
+ integrity sha512-zGZJzzBUVDo/eV6KgbE0f0ZI7dInEYvo12Rb70uNQDshC3SkRMb67ja0GgRHZgAX3Za6rhaWlvbDO8rrGyAb1g==
dependencies:
"@babel/parser" "^7.1.0"
"@babel/types" "^7.0.0"
@@ -1565,9 +1565,9 @@
"@babel/types" "^7.0.0"
"@types/babel__traverse@*", "@types/babel__traverse@^7.0.4", "@types/babel__traverse@^7.0.6":
- version "7.11.0"
- resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.11.0.tgz#b9a1efa635201ba9bc850323a8793ee2d36c04a0"
- integrity sha512-kSjgDMZONiIfSH1Nxcr5JIRMwUetDki63FSQfpTCz8ogF3Ulqm8+mr5f78dUYs6vMiB6gBusQqfQmBvHZj/lwg==
+ version "7.11.1"
+ resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.11.1.tgz#654f6c4f67568e24c23b367e947098c6206fa639"
+ integrity sha512-Vs0hm0vPahPMYi9tDjtP66llufgO3ST16WXaSTtDGEl9cewAl3AibmxWw6TINOqHPT9z0uABKAYjT9jNSg4npw==
dependencies:
"@babel/types" "^7.3.0"
@@ -1592,9 +1592,9 @@
"@types/estree" "*"
"@types/eslint@*":
- version "7.2.6"
- resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.2.6.tgz#5e9aff555a975596c03a98b59ecd103decc70c3c"
- integrity sha512-I+1sYH+NPQ3/tVqCeUSBwTE/0heyvtXqpIopUUArlBm0Kpocb8FbMa3AZ/ASKIFpN3rnEx932TTXDbt9OXsNDw==
+ version "7.2.7"
+ resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.2.7.tgz#f7ef1cf0dceab0ae6f9a976a0a9af14ab1baca26"
+ integrity sha512-EHXbc1z2GoQRqHaAT7+grxlTJ3WE2YNeD6jlpPoRc83cCoThRY+NUWjCUZaYmk51OICkPXn2hhphcWcWXgNW0Q==
dependencies:
"@types/estree" "*"
"@types/json-schema" "*"
@@ -1706,10 +1706,10 @@
dependencies:
moment "*"
-"@types/node@*", "@types/node@>= 8":
- version "14.14.31"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.31.tgz#72286bd33d137aa0d152d47ec7c1762563d34055"
- integrity sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g==
+"@types/node@*", "@types/node@>= 8", "@types/node@^14.14.35":
+ version "14.14.35"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.35.tgz#42c953a4e2b18ab931f72477e7012172f4ffa313"
+ integrity sha512-Lt+wj8NVPx0zUmUwumiVXapmaLUcAk3yPuHCFVXras9k5VT9TdhJqKqGVUQCD60OTMCl0qxJ57OiTL0Mic3Iag==
"@types/node@12.12.50":
version "12.12.50"
@@ -1721,11 +1721,6 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.0.tgz#acaa89247afddc7967e9902fd11761dadea1a555"
integrity sha512-j2tekvJCO7j22cs+LO6i0kRPhmQ9MXaPZ55TzOc1lzkN5b6BWqq4AFjl04s1oRRQ1v5rSe+KEvnLUSTonuls/A==
-"@types/node@^14.14.35":
- version "14.14.35"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.35.tgz#42c953a4e2b18ab931f72477e7012172f4ffa313"
- integrity sha512-Lt+wj8NVPx0zUmUwumiVXapmaLUcAk3yPuHCFVXras9k5VT9TdhJqKqGVUQCD60OTMCl0qxJ57OiTL0Mic3Iag==
-
"@types/normalize-package-data@^2.4.0":
version "2.4.0"
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
@@ -1737,9 +1732,9 @@
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
"@types/prettier@^2.0.0":
- version "2.2.2"
- resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.2.2.tgz#e2280c89ddcbeef340099d6968d8c86ba155fdf6"
- integrity sha512-i99hy7Ki19EqVOl77WplDrvgNugHnsSjECVR/wUrzw2TJXz1zlUfT2ngGckR6xN7yFYaijsMAqPkOLx9HgUqHg==
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.2.3.tgz#ef65165aea2924c9359205bf748865b8881753c0"
+ integrity sha512-PijRCG/K3s3w1We6ynUKdxEc5AcuuH3NBmMDP8uvKVp6X43UY7NQlTzczakXP3DJR0F4dfNQIGjU2cUeRYs2AA==
"@types/sinonjs__fake-timers@^6.0.1":
version "6.0.2"
@@ -1782,9 +1777,9 @@
integrity sha512-I8MnZqNXsOLHsU111oHbn3khtvKMi5Bn4qVFsIWSJcCP1KKDiXX5AEw8UPk0nSopeC+Hvxt6yAy1/a5PailFqg==
"@types/uglify-js@*":
- version "3.12.0"
- resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.12.0.tgz#2bb061c269441620d46b946350c8f16d52ef37c5"
- integrity sha512-sYAF+CF9XZ5cvEBkI7RtrG9g2GtMBkviTnBxYYyq+8BWvO4QtXfwwR6a2LFwCi4evMKZfpv6U43ViYvv17Wz3Q==
+ version "3.13.0"
+ resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.13.0.tgz#1cad8df1fb0b143c5aba08de5712ea9d1ff71124"
+ integrity sha512-EGkrJD5Uy+Pg0NUR8uA4bJ5WMfljyad0G+784vLCNUkD+QwOJXUbBYExXfVGf7YtyzdQp3L/XMYcliB987kL5Q==
dependencies:
source-map "^0.6.1"
@@ -1828,13 +1823,13 @@
dependencies:
"@types/yargs-parser" "*"
-"@typescript-eslint/eslint-plugin@^4.18.0":
- version "4.18.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.18.0.tgz#50fbce93211b5b690895d20ebec6fe8db48af1f6"
- integrity sha512-Lzkc/2+7EoH7+NjIWLS2lVuKKqbEmJhtXe3rmfA8cyiKnZm3IfLf51irnBcmow8Q/AptVV0XBZmBJKuUJTe6cQ==
+"@typescript-eslint/eslint-plugin@^4.19.0":
+ version "4.19.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.19.0.tgz#56f8da9ee118fe9763af34d6a526967234f6a7f0"
+ integrity sha512-CRQNQ0mC2Pa7VLwKFbrGVTArfdVDdefS+gTw0oC98vSI98IX5A8EVH4BzJ2FOB0YlCmm8Im36Elad/Jgtvveaw==
dependencies:
- "@typescript-eslint/experimental-utils" "4.18.0"
- "@typescript-eslint/scope-manager" "4.18.0"
+ "@typescript-eslint/experimental-utils" "4.19.0"
+ "@typescript-eslint/scope-manager" "4.19.0"
debug "^4.1.1"
functional-red-black-tree "^1.0.1"
lodash "^4.17.15"
@@ -1842,60 +1837,60 @@
semver "^7.3.2"
tsutils "^3.17.1"
-"@typescript-eslint/experimental-utils@4.18.0":
- version "4.18.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.18.0.tgz#ed6c955b940334132b17100d2917449b99a91314"
- integrity sha512-92h723Kblt9JcT2RRY3QS2xefFKar4ZQFVs3GityOKWQYgtajxt/tuXIzL7sVCUlM1hgreiV5gkGYyBpdOwO6A==
+"@typescript-eslint/experimental-utils@4.19.0":
+ version "4.19.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.19.0.tgz#9ca379919906dc72cb0fcd817d6cb5aa2d2054c6"
+ integrity sha512-9/23F1nnyzbHKuoTqFN1iXwN3bvOm/PRIXSBR3qFAYotK/0LveEOHr5JT1WZSzcD6BESl8kPOG3OoDRKO84bHA==
dependencies:
"@types/json-schema" "^7.0.3"
- "@typescript-eslint/scope-manager" "4.18.0"
- "@typescript-eslint/types" "4.18.0"
- "@typescript-eslint/typescript-estree" "4.18.0"
+ "@typescript-eslint/scope-manager" "4.19.0"
+ "@typescript-eslint/types" "4.19.0"
+ "@typescript-eslint/typescript-estree" "4.19.0"
eslint-scope "^5.0.0"
eslint-utils "^2.0.0"
-"@typescript-eslint/parser@^4.18.0":
- version "4.18.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.18.0.tgz#a211edb14a69fc5177054bec04c95b185b4dde21"
- integrity sha512-W3z5S0ZbecwX3PhJEAnq4mnjK5JJXvXUDBYIYGoweCyWyuvAKfGHvzmpUzgB5L4cRBb+cTu9U/ro66dx7dIimA==
+"@typescript-eslint/parser@^4.19.0":
+ version "4.19.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.19.0.tgz#4ae77513b39f164f1751f21f348d2e6cb2d11128"
+ integrity sha512-/uabZjo2ZZhm66rdAu21HA8nQebl3lAIDcybUoOxoI7VbZBYavLIwtOOmykKCJy+Xq6Vw6ugkiwn8Js7D6wieA==
dependencies:
- "@typescript-eslint/scope-manager" "4.18.0"
- "@typescript-eslint/types" "4.18.0"
- "@typescript-eslint/typescript-estree" "4.18.0"
+ "@typescript-eslint/scope-manager" "4.19.0"
+ "@typescript-eslint/types" "4.19.0"
+ "@typescript-eslint/typescript-estree" "4.19.0"
debug "^4.1.1"
-"@typescript-eslint/scope-manager@4.18.0":
- version "4.18.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.18.0.tgz#d75b55234c35d2ff6ac945758d6d9e53be84a427"
- integrity sha512-olX4yN6rvHR2eyFOcb6E4vmhDPsfdMyfQ3qR+oQNkAv8emKKlfxTWUXU5Mqxs2Fwe3Pf1BoPvrwZtwngxDzYzQ==
+"@typescript-eslint/scope-manager@4.19.0":
+ version "4.19.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.19.0.tgz#5e0b49eca4df7684205d957c9856f4e720717a4f"
+ integrity sha512-GGy4Ba/hLXwJXygkXqMzduqOMc+Na6LrJTZXJWVhRrSuZeXmu8TAnniQVKgj8uTRKe4igO2ysYzH+Np879G75g==
dependencies:
- "@typescript-eslint/types" "4.18.0"
- "@typescript-eslint/visitor-keys" "4.18.0"
+ "@typescript-eslint/types" "4.19.0"
+ "@typescript-eslint/visitor-keys" "4.19.0"
-"@typescript-eslint/types@4.18.0":
- version "4.18.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.18.0.tgz#bebe323f81f2a7e2e320fac9415e60856267584a"
- integrity sha512-/BRociARpj5E+9yQ7cwCF/SNOWwXJ3qhjurMuK2hIFUbr9vTuDeu476Zpu+ptxY2kSxUHDGLLKy+qGq2sOg37A==
+"@typescript-eslint/types@4.19.0":
+ version "4.19.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.19.0.tgz#5181d5d2afd02e5b8f149ebb37ffc8bd7b07a568"
+ integrity sha512-A4iAlexVvd4IBsSTNxdvdepW0D4uR/fwxDrKUa+iEY9UWvGREu2ZyB8ylTENM1SH8F7bVC9ac9+si3LWNxcBuA==
-"@typescript-eslint/typescript-estree@4.18.0":
- version "4.18.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.18.0.tgz#756d3e61da8c16ab99185532c44872f4cd5538cb"
- integrity sha512-wt4xvF6vvJI7epz+rEqxmoNQ4ZADArGQO9gDU+cM0U5fdVv7N+IAuVoVAoZSOZxzGHBfvE3XQMLdy+scsqFfeg==
+"@typescript-eslint/typescript-estree@4.19.0":
+ version "4.19.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.19.0.tgz#8a709ffa400284ab72df33376df085e2e2f61147"
+ integrity sha512-3xqArJ/A62smaQYRv2ZFyTA+XxGGWmlDYrsfZG68zJeNbeqRScnhf81rUVa6QG4UgzHnXw5VnMT5cg75dQGDkA==
dependencies:
- "@typescript-eslint/types" "4.18.0"
- "@typescript-eslint/visitor-keys" "4.18.0"
+ "@typescript-eslint/types" "4.19.0"
+ "@typescript-eslint/visitor-keys" "4.19.0"
debug "^4.1.1"
globby "^11.0.1"
is-glob "^4.0.1"
semver "^7.3.2"
tsutils "^3.17.1"
-"@typescript-eslint/visitor-keys@4.18.0":
- version "4.18.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.18.0.tgz#4e6fe2a175ee33418318a029610845a81e2ff7b6"
- integrity sha512-Q9t90JCvfYaN0OfFUgaLqByOfz8yPeTAdotn/XYNm5q9eHax90gzdb+RJ6E9T5s97Kv/UHWKERTmqA0jTKAEHw==
+"@typescript-eslint/visitor-keys@4.19.0":
+ version "4.19.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.19.0.tgz#cbea35109cbd9b26e597644556be4546465d8f7f"
+ integrity sha512-aGPS6kz//j7XLSlgpzU2SeTqHPsmRYxFztj2vPuMMFJXZudpRSehE3WCV+BaxwZFvfAqMoSd86TEuM0PQ59E/A==
dependencies:
- "@typescript-eslint/types" "4.18.0"
+ "@typescript-eslint/types" "4.19.0"
eslint-visitor-keys "^2.0.0"
"@ungap/promise-all-settled@1.1.2":
@@ -2109,12 +2104,7 @@ acorn@^7.1.1, acorn@^7.4.0:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
-acorn@^8.0.4:
- version "8.0.5"
- resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.0.5.tgz#a3bfb872a74a6a7f661bc81b9849d9cac12601b7"
- integrity sha512-v+DieK/HJkJOpFBETDJioequtc3PfxsWMaxIdIwujtF7FEV/MAyDQLlm6/zPvr7Mix07mLh6ccVwIsloceodlg==
-
-acorn@^8.0.5:
+acorn@^8.0.4, acorn@^8.0.5:
version "8.1.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.1.0.tgz#52311fd7037ae119cbb134309e901aa46295b3fe"
integrity sha512-LWCF/Wn0nfHOmJ9rzQApGnxnvgfROzGilS8936rqN/lfcYkY9MYZzdMqN+2NJ4SlTc+m5HiSa+kNfDtI64dwUA==
@@ -2161,9 +2151,9 @@ ajv@^6.1.0, ajv@^6.10.0, ajv@^6.12.2, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5:
uri-js "^4.2.2"
ajv@^7.0.2:
- version "7.1.1"
- resolved "https://registry.yarnpkg.com/ajv/-/ajv-7.1.1.tgz#1e6b37a454021fa9941713f38b952fc1c8d32a84"
- integrity sha512-ga/aqDYnUy/o7vbsRTFhhTsNeXiYb5JWDIcRIeZfwRNCefwjNTVYCGdGSUrEmiu3yDK3vFvNbgJxvrQW4JXrYQ==
+ version "7.2.3"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-7.2.3.tgz#ca78d1cf458d7d36d1c3fa0794dd143406db5772"
+ integrity sha512-idv5WZvKVXDqKralOImQgPM9v6WOdLNa0IY3B3doOjw/YxRGT8I+allIJ6kd7Uaj+SF1xZUSU+nPM5aDNBVtnw==
dependencies:
fast-deep-equal "^3.1.1"
json-schema-traverse "^1.0.0"
@@ -2990,9 +2980,9 @@ camelcase@^6.0.0, camelcase@^6.2.0:
integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==
caniuse-lite@^1.0.30001181, caniuse-lite@^1.0.30001196:
- version "1.0.30001196"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001196.tgz#00518a2044b1abf3e0df31fadbe5ed90b63f4e64"
- integrity sha512-CPvObjD3ovWrNBaXlAIGWmg2gQQuJ5YhuciUOjPRox6hIQttu8O+b51dx6VIpIY9ESd2d0Vac1RKpICdG4rGUg==
+ version "1.0.30001203"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001203.tgz#a7a34df21a387d9deffcd56c000b8cf5ab540580"
+ integrity sha512-/I9tvnzU/PHMH7wBPrfDMSuecDeUKerjCPX7D0xBbaJZPxoT9m+yYxt0zCTkcijCkjTdim3H56Zm0i5Adxch4w==
capture-exit@^2.0.0:
version "2.0.0"
@@ -3562,14 +3552,14 @@ copy-descriptor@^0.1.0:
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
-copy-webpack-plugin@^8.0.0:
- version "8.0.0"
- resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-8.0.0.tgz#3db5efb80d492127507303d1842e35011e2f318f"
- integrity sha512-sqGe2FsB67wV/De+sz5azQklADe4thN016od6m7iK9KbjrSc1SEgg5QZ0LN+jGx5aZR52CbuXbqOhoIbqzzXlA==
+copy-webpack-plugin@^8.1.0:
+ version "8.1.0"
+ resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-8.1.0.tgz#0b53170db798ed301439536a02f2868ff63291a0"
+ integrity sha512-Soiq8kXI2AZkpw3dSp18u6oU2JonC7UKv3UdXsKOmT1A5QT46ku9+6c0Qy29JDbSavQJNN1/eKGpd3QNw+cZWg==
dependencies:
fast-glob "^3.2.5"
glob-parent "^5.1.1"
- globby "^11.0.2"
+ globby "^11.0.3"
normalize-path "^3.0.0"
p-limit "^3.1.0"
schema-utils "^3.0.0"
@@ -3683,10 +3673,10 @@ crypto-random-string@^2.0.0:
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==
-css-loader@^5.1.3:
- version "5.1.3"
- resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.1.3.tgz#87f6fc96816b20debe3cf682f85c7e56a963d0d1"
- integrity sha512-CoPZvyh8sLiGARK3gqczpfdedbM74klGWurF2CsNZ2lhNaXdLIUks+3Mfax3WBeRuHoglU+m7KG/+7gY6G4aag==
+css-loader@^5.2.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.2.0.tgz#a9ecda190500863673ce4434033710404efbff00"
+ integrity sha512-MfRo2MjEeLXMlUkeUwN71Vx5oc6EJnx5UQ4Yi9iUtYQvrPtwLUucYptz0hc6n++kdNcyF5olYBS4vPjJDAcLkw==
dependencies:
camelcase "^6.2.0"
cssesc "^3.0.0"
@@ -3731,7 +3721,7 @@ cssom@~0.3.6:
resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a"
integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==
-cssstyle@^2.2.0, cssstyle@^2.3.0:
+cssstyle@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852"
integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==
@@ -3892,7 +3882,7 @@ decamelize@^4.0.0:
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837"
integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==
-decimal.js@^10.2.0, decimal.js@^10.2.1:
+decimal.js@^10.2.1:
version "10.2.1"
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.1.tgz#238ae7b0f0c793d3e3cea410108b35a2c01426a3"
integrity sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw==
@@ -4057,9 +4047,9 @@ detect-newline@^3.0.0:
integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
detect-node@^2.0.4:
- version "2.0.4"
- resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c"
- integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.5.tgz#9d270aa7eaa5af0b72c4c9d9b814e7f4ce738b79"
+ integrity sha512-qi86tE6hRcFHy8jI1m2VG+LaPUR1LhqDa5G8tVjuUXmOrpuAgqsA1pN0+ldgr3aKUH+QLI9hCY/OcRYisERejw==
dezalgo@^1.0.0:
version "1.0.3"
@@ -4277,9 +4267,9 @@ ee-first@1.1.1:
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
electron-to-chromium@^1.3.649:
- version "1.3.681"
- resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.681.tgz#facd915ae46a020e8be566a2ecdc0b9444439be9"
- integrity sha512-W6uYvSUTHuyX2DZklIESAqx57jfmGjUkd7Z3RWqLdj9Mmt39ylhBuvFXlskQnvBHj0MYXIeQI+mjiwVddZLSvA==
+ version "1.3.693"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.693.tgz#5089c506a925c31f93fcb173a003a22e341115dd"
+ integrity sha512-vUdsE8yyeu30RecppQtI+XTz2++LWLVEIYmzeCaCRLSdtKZ2eXqdJcrs85KwLiPOPVc6PELgWyXBsfqIvzGZag==
elegant-spinner@^1.0.1:
version "1.0.1"
@@ -4360,9 +4350,9 @@ entities@^2.0.0:
integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
env-paths@^2.2.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43"
- integrity sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2"
+ integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==
envinfo@^7.3.1, envinfo@^7.7.3:
version "7.7.4"
@@ -4466,18 +4456,6 @@ escape-string-regexp@^2.0.0:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344"
integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==
-escodegen@^1.14.1:
- version "1.14.3"
- resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503"
- integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==
- dependencies:
- esprima "^4.0.1"
- estraverse "^4.2.0"
- esutils "^2.0.2"
- optionator "^0.8.1"
- optionalDependencies:
- source-map "~0.6.1"
-
escodegen@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd"
@@ -4626,7 +4604,7 @@ esrecurse@^4.3.0:
dependencies:
estraverse "^5.2.0"
-estraverse@^4.1.1, estraverse@^4.2.0:
+estraverse@^4.1.1:
version "4.3.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
@@ -4667,9 +4645,9 @@ events@^3.2.0:
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
eventsource@^1.0.7:
- version "1.0.7"
- resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.0.7.tgz#8fbc72c93fcd34088090bc0a4e64f4b5cee6d8d0"
- integrity sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.1.0.tgz#00e8ca7c92109e94b0ddf32dac677d841028cfaf"
+ integrity sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg==
dependencies:
original "^1.0.0"
@@ -5448,9 +5426,9 @@ glob-parent@^3.1.0:
path-dirname "^1.0.0"
glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@^5.1.1, glob-parent@~5.1.0:
- version "5.1.1"
- resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229"
- integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+ integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
dependencies:
is-glob "^4.0.1"
@@ -5513,7 +5491,7 @@ globals@^13.6.0:
dependencies:
type-fest "^0.20.2"
-globby@^11.0.0, globby@^11.0.1, globby@^11.0.2:
+globby@^11.0.0, globby@^11.0.1:
version "11.0.2"
resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.2.tgz#1af538b766a3b540ebfb58a32b2e2d5897321d83"
integrity sha512-2ZThXDvvV8fYFRVIxnrMQBipZQDr7MxKAmQK1vujaj9/7eF0efG7BPUKJ7jP7G5SLF37xKDXvO4S/KKLj/Z0og==
@@ -5525,6 +5503,18 @@ globby@^11.0.0, globby@^11.0.1, globby@^11.0.2:
merge2 "^1.3.0"
slash "^3.0.0"
+globby@^11.0.3:
+ version "11.0.3"
+ resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.3.tgz#9b1f0cb523e171dd1ad8c7b2a9fb4b644b9593cb"
+ integrity sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg==
+ dependencies:
+ array-union "^2.1.0"
+ dir-glob "^3.0.1"
+ fast-glob "^3.1.1"
+ ignore "^5.1.4"
+ merge2 "^1.3.0"
+ slash "^3.0.0"
+
globby@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c"
@@ -5711,10 +5701,10 @@ hosted-git-info@^2.1.4, hosted-git-info@^2.7.1:
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
-hosted-git-info@^3.0.6:
- version "3.0.8"
- resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-3.0.8.tgz#6e35d4cc87af2c5f816e4cb9ce350ba87a3f370d"
- integrity sha512-aXpmwoOhRBrw6X3j0h5RloK4x1OzsxMPyxqIHyNfSe2pypkVTZFpEiRoSipPEPlMrh0HW/XsjkJ5WgnCirpNUw==
+hosted-git-info@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.0.1.tgz#710ef5452ea429a844abc33c981056e7371edab7"
+ integrity sha512-eT7NrxAsppPRQEBSwKSosReE+v8OzABwEScQYk5d4uxaEPlzxTIku7LINXtBGalthkLhJnq5lBI89PfK43zAKg==
dependencies:
lru-cache "^6.0.0"
@@ -6404,9 +6394,9 @@ is-path-inside@^2.1.0:
path-is-inside "^1.0.2"
is-path-inside@^3.0.1:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017"
- integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
+ integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
is-plain-obj@^1.0.0, is-plain-obj@^1.1.0:
version "1.1.0"
@@ -6489,6 +6479,11 @@ is-typedarray@^1.0.0, is-typedarray@~1.0.0:
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
+is-unicode-supported@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7"
+ integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==
+
is-utf8@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
@@ -7049,7 +7044,7 @@ jquery-ui@>=1.8.0:
resolved "https://registry.yarnpkg.com/jquery-ui/-/jquery-ui-1.12.1.tgz#bcb4045c8dd0539c134bc1488cdd3e768a7a9e51"
integrity sha1-vLQEXI3QU5wTS8FIjN0+dop6nlE=
-jquery@>=1.8.0, jquery@^3.5.1:
+jquery@>=1.8.0, jquery@^3.5.1, jquery@~3.5.1:
version "3.5.1"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.5.1.tgz#d7b4d08e1bfdb86ad2f1a3d039ea17304717abb5"
integrity sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg==
@@ -7089,39 +7084,7 @@ jsdom-global@^3.0.2:
resolved "https://registry.yarnpkg.com/jsdom-global/-/jsdom-global-3.0.2.tgz#6bd299c13b0c4626b2da2c0393cd4385d606acb9"
integrity sha1-a9KZwTsMRiay2iwDk81DhdYGrLk=
-jsdom@^16.4.0:
- version "16.4.0"
- resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.4.0.tgz#36005bde2d136f73eee1a830c6d45e55408edddb"
- integrity sha512-lYMm3wYdgPhrl7pDcRmvzPhhrGVBeVhPIqeHjzeiHN3DFmD1RBpbExbi8vU7BJdH8VAZYovR8DMt0PNNDM7k8w==
- dependencies:
- abab "^2.0.3"
- acorn "^7.1.1"
- acorn-globals "^6.0.0"
- cssom "^0.4.4"
- cssstyle "^2.2.0"
- data-urls "^2.0.0"
- decimal.js "^10.2.0"
- domexception "^2.0.1"
- escodegen "^1.14.1"
- html-encoding-sniffer "^2.0.1"
- is-potential-custom-element-name "^1.0.0"
- nwsapi "^2.2.0"
- parse5 "5.1.1"
- request "^2.88.2"
- request-promise-native "^1.0.8"
- saxes "^5.0.0"
- symbol-tree "^3.2.4"
- tough-cookie "^3.0.1"
- w3c-hr-time "^1.0.2"
- w3c-xmlserializer "^2.0.0"
- webidl-conversions "^6.1.0"
- whatwg-encoding "^1.0.5"
- whatwg-mimetype "^2.3.0"
- whatwg-url "^8.0.0"
- ws "^7.2.3"
- xml-name-validator "^3.0.0"
-
-jsdom@^16.5.1:
+jsdom@^16.4.0, jsdom@^16.5.1:
version "16.5.1"
resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.5.1.tgz#4ced6bbd7b77d67fb980e64d9e3e6fb900f97dd6"
integrity sha512-pF73EOsJgwZekbDHEY5VO/yKXUkab/DuvrQB/ANVizbr6UAHJsDdHXuotZYwkJSGQl1JM+ivXaqY+XBDDL4TiA==
@@ -7625,7 +7588,7 @@ lodash@4.x, lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lo
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
-log-symbols@4.0.0, log-symbols@^4.0.0:
+log-symbols@4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920"
integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==
@@ -7639,6 +7602,14 @@ log-symbols@^1.0.2:
dependencies:
chalk "^1.0.0"
+log-symbols@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"
+ integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==
+ dependencies:
+ chalk "^4.1.0"
+ is-unicode-supported "^0.1.0"
+
log-update@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/log-update/-/log-update-2.3.0.tgz#88328fd7d1ce7938b29283746f0b1bc126b24708"
@@ -7771,9 +7742,9 @@ map-obj@^2.0.0:
integrity sha1-plzSkIepJZi4eRJXpSPgISIqwfk=
map-obj@^4.0.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.1.0.tgz#b91221b542734b9f14256c0132c897c5d7256fd5"
- integrity sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g==
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.2.0.tgz#0e8bc823e2aaca8a0942567d12ed14f389eec153"
+ integrity sha512-NAq0fCmZYGz9UFEQyndp7sisrow4GroyGeKluyKC/chuITZsPyOyC1UJZPJlVFImhXdROIP5xqouRLThT3BbpQ==
map-visit@^1.0.0:
version "1.0.0"
@@ -8239,11 +8210,16 @@ nan@^2.12.1, nan@^2.13.2:
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19"
integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
-nanoid@3.1.20, nanoid@^3.1.20:
+nanoid@3.1.20:
version "3.1.20"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788"
integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==
+nanoid@^3.1.20:
+ version "3.1.22"
+ resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.22.tgz#b35f8fb7d151990a8aebd5aa5015c03cf726f844"
+ integrity sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ==
+
nanomatch@^1.2.9:
version "1.2.13"
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
@@ -8352,9 +8328,9 @@ node-modules-regexp@^1.0.0:
integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=
node-notifier@^8.0.0:
- version "8.0.1"
- resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-8.0.1.tgz#f86e89bbc925f2b068784b31f382afdc6ca56be1"
- integrity sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==
+ version "8.0.2"
+ resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-8.0.2.tgz#f3167a38ef0d2c8a866a83e318c1ba0efeb702c5"
+ integrity sha512-oJP/9NAdd9+x2Q+rfphB2RJCHjod70RcRLjosiPMMu5gjIfwVnOUGq2nbTjTUbmy0DJ/tFIVT30+Qe3nzl4TJg==
dependencies:
growly "^1.3.0"
is-wsl "^2.2.0"
@@ -8447,13 +8423,13 @@ normalize-package-data@^2.0.0, normalize-package-data@^2.3.0, normalize-package-
validate-npm-package-license "^3.0.1"
normalize-package-data@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.0.tgz#1f8a7c423b3d2e85eb36985eaf81de381d01301a"
- integrity sha512-6lUjEI0d3v6kFrtgA/lOx4zHCWULXsFNIjHolnZCKCTLA6m/G625cdn3O7eNmT0iD3jfo6HZ9cdImGZwf21prw==
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.2.tgz#cae5c410ae2434f9a6c1baa65d5bc3b9366c8699"
+ integrity sha512-6CdZocmfGaKnIHPVFhJJZ3GuR8SsLKvDANFp47Jmy51aKIr8akjAWTSxtpI+MBgBFdSMRyo4hMpDlT6dTffgZg==
dependencies:
- hosted-git-info "^3.0.6"
- resolve "^1.17.0"
- semver "^7.3.2"
+ hosted-git-info "^4.0.1"
+ resolve "^1.20.0"
+ semver "^7.3.4"
validate-npm-package-license "^3.0.1"
normalize-path@^2.1.1:
@@ -9001,11 +8977,6 @@ parse-url@^5.0.0:
parse-path "^4.0.0"
protocols "^1.4.0"
-parse5@5.1.1:
- version "5.1.1"
- resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178"
- integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==
-
parse5@6.0.1, parse5@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
@@ -9494,9 +9465,11 @@ qs@6.7.0:
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
qs@^6.4.0, qs@^6.9.4:
- version "6.9.6"
- resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.6.tgz#26ed3c8243a431b2924aca84cc90471f35d5a0ee"
- integrity sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ==
+ version "6.10.0"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.0.tgz#8b6519121ab291c316a3e4d49cecf6d13d8c7fe5"
+ integrity sha512-yjACOWijC6L/kmPZZAsVBNY2zfHSIbpdpL977quseu56/8BZ2LoF5axK2bGhbzhVKt7V9xgWTtpyLbxwIoER0Q==
+ dependencies:
+ side-channel "^1.0.4"
qs@~6.5.2:
version "6.5.2"
@@ -9878,7 +9851,7 @@ request-promise-core@1.1.4:
dependencies:
lodash "^4.17.19"
-request-promise-native@^1.0.8, request-promise-native@^1.0.9:
+request-promise-native@^1.0.9:
version "1.0.9"
resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.9.tgz#e407120526a5efdc9a39b28a5679bf47b9d9dc28"
integrity sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==
@@ -9967,7 +9940,7 @@ resolve-url@^0.2.1:
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
-resolve@^1.10.0, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.9.0:
+resolve@^1.10.0, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.20.0, resolve@^1.9.0:
version "1.20.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
@@ -10056,13 +10029,20 @@ run-queue@^1.0.0, run-queue@^1.0.3:
dependencies:
aproba "^1.1.1"
-rxjs@^6.3.3, rxjs@^6.4.0:
+rxjs@^6.3.3, rxjs@^6.4.0, rxjs@^6.6.6:
version "6.6.6"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.6.tgz#14d8417aa5a07c5e633995b525e1e3c0dec03b70"
integrity sha512-/oTwee4N4iWzAMAL9xdGKjkEHmIwupR3oXbQjCKywF1BeFohswF3vZdogbmEF6pZkOsXTzWkrZszrWpQTByYVg==
dependencies:
tslib "^1.9.0"
+rxjs@next:
+ version "7.0.0-beta.13"
+ resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.0.0-beta.13.tgz#f8dd03f7aa414c9f7fde831431e54c4534ecfa39"
+ integrity sha512-//ll3EfI5SgTfrS5DUjc/uEmyazS0vUDMHqa/GbPR9ubdVLtbrv2bK3y75JyZPfYl1Bq2JbR/IbByhfzfpJyvw==
+ dependencies:
+ tslib "~2.1.0"
+
safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
@@ -10118,7 +10098,7 @@ sass-loader@^11.0.1:
klona "^2.0.4"
neo-async "^2.6.2"
-saxes@^5.0.0, saxes@^5.0.1:
+saxes@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d"
integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==
@@ -10321,6 +10301,15 @@ shellwords@^0.1.1:
resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==
+side-channel@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
+ integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==
+ dependencies:
+ call-bind "^1.0.0"
+ get-intrinsic "^1.0.2"
+ object-inspect "^1.9.0"
+
signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
@@ -10665,9 +10654,9 @@ strict-uri-encode@^2.0.0:
integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY=
string-length@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.1.tgz#4a973bf31ef77c4edbceadd6af2611996985f8a1"
- integrity sha512-PKyXUd0LK0ePjSOnWn34V2uD6acUWev9uy0Ft05k0E8xRW+SKcA0F7eMr7h5xlzfn+4O3N+55rduYyet3Jk+jw==
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a"
+ integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==
dependencies:
char-regex "^1.0.2"
strip-ansi "^6.0.0"
@@ -11023,9 +11012,9 @@ terser@^4.6.3:
source-map-support "~0.5.12"
terser@^5.5.1:
- version "5.6.0"
- resolved "https://registry.yarnpkg.com/terser/-/terser-5.6.0.tgz#138cdf21c5e3100b1b3ddfddf720962f88badcd2"
- integrity sha512-vyqLMoqadC1uR0vywqOZzriDYzgEkNJFK4q9GeyOBHIbiECHiWLKcWfbQWAUaPfxkjDhapSlZB9f7fkMrvkVjA==
+ version "5.6.1"
+ resolved "https://registry.yarnpkg.com/terser/-/terser-5.6.1.tgz#a48eeac5300c0a09b36854bf90d9c26fb201973c"
+ integrity sha512-yv9YLFQQ+3ZqgWCUk+pvNJwgUTdlIxUk1WTN+RnaFJe2L7ipG2csPT0ra2XRm7Cs8cxN7QXmK1rFzEwYEQkzXw==
dependencies:
commander "^2.20.0"
source-map "~0.7.2"
@@ -11193,15 +11182,6 @@ tough-cookie@^2.3.3, tough-cookie@~2.5.0:
psl "^1.1.28"
punycode "^2.1.1"
-tough-cookie@^3.0.1:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2"
- integrity sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==
- dependencies:
- ip-regex "^2.1.0"
- psl "^1.1.28"
- punycode "^2.1.1"
-
tough-cookie@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4"
@@ -11306,15 +11286,15 @@ tslib@^1.8.1, tslib@^1.9.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
-tslib@^2.0.3:
+tslib@^2.0.3, tslib@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==
tsutils@^3.17.1:
- version "3.20.0"
- resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.20.0.tgz#ea03ea45462e146b53d70ce0893de453ff24f698"
- integrity sha512-RYbuQuvkhuqVeXweWT3tJLKOEJ/UUw9GjNEZGWdrLLlM+611o1gwLHBpxoFJKKl25fLprp2eVthtKs5JOrNeXg==
+ version "3.21.0"
+ resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
+ integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
dependencies:
tslib "^1.8.1"
@@ -11405,9 +11385,9 @@ typescript@^4.2.3:
integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==
uglify-js@^3.1.4:
- version "3.13.0"
- resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.0.tgz#66ed69f7241f33f13531d3d51d5bcebf00df7f69"
- integrity sha512-TWYSWa9T2pPN4DIJYbU9oAjQx+5qdV5RUDxwARg8fmJZrD/V27Zj0JngW5xg1DFz42G0uDYl2XhzF6alSzD62w==
+ version "3.13.1"
+ resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.1.tgz#2749d4b8b5b7d67460b4a418023ff73c3fefa60a"
+ integrity sha512-EWhx3fHy3M9JbaeTnO+rEqzCe1wtyQClv6q3YWq0voOj4E+bMZBErVS1GAHPDiRGONYq34M1/d8KuQMgvi6Gjw==
uid-number@0.0.6:
version "0.0.6"
@@ -11630,9 +11610,9 @@ uuid@^8.3.0, uuid@^8.3.2:
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
v8-compile-cache@^2.0.3, v8-compile-cache@^2.2.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz#9471efa3ef9128d2f7c6a7ca39c4dd6b5055b132"
- integrity sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
+ integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
v8-to-istanbul@^7.0.0:
version "7.1.0"
@@ -11837,10 +11817,10 @@ webpack-sources@^2.1.1:
source-list-map "^2.0.1"
source-map "^0.6.1"
-webpack@^5.27.0:
- version "5.27.0"
- resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.27.0.tgz#387458f83142253f2759e22415dc1359746e6940"
- integrity sha512-So7grHu//UyJ+80VrNHjWwW6WSZkfWWD6a7NV/88r8G92PO6TYOGzbtTiZBwbPVkx6LVP8OYvHD+IxuJ2KBz4g==
+webpack@^5.28.0:
+ version "5.28.0"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.28.0.tgz#0de8bcd706186b26da09d4d1e8cbd3e4025a7c2f"
+ integrity sha512-1xllYVmA4dIvRjHzwELgW4KjIU1fW4PEuEnjsylz7k7H5HgPOctIq7W1jrt3sKH9yG5d72//XWzsHhfoWvsQVg==
dependencies:
"@types/eslint-scope" "^3.7.0"
"@types/estree" "^0.0.46"
@@ -12084,11 +12064,6 @@ ws@^6.2.1:
dependencies:
async-limiter "~1.0.0"
-ws@^7.2.3:
- version "7.4.3"
- resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.3.tgz#1f9643de34a543b8edb124bdcbc457ae55a6e5cd"
- integrity sha512-hr6vCR76GsossIRsr8OLR9acVVm1jyfEWvhbNjtgPOrfvAlKzvyeg/P6r8RuDjRyrcQoPQT7K0DGEPc7Ae6jzA==
-
ws@^7.4.4:
version "7.4.4"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.4.tgz#383bc9742cb202292c9077ceab6f6047b17f2d59"
@@ -12140,9 +12115,9 @@ yallist@^4.0.0:
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
yaml@^1.10.0, yaml@^1.7.2:
- version "1.10.0"
- resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e"
- integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==
+ version "1.10.2"
+ resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
+ integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
yargs-parser@20.2.4:
version "20.2.4"
@@ -12150,9 +12125,9 @@ yargs-parser@20.2.4:
integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==
yargs-parser@20.x, yargs-parser@^20.2.2, yargs-parser@^20.2.3:
- version "20.2.6"
- resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.6.tgz#69f920addf61aafc0b8b89002f5d66e28f2d8b20"
- integrity sha512-AP1+fQIWSM/sMiET8fyayjx/J+JmTPt2Mr0FkrgqB4todtfa53sOsrSAcIrJRD5XS20bKUwaDIuMkWKCEiQLKA==
+ version "20.2.7"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.7.tgz#61df85c113edfb5a7a4e36eb8aa60ef423cbc90a"
+ integrity sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==
yargs-parser@^13.1.2:
version "13.1.2"