UNPKG

slickgrid-react

Version:
864 lines 81.2 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { autoAddEditorFormatterToColumnsWithEditor, BackendUtilityService, collectionObserver, CollectionService, emptyElement, EventNamingStyle, ExtensionService, ExtensionUtility, FilterFactory, FilterService, GridEventService, GridService, GridStateService, HeaderGroupingService, isColumnDateType, PaginationService, ResizerService, SharedService, SlickDataView, SlickEventHandler, SlickGrid, SlickgridConfig, SlickGroupItemMetadataProvider, SortService, TreeDataService, } from '@slickgrid-universal/common'; import { SlickFooterComponent } from '@slickgrid-universal/custom-footer-component'; import { SlickEmptyWarningComponent } from '@slickgrid-universal/empty-warning-component'; import { EventPubSubService } from '@slickgrid-universal/event-pub-sub'; import { SlickPaginationComponent } from '@slickgrid-universal/pagination-component'; import { deepCopy, extend } from '@slickgrid-universal/utils'; import { dequal } from 'dequal/lite'; import React from 'react'; import { I18nextContext } from '../contexts/i18nextContext.js'; import { GlobalGridOptions } from '../global-grid-options.js'; import { loadReactComponentDynamically } from '../services/reactUtils.js'; import { GlobalContainerService } from '../services/singletons.js'; import { TranslaterI18NextService } from '../services/translaterI18Next.service.js'; import { disposeAllSubscriptions } from '../services/utilities.js'; const WARN_NO_PREPARSE_DATE_SIZE = 10000; // data size to warn user when pre-parse isn't enabled export class SlickgridReact extends React.Component { props; // i18next has to be provided by the external user through our `I18nextProvider` static contextType = I18nextContext; _mounted = false; setStateValue(key, value, callback) { if (this.state?.[key] === value) { return; } if (!this._mounted) { this.state = this.state || {}; this.state[key] = value; return; } this.setState(() => { const result = {}; result[key] = value; return result; }, callback); } _columns = []; _currentDatasetLength = 0; _dataset = null; _options = {}; _elm; _collectionObservers = []; _eventHandler; _eventPubSubService; _hideHeaderRowAfterPageLoad = false; _i18next; _isAutosizeColsCalled = false; _isGridInitialized = false; _isDatasetInitialized = false; _isDatasetHierarchicalInitialized = false; _isPaginationInitialized = false; _isLocalGrid = true; _paginationOptions; _registeredResources = []; _scrollEndCalled = false; get options() { return this._options || {}; } set options(options) { let mergedOptions; // if we already have grid options, when grid was already initialized, we'll merge with those options // else we'll merge with global grid options if (this.grid?.getOptions) { mergedOptions = extend(true, {}, this.grid.getOptions(), options); } else { mergedOptions = this.mergeGridOptions(options); } if (this.sharedService?.gridOptions && this.grid?.setOptions) { this.sharedService.gridOptions = mergedOptions; this.grid.setOptions(mergedOptions, false, true); // make sure to supressColumnCheck (3rd arg) to avoid problem with changeColumnsArrangement() and custom grid view this.grid.reRenderColumns(true); // then call a re-render since we did supressColumnCheck on previous setOptions } this._options = mergedOptions; } groupItemMetadataProvider; backendServiceApi; metrics; showPagination = false; serviceList = []; subscriptions = []; // components slickEmptyWarning; slickFooter; slickPagination; // services backendUtilityService; collectionService; extensionService; extensionUtility; filterFactory; filterService; gridEventService; gridService; gridStateService; groupingService; headerGroupingService; resizerService; rxjs; sharedService; sortService; treeDataService; dataView; grid; totalItems = 0; extensions; instances = null; static defaultProps = { containerService: GlobalContainerService, translaterService: new TranslaterI18NextService(), gridId: '', dataset: [], columns: [], options: {}, }; get dataset() { return this.dataView?.getItems() || []; } set dataset(newDataset) { const prevDatasetLn = this._currentDatasetLength; const isDatasetEqual = dequal(newDataset, this.dataset || []); let data = this._options?.enableDeepCopyDatasetOnPageLoad ? deepCopy(newDataset) : newDataset; // when Tree Data is enabled and we don't yet have the hierarchical dataset filled, we can force a convert+sort of the array if (this.grid && this.options?.enableTreeData && Array.isArray(newDataset) && (newDataset.length > 0 || newDataset.length !== prevDatasetLn || !isDatasetEqual)) { this._isDatasetHierarchicalInitialized = false; data = this.sortTreeDataset(newDataset, !isDatasetEqual); // if dataset changed, then force a refresh anyway } this.refreshGridData(data || []); this._currentDatasetLength = (newDataset || []).length; // expand/autofit columns on first page load // we can assume that if the prevDataset was empty then we are on first load if (this.grid && this.options.autoFitColumnsOnFirstLoad && prevDatasetLn === 0 && !this._isAutosizeColsCalled) { this.grid.autosizeColumns(); this._isAutosizeColsCalled = true; } } get datasetHierarchical() { return this.sharedService.hierarchicalDataset; } set datasetHierarchical(newHierarchicalDataset) { const isDatasetEqual = dequal(newHierarchicalDataset, this.sharedService?.hierarchicalDataset ?? []); const prevFlatDatasetLn = this._currentDatasetLength; if (this.sharedService) { this.sharedService.hierarchicalDataset = newHierarchicalDataset; } if (newHierarchicalDataset && this.props.columns && this.filterService?.clearFilters) { this.filterService.clearFilters(); } // when a hierarchical dataset is set afterward, we can reset the flat dataset and call a tree data sort that will overwrite the flat dataset if (this.dataView && newHierarchicalDataset && this.grid && this.sortService?.processTreeDataInitialSort) { this.dataView.setItems([], this._options?.datasetIdPropertyName ?? 'id'); this.sortService.processTreeDataInitialSort(); this.treeDataService.initHierarchicalTree(); // we also need to reset/refresh the Tree Data filters because if we inserted new item(s) then it might not show up without doing this refresh // however we need to queue our process until the flat dataset is ready, so we can queue a microtask to execute the DataView refresh only after everything is ready queueMicrotask(() => { const flatDatasetLn = this.dataView?.getItemCount() ?? 0; if (flatDatasetLn > 0 && (flatDatasetLn !== prevFlatDatasetLn || !isDatasetEqual)) { this.filterService.refreshTreeDataFilters(); } }); } this._isDatasetHierarchicalInitialized = true; } get paginationService() { return this.state?.paginationService; } set paginationService(value) { this.setStateValue('paginationService', value); } constructor(props) { super(props); this.props = props; const slickgridConfig = new SlickgridConfig(); this._eventHandler = new SlickEventHandler(); this.showPagination = false; // check if the user wants to hide the header row from the start // we only want to do this check once in the constructor this._hideHeaderRowAfterPageLoad = props.options?.showHeaderRow === false; this._options = this.mergeGridOptions(props.options || {}); // initialize and assign all Service Dependencies this._eventPubSubService = new EventPubSubService(); this._eventPubSubService.eventNamingStyle = EventNamingStyle.camelCase; this.backendUtilityService = new BackendUtilityService(); this.gridEventService = new GridEventService(); this.sharedService = new SharedService(); this.collectionService = new CollectionService(this.props.translaterService); this.extensionUtility = new ExtensionUtility(this.sharedService, this.backendUtilityService, this.props.translaterService); this.filterFactory = new FilterFactory(slickgridConfig, this.props.translaterService, this.collectionService); this.filterService = new FilterService(this.filterFactory, this._eventPubSubService, this.sharedService, this.backendUtilityService); this.resizerService = new ResizerService(this._eventPubSubService); this.sortService = new SortService(this.collectionService, this.sharedService, this._eventPubSubService, this.backendUtilityService); this.treeDataService = new TreeDataService(this._eventPubSubService, this.filterService, this.sharedService, this.sortService); this.paginationService = new PaginationService(this._eventPubSubService, this.sharedService, this.backendUtilityService); this.extensionService = new ExtensionService(this.extensionUtility, this.filterService, this._eventPubSubService, this.sharedService, this.sortService, this.treeDataService, this.props.translaterService, () => this.gridService); this.gridStateService = new GridStateService(this.extensionService, this.filterService, this._eventPubSubService, this.sharedService, this.sortService, this.treeDataService); this.gridService = new GridService(this.gridStateService, this.filterService, this._eventPubSubService, this.paginationService, this.sharedService, this.sortService, this.treeDataService); this.headerGroupingService = new HeaderGroupingService(this.extensionUtility); this.serviceList = [ this.extensionService, this.filterService, this.gridEventService, this.gridService, this.gridStateService, this.headerGroupingService, this.paginationService, this.resizerService, this.sortService, this.treeDataService, ]; if (this.props.datasetHierarchical) { this.sharedService.hierarchicalDataset = this.props.datasetHierarchical || []; } // register all Service instances in the container this.props.containerService.registerInstance('PubSubService', this._eventPubSubService); this.props.containerService.registerInstance('EventPubSubService', this._eventPubSubService); this.props.containerService.registerInstance('ExtensionUtility', this.extensionUtility); this.props.containerService.registerInstance('FilterService', this.filterService); this.props.containerService.registerInstance('CollectionService', this.collectionService); this.props.containerService.registerInstance('ExtensionService', this.extensionService); this.props.containerService.registerInstance('GridEventService', this.gridEventService); this.props.containerService.registerInstance('GridService', this.gridService); this.props.containerService.registerInstance('GridStateService', this.gridStateService); this.props.containerService.registerInstance('HeaderGroupingService', this.headerGroupingService); this.props.containerService.registerInstance('PaginationService', this.paginationService); this.props.containerService.registerInstance('ResizerService', this.resizerService); this.props.containerService.registerInstance('SharedService', this.sharedService); this.props.containerService.registerInstance('SortService', this.sortService); this.props.containerService.registerInstance('TranslaterService', this.props.translaterService); this.props.containerService.registerInstance('TreeDataService', this.treeDataService); } get backendService() { return this.options.backendServiceApi?.service; } get eventHandler() { return this._eventHandler; } get isDatasetInitialized() { return this._isDatasetInitialized; } set isDatasetInitialized(isInitialized) { this._isDatasetInitialized = isInitialized; } set isDatasetHierarchicalInitialized(isInitialized) { this._isDatasetHierarchicalInitialized = isInitialized; } get registeredResources() { return this._registeredResources; } componentDidMount() { this._mounted = true; if (this._elm && this._eventPubSubService instanceof EventPubSubService) { this._eventPubSubService.elementSource = this._elm; // React doesn't play well with Custom Events & also the render is called after the constructor which brings a second problem // to fix both issues, we need to do the following: // loop through all component props and subscribe to the ones that startsWith "on", we'll assume that it's the custom events // we'll then call the assigned listener(s) when events are dispatching for (const prop in this.props) { if (prop.startsWith('on')) { const eventCallback = this.props[prop]; if (typeof eventCallback === 'function') { this.subscriptions.push(this._eventPubSubService.subscribe(prop, (data) => { const gridEventName = this._eventPubSubService.getEventNameByNamingConvention(prop, ''); eventCallback.call(null, new CustomEvent(gridEventName, { detail: data })); })); } } } } // save resource refs to register before the grid options are merged and possibly deep copied // since a deep copy of grid options would lose original resource refs but we want to keep them as singleton this._registeredResources = this.options?.externalResources || []; this.initialization(this._eventHandler); this._isGridInitialized = true; // if we have a backendServiceApi and the enablePagination is undefined, we'll assume that we do want to see it, else get that defined value if (!this.hasBackendInfiniteScroll()) { this.options.enablePagination = !!(this.options.backendServiceApi && this.options.enablePagination === undefined ? true : this.options.enablePagination); } if (!this._isPaginationInitialized && !this.props.datasetHierarchical && this._options?.enablePagination && this._isLocalGrid) { this.showPagination = true; this.loadLocalGridPagination(this.dataset); } // recheck the empty warning message after grid is shown so that it works in every use case if (this._options?.enableEmptyDataWarningMessage) { const dataset = this.props.dataset || []; if (Array.isArray(dataset)) { const finalTotalCount = dataset.length; this.displayEmptyDataWarning(finalTotalCount < 1); } } // add dark mode CSS class when enabled if (this.options.darkMode) { this.setDarkMode(true); } this.suggestDateParsingWhenHelpful(); } initialization(eventHandler) { if (!this._columns) { throw new Error('Using `<SlickgridReact>` requires `columns` and it seems that you might have forgot to provide this missing bindable model.'); } this._options.translater = this.props.translaterService; this._eventHandler = eventHandler; this._isAutosizeColsCalled = false; // when detecting a frozen grid, we'll automatically enable the mousewheel scroll handler so that we can scroll from both left/right frozen containers if (this._options && ((this._options.frozenRow !== undefined && this._options.frozenRow >= 0) || (this._options.frozenColumn !== undefined && this._options.frozenColumn >= 0)) && this._options.enableMouseWheelScrollHandler === undefined) { this._options.enableMouseWheelScrollHandler = true; } this._eventPubSubService.eventNamingStyle = this._options?.eventNamingStyle ?? EventNamingStyle.camelCase; this._eventPubSubService.publish(`onBeforeGridCreate`, true); // make sure the dataset is initialized (if not it will throw an error that it cannot getLength of null) this._dataset ||= this.props.dataset || []; this._currentDatasetLength = this._dataset.length; this._options = this.mergeGridOptions(this._options); this._paginationOptions = this._options?.pagination; this.backendServiceApi = this._options?.backendServiceApi; this._isLocalGrid = !this.backendServiceApi; // considered a local grid if it doesn't have a backend service set // inject the I18Next instance when translation is enabled if (this._options?.enableTranslate || this._options?.i18n) { const importErrorMsg = '[Slickgrid-React] Enabling translation requires you to install I18Next in your App and use `I18nextProvider` to provide it. ' + 'Please make sure to first install it via "npm install i18next react-i18next" and then ' + 'use `<I18nextProvider value={i18n}><App/></I18nextProvider>` in your main index.tsx file. ' + 'Visit https://ghiscoding.gitbook.io/slickgrid-react/localization/localization for more info.'; this._i18next = this.context; // Access the context directly if (this.props.translaterService && this._i18next) { this.props.translaterService.i18nInstance = this._i18next; } else { throw new Error(importErrorMsg); } } // unless specified, we'll create an internal postProcess callback (currently only available for GraphQL) if (this.options.backendServiceApi && !this.options.backendServiceApi?.disableInternalPostProcess) { this.createBackendApiInternalPostProcessCallback(this._options); } if (!this.props.customDataView) { const dataviewInlineFilters = (this._options.dataView && this._options.dataView.inlineFilters) || false; let dataViewOptions = { ...this._options.dataView, inlineFilters: dataviewInlineFilters }; if (this._options.draggableGrouping || this._options.enableGrouping) { this.groupItemMetadataProvider = new SlickGroupItemMetadataProvider(); this.sharedService.groupItemMetadataProvider = this.groupItemMetadataProvider; dataViewOptions = { ...dataViewOptions, groupItemMetadataProvider: this.groupItemMetadataProvider }; } this.dataView = new SlickDataView(dataViewOptions, this._eventPubSubService); 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(); // prepare and load all SlickGrid editors, if an async editor is found then we'll also execute it. this._columns = this.loadSlickGridEditors(this.props.columns); // if the user wants to automatically add a Custom Editor Formatter, we need to call the auto add function again if (this._options.autoAddCustomEditorFormatter) { autoAddEditorFormatterToColumnsWithEditor(this._columns, this._options.autoAddCustomEditorFormatter); } // save reference for all columns before they optionally become hidden/visible this.sharedService.allColumns = this._columns; this.sharedService.visibleColumns = this._columns; // after subscribing to potential columns changed, we are ready to create these optional extensions // when we did find some to create (RowMove, RowDetail, RowSelections), it will automatically modify column definitions (by previous subscribe) this.extensionService.createExtensionsBeforeGridCreation(this._columns, this._options); // if user entered some Pinning/Frozen "presets", we need to apply them in the grid options if (this.options.presets?.pinning) { this.options = { ...this.options, ...this.options.presets.pinning }; } // build SlickGrid Grid, also user might optionally pass a custom dataview (e.g. remote model) this.grid = new SlickGrid(`#${this.props.gridId}`, this.props.customDataView || this.dataView, this._columns, this._options, this._eventPubSubService); this.sharedService.dataView = this.dataView; this.sharedService.slickGrid = this.grid; this.sharedService.gridContainerElement = this._elm; if (this.groupItemMetadataProvider) { this.grid.registerPlugin(this.groupItemMetadataProvider); // register GroupItemMetadataProvider when Grouping is enabled } this.extensionService.bindDifferentExtensions(); this.bindDifferentHooks(this.grid, this._options, this.dataView); // when it's a frozen grid, we need to keep the frozen column id for reference if we ever show/hide column from ColumnPicker/GridMenu afterward this.sharedService.frozenVisibleColumnId = this.grid.getFrozenColumnId(); // get any possible Services that user want to register this.registerResources(); // initialize the SlickGrid grid this.grid.init(); // initialized the resizer service only after SlickGrid is initialized // if we don't we end up binding our resize to a grid element that doesn't yet exist in the DOM and the resizer service will fail silently (because it has a try/catch that unbinds the resize without throwing back) const gridContainerElm = this._elm; if (gridContainerElm) { this.resizerService.init(this.grid, gridContainerElm); } // user could show a custom footer with the data metrics (dataset length and last updated timestamp) if (!this._options.enablePagination && this._options.showCustomFooter && this._options.customFooterOptions && gridContainerElm) { this.slickFooter = new SlickFooterComponent(this.grid, this._options.customFooterOptions, this._eventPubSubService, this.props.translaterService); this.slickFooter.renderFooter(gridContainerElm); } if (!this.props.customDataView && this.dataView) { const initialDataset = this._options?.enableTreeData ? this.sortTreeDataset(this.props.dataset) : this.props.dataset; if (Array.isArray(initialDataset)) { this.dataView.setItems(initialDataset, this._options.datasetIdPropertyName ?? 'id'); } // if you don't want the items that are not visible (due to being filtered out or being on a different page) // to stay selected, pass 'false' to the second arg if (this.grid?.getSelectionModel() && this._options?.dataView && this._options.dataView.hasOwnProperty('syncGridSelection')) { // if we are using a Backend Service, we will do an extra flag check, the reason is because it might have some unintended behaviors // with the BackendServiceApi because technically the data in the page changes the DataView on every page change. let preservedRowSelectionWithBackend = false; if (this._options.backendServiceApi && this._options.dataView.hasOwnProperty('syncGridSelectionWithBackendService')) { preservedRowSelectionWithBackend = this._options.dataView.syncGridSelectionWithBackendService; } const syncGridSelection = this._options.dataView.syncGridSelection; if (typeof syncGridSelection === 'boolean') { let preservedRowSelection = syncGridSelection; if (!this._isLocalGrid) { // when using BackendServiceApi, we'll be using the "syncGridSelectionWithBackendService" flag BUT "syncGridSelection" must also be set to True preservedRowSelection = syncGridSelection && preservedRowSelectionWithBackend; } this.dataView.syncGridSelection(this.grid, preservedRowSelection); } else if (typeof syncGridSelection === 'object') { this.dataView.syncGridSelection(this.grid, syncGridSelection.preserveHidden, syncGridSelection.preserveHiddenOnSelectionChange); } } if (this._dataset.length > 0) { if (!this._isDatasetInitialized && (this._options.enableCheckboxSelector || this._options.enableRowSelection || this._options.enableHybridSelection)) { this.loadRowSelectionPresetWhenExists(); } this.loadFilterPresetsWhenDatasetInitialized(); this._isDatasetInitialized = true; } } // user might want to hide the header row on page load but still have `enableFiltering: true` // if that is the case, we need to hide the headerRow ONLY AFTER all filters got created & dataView exist if (this._hideHeaderRowAfterPageLoad) { this.showHeaderRow(false); this.sharedService.hideHeaderRowAfterPageLoad = this._hideHeaderRowAfterPageLoad; } // publish & dispatch certain events this._eventPubSubService.publish(`onGridCreated`, this.grid); // after the DataView is created & updated execute some processes & dispatch some events if (!this.props.customDataView) { this.executeAfterDataviewCreated(this.grid, this._options); } // bind resize ONLY after the dataView is ready this.bindResizeHook(this.grid, this._options); // bind the Backend Service API callback functions only after the grid is initialized // because the preProcess() and onInit() might get triggered if (this._options?.backendServiceApi) { this.bindBackendCallbackFunctions(this._options); } // create the React Grid Instance with reference to all Services const reactElementInstance = { element: this._elm, // Slick Grid & DataView objects dataView: this.dataView, slickGrid: this.grid, // public methods dispose: this.dispose.bind(this), // return all available Services (non-singleton) backendService: this.backendService, eventPubSubService: this._eventPubSubService, extensionService: this.extensionService, filterService: this.filterService, gridEventService: this.gridEventService, gridStateService: this.gridStateService, gridService: this.gridService, headerGroupingService: this.headerGroupingService, paginationService: this.paginationService, resizerService: this.resizerService, sortService: this.sortService, treeDataService: this.treeDataService, }; // addons (SlickGrid extra plugins/controls) this.extensions = this.extensionService?.extensionList; // all instances (SlickGrid, DataView & all Services) this.instances = reactElementInstance; this.setStateValue('instances', reactElementInstance); this._eventPubSubService.publish('onReactGridCreated', reactElementInstance); // subscribe to column definitions assignment changes this.observeColumns(); } componentWillUnmount(shouldEmptyDomElementContainer = false) { this._eventPubSubService.publish('onBeforeGridDestroy', this.grid); this._eventHandler?.unsubscribeAll(); if (typeof this._i18next?.off === 'function') { this._i18next.off('languageChanged'); } // we could optionally also empty the content of the grid container DOM element if (shouldEmptyDomElementContainer) { this.emptyGridContainerElm(); } this._collectionObservers.forEach((obs) => obs?.disconnect()); this._eventPubSubService.publish('onAfterGridDestroyed', true); // dispose of all Services this.serviceList.forEach((service) => { if (service?.dispose) { service.dispose(); } }); this.serviceList = []; // dispose backend service when defined and a dispose method exists this.backendService?.dispose?.(); // dispose all registered external resources this.disposeExternalResources(); // dispose the Components this.slickEmptyWarning?.dispose(); this.slickFooter?.dispose(); this.slickPagination?.dispose(); if (this.dataView) { if (this.dataView.setItems) { this.dataView.setItems([]); } if (this.dataView.destroy) { this.dataView.destroy(); } } if (this.grid?.destroy) { this.grid.destroy(shouldEmptyDomElementContainer); } // also dispose of all Subscriptions this.subscriptions = disposeAllSubscriptions(this.subscriptions); if (this.backendServiceApi) { for (const prop of Object.keys(this.backendServiceApi)) { this.backendServiceApi[prop] = null; } this.backendServiceApi = undefined; } for (const prop of Object.keys(this.props.columns)) { this.props.columns[prop] = null; } for (const prop of Object.keys(this.sharedService)) { this.sharedService[prop] = null; } this._dataset = null; this._columns = []; } emptyGridContainerElm() { const gridContainerId = this._options?.gridContainerId || 'grid1'; const gridContainerElm = document.querySelector(`#${gridContainerId}`); emptyElement(gridContainerElm); } dispose(shouldEmptyDomElementContainer = false) { this.componentWillUnmount(shouldEmptyDomElementContainer); } disposeExternalResources() { if (Array.isArray(this._registeredResources)) { while (this._registeredResources.length > 0) { const res = this._registeredResources.pop(); if (res?.dispose) { res.dispose(); } } } this._registeredResources = []; } componentDidUpdate(prevProps) { // get the grid options (order of precedence is Global Options first, then user option which could overwrite the Global options) if (this.props.options !== prevProps.options) { this._options = { ...GlobalGridOptions, ...this._options }; } if (this.props.columns !== prevProps.columns) { this._columns = this.props.columns; this.columnsChanged(this.props.columns); } if (this.props.dataset !== prevProps.dataset) { this.dataset = this.props.dataset || prevProps.dataset; } if (this.props.datasetHierarchical && this.props.datasetHierarchical !== prevProps.datasetHierarchical) { this.datasetHierarchical = this.props.datasetHierarchical; } this.suggestDateParsingWhenHelpful(); } columnsChanged(columns) { if (columns) { this._columns = columns; } if (this._isGridInitialized) { this.updateColumnsList(this._columns); } if (this._columns.length > 0) { this.copyColumnWidthsReference(this._columns); } } /** * Define our internal Post Process callback, it will execute internally after we get back result from the Process backend call * Currently ONLY available with the GraphQL Backend Service. * The behavior is to refresh the Dataset & Pagination without requiring the user to create his own PostProcess every time */ createBackendApiInternalPostProcessCallback(gridOptions) { const backendApi = gridOptions?.backendServiceApi; if (backendApi?.service) { const backendApiService = backendApi.service; // internalPostProcess only works (for now) with a GraphQL Service, so make sure it is of that type if (typeof backendApiService.getDatasetName === 'function') { backendApi.internalPostProcess = (processResult) => { const datasetName = backendApi && backendApiService && typeof backendApiService.getDatasetName === 'function' ? backendApiService.getDatasetName() : ''; if (processResult?.data[datasetName]) { const data = processResult.data[datasetName].hasOwnProperty('nodes') ? processResult.data[datasetName].nodes : processResult.data[datasetName]; const totalCount = processResult.data[datasetName].hasOwnProperty('totalCount') ? processResult.data[datasetName].totalCount : processResult.data[datasetName].length; this.refreshGridData(data, totalCount || 0); } }; } } } bindDifferentHooks(grid, gridOptions, dataView) { // translate some of them on first load, then on each language change if (gridOptions.enableTranslate) { this.extensionService.translateAllExtensions(); } // on locale change, we have to manually translate the Headers, GridMenu if (typeof this._i18next?.on === 'function') { this._i18next.on('languageChanged', (lang) => { // publish event of the same name that Slickgrid-Universal uses on a language change event this._eventPubSubService.publish('onLanguageChange', lang); if (gridOptions.enableTranslate) { this.extensionService.translateAllExtensions(lang); if ((gridOptions.createPreHeaderPanel && gridOptions.createTopHeaderPanel) || (gridOptions.createPreHeaderPanel && !gridOptions.enableDraggableGrouping)) { this.headerGroupingService.translateHeaderGrouping(); } } }); } // if user set an onInit Backend, we'll run it right away (and if so, we also need to run preProcess, internalPostProcess & postProcess) if (gridOptions.backendServiceApi) { const backendApi = gridOptions.backendServiceApi; if (backendApi?.service?.init) { backendApi.service.init(backendApi.options, gridOptions.pagination, this.grid, this.sharedService); } } if (dataView && grid) { // on cell click, mainly used with the columnDef.action callback this.gridEventService.bindOnBeforeEditCell(grid); this.gridEventService.bindOnCellChange(grid); this.gridEventService.bindOnClick(grid); if (dataView && grid) { // bind external sorting (backend) when available or default onSort (dataView) if (gridOptions.enableSorting) { // bind external sorting (backend) unless specified to use the local one if (gridOptions.backendServiceApi && !gridOptions.backendServiceApi.useLocalSorting) { this.sortService.bindBackendOnSort(grid); } else { this.sortService.bindLocalOnSort(grid); } } // bind external filter (backend) when available or default onFilter (dataView) if (gridOptions.enableFiltering) { this.filterService.init(grid); // bind external filter (backend) unless specified to use the local one if (gridOptions.backendServiceApi && !gridOptions.backendServiceApi.useLocalFiltering) { this.filterService.bindBackendOnFilter(grid); } else { this.filterService.bindLocalOnFilter(grid); } } // when column are reordered, we need to update the visibleColumn array this._eventHandler.subscribe(grid.onColumnsReordered, (_e, args) => { this.sharedService.hasColumnsReordered = true; this.sharedService.visibleColumns = args.impactedColumns; }); this._eventHandler.subscribe(grid.onSetOptions, (_e, args) => { // add/remove dark mode CSS class when enabled if (args.optionsBefore.darkMode !== args.optionsAfter.darkMode && this.sharedService.gridContainerElement) { this.setDarkMode(args.optionsAfter.darkMode); } }); // load any presets if any (after dataset is initialized) this.loadColumnPresetsWhenDatasetInitialized(); this.loadFilterPresetsWhenDatasetInitialized(); // When data changes in the DataView, we need to refresh the metrics and/or display a warning if the dataset is empty this._eventHandler.subscribe(dataView.onRowCountChanged, (_e, args) => { if (!gridOptions.enableRowDetailView || !Array.isArray(args.changedRows) || args.changedRows.length === args.itemCount) { grid.invalidate(); } else { grid.invalidateRows(args.changedRows); grid.render(); } this.handleOnItemCountChanged(dataView.getFilteredItemCount() || 0, dataView.getItemCount() || 0); }); this._eventHandler.subscribe(dataView.onSetItemsCalled, (_e, args) => { this.sharedService.isItemsDateParsed = false; this.handleOnItemCountChanged(dataView.getFilteredItemCount() || 0, args.itemCount); // when user has resize by content enabled, we'll force a full width calculation since we change our entire dataset if (args.itemCount > 0 && (this.options.autosizeColumnsByCellContentOnFirstLoad || this.options.enableAutoResizeColumnsByCellContent)) { this.resizerService.resizeColumnsByCellContent(!this._options?.resizeByContentOnlyOnFirstLoad); } }); if (gridOptions?.enableFiltering && !gridOptions.enableRowDetailView) { this._eventHandler.subscribe(dataView.onRowsChanged, (_e, { calledOnRowCountChanged, rows }) => { // filtering data with local dataset will not always show correctly unless we call this updateRow/render // also don't use "invalidateRows" since it destroys the entire row and as bad user experience when updating a row // see commit: https://github.com/ghiscoding/aurelia-slickgrid/commit/8c503a4d45fba11cbd8d8cc467fae8d177cc4f60 if (!calledOnRowCountChanged && Array.isArray(rows)) { const ranges = grid.getRenderedRange(); rows.filter((row) => row >= ranges.top && row <= ranges.bottom).forEach((row) => grid.updateRow(row)); grid.render(); } }); } } } } bindBackendCallbackFunctions(gridOptions) { const backendApi = gridOptions.backendServiceApi; const backendApiService = backendApi?.service; const serviceOptions = backendApiService?.options || {}; const isExecuteCommandOnInit = !serviceOptions ? false : serviceOptions && serviceOptions.hasOwnProperty('executeProcessCommandOnInit') ? serviceOptions['executeProcessCommandOnInit'] : true; if (backendApiService) { // update backend filters (if need be) BEFORE the query runs (via the onInit command a few lines below) // if user entered some any "presets", we need to reflect them all in the grid if (gridOptions?.presets) { // Filters "presets" if (backendApiService.updateFilters && Array.isArray(gridOptions.presets.filters) && gridOptions.presets.filters.length > 0) { backendApiService.updateFilters(gridOptions.presets.filters, true); } // Sorters "presets" if (backendApiService.updateSorters && Array.isArray(gridOptions.presets.sorters) && gridOptions.presets.sorters.length > 0) { // when using multi-column sort, we can have multiple but on single sort then only grab the first sort provided const sortColumns = this._options?.multiColumnSort ? gridOptions.presets.sorters : gridOptions.presets.sorters.slice(0, 1); backendApiService.updateSorters(undefined, sortColumns); } // Pagination "presets" if (backendApiService.updatePagination && gridOptions.presets.pagination && !this.hasBackendInfiniteScroll()) { const { pageNumber, pageSize } = gridOptions.presets.pagination; backendApiService.updatePagination(pageNumber, pageSize); } } else { const columnFilters = this.filterService.getColumnFilters(); if (columnFilters && backendApiService.updateFilters) { backendApiService.updateFilters(columnFilters, false); } } // execute onInit command when necessary if (backendApi && backendApiService && (backendApi.onInit || isExecuteCommandOnInit)) { const query = typeof backendApiService.buildQuery === 'function' ? backendApiService.buildQuery() : ''; const process = isExecuteCommandOnInit ? (backendApi.process?.(query) ?? null) : (backendApi.onInit?.(query) ?? null); // wrap this inside a microtask to be executed at the end of the task and avoid timing issue since the gridOptions needs to be ready before running this onInit queueMicrotask(() => { const backendUtilityService = this.backendUtilityService; // keep start time & end timestamps & return it after process execution const startTime = new Date(); // run any pre-process, if defined, for example a spinner if (backendApi.preProcess) { backendApi.preProcess(); } // the processes can be a Promise (like Http) const totalItems = this._options?.pagination?.totalItems ?? 0; if (process instanceof Promise) { process .then((processResult) => backendUtilityService.executeBackendProcessesCallback(startTime, processResult, backendApi, totalItems)) .catch((error) => backendUtilityService.onBackendError(error, backendApi)); } else if (process && this.rxjs?.isObservable(process)) { this.subscriptions.push(process.subscribe((processResult) => backendUtilityService.executeBackendProcessesCallback(startTime, processResult, backendApi, totalItems), (error) => backendUtilityService.onBackendError(error, backendApi))); } }); } // when user enables Infinite Scroll if (backendApi.service.options?.infiniteScroll) { this.addBackendInfiniteScrollCallback(); } } } addBackendInfiniteScrollCallback() { if (this.grid && this.options.backendServiceApi && this.hasBackendInfiniteScroll() && !this.options.backendServiceApi?.onScrollEnd) { const onScrollEnd = () => { this.backendUtilityService.setInfiniteScrollBottomHit(true); // even if we're not showing pagination, we still use pagination service behind the scene // to keep track of the scroll position and fetch next set of data (aka next page) // we also need a flag to know if we reached the of the dataset or not (no more pages) this.paginationService.goToNextPage().then((hasNext) => { if (!hasNext) { this.backendUtilityService.setInfiniteScrollBottomHit(false); } }); }; this.options.backendServiceApi.onScrollEnd = onScrollEnd; // subscribe to SlickGrid onScroll to determine when reaching the end of the scroll bottom position // run onScrollEnd() method when that happens this._eventHandler.subscribe(this.grid.onScroll, (_e, args) => { const viewportElm = args.grid.getViewportNode(); if (['mousewheel', 'scroll'].includes(args.triggeredBy || '') && this.paginationService?.totalItems && args.scrollTop > 0 && Math.ceil(viewportElm.offsetHeight + args.scrollTop) >= args.scrollHeight) { if (!this._scrollEndCalled) { onScrollEnd(); this._scrollEndCalled = true; } } }); // use postProcess to identify when scrollEnd process is finished to avoid calling the scrollEnd multiple times // we also need to keep a ref of the user's postProcess and call it after our own postProcess const orgPostProcess = this.options.backendServiceApi.postProcess; this.options.backendServiceApi.postProcess = (processResult) => { this._scrollEndCalled = false; if (orgPostProcess) { orgPostProcess(processResult); } }; } } bindResizeHook(grid, options) { if ((options.autoFitColumnsOnFirstLoad && options.autosizeColumnsByCellContentOnFirstLoad) || (options.enableAutoSizeColumns && options.enableAutoResizeColumnsByCellContent)) { throw new Error(`[Slickgrid-React] You cannot enable both autosize/fit viewport & resize by content, you must choose which resize technique to use. You can enable these 2 options ("autoFitColumnsOnFirstLoad" and "enableAutoSizeColumns") OR these other 2 options ("autosizeColumnsByCellContentOnFirstLoad" and "enableAutoResizeColumnsByCellContent").`); } // auto-resize grid on browser resize if (options.gridHeight || options.gridWidth) { this.resizerService.resizeGrid(0, { height: options.gridHeight, width: options.gridWidth }); } else { this.resizerService.resizeGrid(); } // expand/autofit columns on first page load if (grid && options?.enableAutoResize && options.autoFitColumnsOnFirstLoad && options.enableAutoSizeColumns && !this._isAutosizeColsCalled) { grid.autosizeColumns(); this._isAutosizeColsCalled = true; } } executeAfterDataviewCreated(_grid, gridOptions) { // if user entered some Sort "presets", we need to reflect them all in the DOM if (gridOptions.enableSorting) { if (gridOptions.presets && Array.isArray(gridOptions.presets.sorters)) { // when using multi-column sort, we can have multiple but on single sort then only grab the first sort provided const sortColumns = this._options?.multiColumnSort ? gridOptions.presets.sorters : gridOptions.presets.sorters.slice(0, 1); this.sortService.loadGridSorters(sortColumns); } } } /** * On a Pagination changed, we will trigger a Grid State changed with the new pagination info * Also if we use Row Selection or the Checkbox Selector with a Backend Service (Odata, GraphQL), we need to reset any selection */ paginationChanged(pagination) { const isSyncGridSelectionEnabled = this.gridStateService?.needToPreserveRowSelection() ?? false; if (this.grid && !isSyncGridSelectionEnabled && this.options?.backendServiceApi && (this.options.enableRowSelection || this.options.enableHybridSelection || this.options.ena