import { CollectionState, FiltersMap, KeyedItem, MultiCollectionSupport, TaskState, } from '@wix/bex-core'; import { NestedCollectionNodeState } from './NestedCollectionNodeState'; import { action, computed, createAtom, makeObservable, reaction, runInAction, } from 'mobx'; import { TableColumn } from '../../model'; import { NestedTableNestedModeState } from './NestedTableNestedModeState'; import { ReactNode } from 'react'; import { NestedTableNodeStateBIReporter } from './NestedTableNodeStateBIReporter'; import { NestedTableLevelDescriptor } from './NestedTableLevelDescriptor'; import { NestedTableNodeStatePublicAPI } from './NestedTableNodeStatePublicAPI'; import { getNodeOrLeaf } from './getNodeOrLeaf'; import { NestedTableNodeStateOptimisticActions } from './NestedTableNodeStateOptimisticActions'; export interface NestedTableDataItem< C extends string, T, F extends FiltersMap, > { state: NestedTableNodeState; originalKeyedItem: KeyedItem; } export interface NestedTableSkeletonItem< C extends string, T, F extends FiltersMap, > { state: NestedTableNodeState; originalKeyedItem: undefined; } export type NestedTableItem = | NestedTableDataItem | NestedTableSkeletonItem; export type NestedTableKeyedItem< C extends string, T, F extends FiltersMap, > = KeyedItem>; export type RenderMainColumn = (item: T) => { title?: string; subtitle?: string; image?: ReactNode; }; export type NestedTableNestColumns = Partial< Record['render']> >; export interface NestedTableNodeStateParams< C extends string, T, F extends FiltersMap, > { readonly nestedTable: NestedTableNestedModeState; readonly node: NestedCollectionNodeState; readonly levelDescriptor: NestedTableLevelDescriptor; readonly parentNode: NestedTableNodeState | null; readonly parent: KeyedItem | null; readonly preRegister?: boolean; } export class NestedTableNodeState implements NestedTableNodeStatePublicAPI { readonly nestedTable; readonly node; readonly levelDescriptor; readonly parentNode; readonly parent; readonly bi: NestedTableNodeStateBIReporter; readonly multi; _rowNum?: number; readonly initTask = new TaskState(); readonly parentKeyToChildrenMap = new Map< string, NestedTableNodeState >(); readonly parentKeyToChildrenMapAtom = createAtom( 'parentKeyToChildrenMapAtom', ); constructor(params: NestedTableNodeStateParams) { this.nestedTable = params.nestedTable; this.node = params.node; this.parentNode = params.parentNode; this.parent = params.parent; this.levelDescriptor = params.levelDescriptor; this.bi = new NestedTableNodeStateBIReporter(this); this.multi = new MultiCollectionSupport({ collections: [this.node.collection], }); makeObservable(this, { keyedItems: computed, _keyedItemsMap: computed, _allKeyedItemsMap: computed, _allKeyedItems: computed, _keyedItemsIfExpanded: computed, _isNotExpandableWhenEmpty: computed, init: action, createNode: action, _preRegisterAll: action, _toggleExpand: action, _notify: action, _notifyAll: action, deepestBranchDepth: computed, unprocessedMap: computed, getKeyedItem: action, hasExpandableItems: computed, _childrenCount: computed, }); this._register(); } get collection() { return this.node.collection; } get unprocessedMap() { const { collection: { result }, } = this; const { _pages, itemKey, _pagesAtom } = result; _pagesAtom.reportObserved(); return new Map( _pages.unprocessed.flatMap((page) => page.map((item) => { const key = itemKey(item); return [key, item]; }), ), ); } get children() { return this.collection; } get optimisticActions() { return new NestedTableNodeStateOptimisticActions(this); } get flatModeCollection(): CollectionState | undefined { return this.nestedTable.wrapper.flat.sections.find((section) => section.levelDescriptors.includes(this.levelDescriptor), )?.collection; } get depth() { return this.parentNode?.childrenDepth ?? 0; } get childrenDepth() { return this.levelDescriptor.depth; } get deepestBranchDepth(): number { const { node, parentKeyToChildrenMap, parentKeyToChildrenMapAtom } = this; const { collection: { result: { keyedItems }, }, } = node; parentKeyToChildrenMapAtom.reportObserved(); return ( keyedItems.reduce((acc, keyedItem) => { return Math.max( acc, parentKeyToChildrenMap.get(keyedItem.id)?.deepestBranchDepth ?? 0, ); }, 0) + (keyedItems.length ? 1 : 0) ); } get hasExpandableItems() { const { parentKeyToChildrenMap, parentKeyToChildrenMapAtom } = this; parentKeyToChildrenMapAtom.reportObserved(); return this.node.collection.result.keyedItems.some( (item) => (parentKeyToChildrenMap.get(item.id)?._childrenCount ?? 0) > 0, ); } get _childrenCount() { return this.node.collection.result.keyedItems.length ?? 0; } get _expanded() { return this.node._expanded; } get columns() { return this.levelDescriptor.columns; } get renderMainColumn() { return this.levelDescriptor.renderMainColumn; } get showHoverHighlight() { return this.node.hoveredOrParentHovered; } _changeHover(hovered: boolean) { this.node._changeHover(hovered); } get showEmptyState() { return this.node.collection.showEmptyState; } get showLoadingState() { return ( this.node.collection.showInitialLoader || this.node.collection.showRefreshLoader ); } get showErrorState() { return this.node.collection.showErrorState; } get errorStatus() { return this.node.collection.result.errorStatus; } retryErrorState = () => this.node.collection.retryErrorState(); get _isNotExpandableWhenEmpty() { const { nestedTable: { isFetchAllAborted }, node: { collection: { showEmptyState }, }, } = this; return !isFetchAllAborted && showEmptyState; } _isExpandableActionDisabled(item: T) { return this.levelDescriptor.expandable?.(item) === false; } isNotExpandable(item: T) { return ( this._isNotExpandableWhenEmpty || this._isExpandableActionDisabled(item) ); } get _keyedItemsIfExpanded(): NestedTableKeyedItem[] { const { parent, nestedTable: { isFetchAllAborted }, _expanded, node: { collection: { showRefreshLoader, showEmptyState, showErrorState }, }, } = this; if (!_expanded) { return []; } return [ ...this.keyedItems, ...(parent && isFetchAllAborted && (showRefreshLoader || showEmptyState || showErrorState) ? new Array(1).fill(null).map((_, index) => ({ id: `placeholder-states-${parent.key}`, key: `placeholder-states-${parent.key}`, index, pageIndex: 0, indexWithinPage: index, item: { state: this, originalKeyedItem: undefined, }, })) : []), ]; } get _keyedItemsMap(): Map> { return new Map( this.keyedItems.map((keyedItem) => [keyedItem.key, keyedItem]), ); } get _allKeyedItemsMap(): Map> { return new Map( this._getKeyedItems({ onlyExpanded: false }).map((keyedItem) => [ keyedItem.key, keyedItem, ]), ); } get _allKeyedItems() { return Array.from(this._allKeyedItemsMap.values()); } _getKeyedItems({ onlyExpanded, }: { onlyExpanded?: boolean; }): NestedTableKeyedItem[] { const { node, parentKeyToChildrenMap, parentKeyToChildrenMapAtom } = this; const { collection: { result: { keyedItems }, }, } = node; parentKeyToChildrenMapAtom.reportObserved(); return keyedItems .flatMap((keyedItem) => { const childrenNode = parentKeyToChildrenMap.get(keyedItem.key); return [ { ...keyedItem, item: { state: this, originalKeyedItem: keyedItem, }, }, ...(childrenNode ? onlyExpanded ? childrenNode._keyedItemsIfExpanded : childrenNode._allKeyedItems : []), ]; }) .map((keyedItem, index) => ({ ...keyedItem, index })); } get keyedItems(): NestedTableKeyedItem[] { return this._getKeyedItems({ onlyExpanded: true }); } getKeyedItem(key: string) { return this._keyedItemsMap.get(key); } getDescendants(): NestedTableNodeStatePublicAPI< C, TD, FD >[] { const { collection } = this; return collection.result.keyedItems.flatMap((keyedItem) => { const node = getNodeOrLeaf(this, keyedItem); return [node, ...node.getDescendants()]; }); } _getNode(key: string) { const node = this._keyedItemsMap.get(key); return node?.item.state.parentKeyToChildrenMap.get(key) as | NestedTableNodeState | undefined; } getParent(): NestedTableNodeStatePublicAPI< C, TD, FD > | null { return this.parentNode; } getData(): TD | null { const { parent, parentNode } = this; if (parent == null || parentNode == null) { return null; } return parentNode.collection.getItem(parent.key); } toggleExpand() { this._toggleExpand(); } get expanded() { return this._expanded; } getKeyedItemByIndex(ind: number) { return this.keyedItems[ind]; } _register() { const { parent, parentNode } = this; if (parent == null || parentNode == null) { return; } parentNode.parentKeyToChildrenMap.set(parent.key, this); return () => { parentNode.parentKeyToChildrenMap.delete(parent.key); }; } async _refreshAll() { const { collection } = this; collection._resetReactQuery(); await collection.moveToFirstPage(); const keyedItems = collection.result.keyedItems; for (const keyedItem of keyedItems) { const node = this.createNode({ parent: keyedItem }); if (node == null) { continue; } await node._refreshAll(); } if (this.childrenDepth === 0 || this.node._notifyAll) { this._notifyAll(); } } async _toggleExpand() { const promise = this.node._toggleExpand(); this.parentNode?.parentKeyToChildrenMapAtom.reportChanged(); await promise; runInAction(() => { this.parentNode?.parentKeyToChildrenMapAtom.reportChanged(); }); } async _preRegisterAll() { const { node } = this; const { collection: { result: { keyedItems }, }, } = node; await Promise.all( keyedItems.map(async (keyedItem) => { const state = this.createNode({ parent: keyedItem }); if (state == null) { return; } const dispose = state.init(); await state.initTask.status.promise; dispose(); }), ); } _notify() { const { parentKeyToChildrenMapAtom } = this; parentKeyToChildrenMapAtom.reportChanged(); } _notifyAll() { const { parentKeyToChildrenMap } = this; this._notify(); parentKeyToChildrenMap.forEach((state) => { state._notifyAll(); }); } _cleanupStaleNodes() { return reaction( () => this.collection.result.keyedItems, (keyedItems) => { const { parentKeyToChildrenMap } = this; // remove children that are not in the collection const keys = new Set(keyedItems.map((item) => item.key)); const mapKeys = Array.from(parentKeyToChildrenMap.keys()); for (const key of mapKeys) { if (keys.has(key)) { continue; } parentKeyToChildrenMap.delete(key); } }, ); } init({ skipCollectionInit }: { skipCollectionInit?: boolean } = {}) { const { node, initTask, nestedTable, collection } = this; const { nestedTableDragAndDrop } = nestedTable; const disposers = [ node.init({ skipCollectionInit }), nestedTableDragAndDrop?.initCollection(collection), this._cleanupStaleNodes(), ]; initTask.runOnce(async () => { node.initTask.runOnce(); await node.initTask.status.promise; await runInAction(async () => { if (!nestedTable.isFetchAllAborted) { await this._preRegisterAll(); if (this.childrenDepth === 0 || node._notifyAll) { this._notifyAll(); } } return; }); }); return () => { disposers.forEach((d) => d?.()); }; } createNode({ parent, notifyAll, }: { parent: KeyedItem; notifyAll?: boolean; }): NestedTableNodeState | null { const { nestedTable, node, childrenDepth, parentKeyToChildrenMap } = this; const existing = parentKeyToChildrenMap.get(parent.key); if (existing != null) { return existing; } const nextLevelDescriptor = nestedTable.levels[childrenDepth + 1]; if (nextLevelDescriptor == null) { return null; } const nextLevelNode = node._createNode(parent, { levelDescriptor: nextLevelDescriptor, notifyAll, }); if (nextLevelNode == null) { return null; } return new NestedTableNodeState({ parent, parentNode: this, nestedTable, node: nextLevelNode, levelDescriptor: nextLevelDescriptor, }); } }