import { addEventListener, CollectionOptimisticSequenceExecutor, FiltersMap, OptionalFiltersMap, QueryState, ReportBI, syncWithSharedQuery, TaskState, WixPatternsContainer, } from '@wix/bex-core'; import { ToolbarCollectionState } from '../ToolbarCollectionState'; import { TableState } from '../TableState'; import { TableColumn } from '../../model'; import { NestedTableColumn } from './NestedTableColumn'; import { NestedTableNestedModeState } from './NestedTableNestedModeState'; import { NestedTableFlatModeState } from './NestedTableFlatModeState'; import { NestedCollectionFetchAllState } from './NestedCollectionFetchAllState'; import { NestedTableLevelDescriptor } from './NestedTableLevelDescriptor'; import { EagerLazyModeOptions } from './types'; import { reaction } from 'mobx'; import { ICollectionComponentState } from '../ICollectionComponentState'; import { NestedTableNodeStatePublicAPI } from './NestedTableNodeStatePublicAPI'; import { createNestedOptimisticActions } from './createNestedOptimisticActions'; export interface NestedTableStateBaseParams< C extends string, F extends FiltersMap = OptionalFiltersMap, > { /** * If the total items in the server are less or equal to this threshold, all items will be fetched and rendered at once, in expanded state (referred to as "Eager" mode). * @default 1000 */ fetchAllLimit?: number; /** * @deprecated Use `fetchAllLimit` instead. */ fetchAllThreshold?: number; /** * In "Eager" mode, items will be in expanded state. This threshold allows to use collapsed state if the total items in the server are greater than this threshold. * @default `fetchAllThreshold` */ expandAllThreshold?: number; /** * Columns to be rendered in the nested table. Same as [TableColumn](./?path=/story/common-types--tablecolumn) - just without rendering logic of cells (`render` prop will be defined for each level individually in the `levels` prop). * @overrideType [TableColumn](./?path=/story/common-types--tablecolumn)[] */ columns: NestedTableColumn[]; /** * Main column of the nested table. * An object with the following properties: * - `title`: The title of the column. @default '' * - `width`: The width of the column. Can be a number (in pixels) or a string (percentage). @default '326px' */ mainColumn?: { title?: string; width?: number | string; }; /** * Configuration of the nested table levels. Use one of the helper function to create the levels configuration: * - [createNestedTableLevel](./?path=/story/base-components-collections-nestedtable--createnestedtablelevel) - for cases where each level if of a different entity type. * - [createNestedTableSingleEntityLevels](./?path=/story/base-components-collections-nestedtable--createnestedtablesingleentitylevels) - for cases where all levels are of the same entity type. * @overrideType [NestedTableLevel](./?path=/story/base-components-collections-nestedtable--nestedtablelevel)[] */ levels: Omit, 'depth'>[]; filters?: F; } export interface NestedTableStateParams< C extends string, F extends FiltersMap = OptionalFiltersMap, > extends NestedTableStateBaseParams { container: WixPatternsContainer; } export interface NestedTableStatePublicAPI { /** * Underlying [ToolbarCollectionState](./?path=/story/common-state--toolbarcollectionstate) instance * @overrideType [ToolbarCollectionState](./?path=/story/common-state--toolbarcollectionstate) */ readonly toolbar: ToolbarCollectionState; /** * Underlying [TableState](./?path=/story/base-components-collections-table-tablestate--tablestate) instance * @overrideType [TableState](./?path=/story/base-components-collections-table-tablestate--tablestate) */ readonly tableState: TableState; /** * Get the node this item is the parent of. i.e. this node's collection items' parent id is the `key` * @param key * @overrideType (key: string) => [NestedTableNodeState](./?path=/story/base-components-collections-nestedtable--nestedtablenodestate) | undefined */ getNode( key: string, ): NestedTableNodeStatePublicAPI | undefined; /** * Get the root node of the nested table, this node's collection items' parent id is `null`, the items are the top level items */ getRootNode(): NestedTableNodeStatePublicAPI< C, TD, FD >; /** * Get all the nodes in the table */ getAllNodes(): NestedTableNodeStatePublicAPI< C, TD, FD >[]; /** * An API to refresh all items in the nested table. */ readonly refreshAll: () => void; } export class NestedTableState< C extends string = string, F extends FiltersMap = OptionalFiltersMap, > implements ICollectionComponentState, NestedTableStatePublicAPI { readonly container; readonly reportBi: ReportBI; readonly toolbar; readonly tableState; readonly mainColumn; readonly options: EagerLazyModeOptions; readonly initTask = new TaskState(); readonly levels: NestedTableLevelDescriptor[]; readonly rootNodeCollection; readonly query; readonly nested: NestedTableNestedModeState; readonly flat: NestedTableFlatModeState; readonly sequences = new Map(); constructor(params: NestedTableStateParams) { this.container = params.container; this.query = new QueryState({ filters: params.filters || {}, }); this.levels = params.levels.map((level, i) => ({ ...level, depth: i, })); const fetchAllLimit = params.fetchAllLimit ?? params.fetchAllThreshold; this.options = { fetchAllThreshold: fetchAllLimit ?? 1000, expandAllThreshold: params.expandAllThreshold ?? fetchAllLimit ?? 1000, }; this.mainColumn = params.mainColumn; const rootLevelDescriptor = this.levels[0]; if (rootLevelDescriptor == null) { throw new Error('NestedTableState must have at least one level'); } const nestedOptimisticActionsState = { parentId: '', sequences: this.sequences, query: this.query, levelDescriptor: rootLevelDescriptor, }; this.rootNodeCollection = rootLevelDescriptor.createCollection( nestedOptimisticActionsState, ); createNestedOptimisticActions( this.container, this.rootNodeCollection.collection, nestedOptimisticActionsState, ); const { collection } = this.rootNodeCollection; syncWithSharedQuery(collection.query, this.query); this.reportBi = (reportProps) => { const { isFetchAllAborted } = this; this.container.reportBi({ ...reportProps, params: { ...reportProps.params, ...(isFetchAllAborted != null && { componentStatus: isFetchAllAborted ? 'All items collapsed' : 'All items expanded', }), }, }); }; this.toolbar = new ToolbarCollectionState({ collection: this.rootNodeCollection.collection, container: params.container, componentType: 'NestedTable', reportBi: this.reportBi, }); this.toolbar.setNewColumns( params.columns.map((column) => ({ ...column, render: () => null, })) as TableColumn[], ); this.tableState = new TableState({ collection: this.rootNodeCollection.collection, container: params.container, toolbar: this.toolbar, }); this.nested = new NestedTableNestedModeState({ wrapper: this, reportBi: this.reportBi, }); this.flat = new NestedTableFlatModeState({ wrapper: this, }); } get nestedMode() { return this.nested; } get flatMode() { return this.flat; } getNode(key: string) { return this.nested.getNode(key); } getRootNode(): NestedTableNodeStatePublicAPI< C, TD, FD > { return this.nested.root; } /** * Get all the nodes in the table */ getAllNodes() { return this.nested.getAllNodes(); } get isFetchAllAborted() { return this.nested.isFetchAllAborted; } get expandAll() { return this.nested.expandAll; } get nestedCollection() { return this.nested.nestedCollection; } _attemptFetchAllAndPopulateCache() { const initTask = new TaskState(); return { initTask, init: ( { invalidate }: { invalidate: boolean } = { invalidate: false }, ) => { initTask.runOnce(async () => { const fetchAllState = new NestedCollectionFetchAllState({ options: this.options, query: this.nested.query, nestedCollectionState: this.nested.nestedCollection, }); const disposers = [fetchAllState.init({ invalidate })]; try { await fetchAllState.initTask.status.promise; } finally { disposers.forEach((d) => d()); } }); return () => {}; }, }; } refreshAll({ invalidate = true }: { invalidate?: boolean } = {}) { const initTask = this.isFlatMode ? this.flat.initTask : this.nested.initTask; initTask.reset(); initTask.runOnce(async () => { const fetchAll = this._attemptFetchAllAndPopulateCache(); fetchAll.init({ invalidate }); await fetchAll.initTask.status.promise; await (this.isFlatMode ? this.flatMode._refreshAll() : this.nestedMode.root._refreshAll()); this.rootNodeCollection.collection.query.events.emit('refresh'); }); } get isFlatMode() { return !!this.query.search.value; } _addFilterListeners() { return Object.values(this.query.filtersEntries).map(([_, filter]) => { return addEventListener(filter.events, 'refresh', () => { this.refreshAll({ invalidate: false, }); }); }); } init() { const { initTask, tableState, query } = this; const disposers = [ query.init({ skipFilterListeners: true }), ...this._addFilterListeners(), reaction( () => this.isFlatMode, (isFlatMode) => { const { query: { _fields }, } = this; if (isFlatMode) { _fields.add('breadcrumbs'); } else { _fields.delete('breadcrumbs'); } }, { fireImmediately: true }, ), tableState.init({ featuresInitializers: [], skipFilterListeners: true, }), ]; initTask.runOnce(async () => { tableState.initTask.runOnce(); await tableState.initTask.status.promise; }); return () => { initTask.reset(); disposers.forEach((d) => d()); }; } }