import { FiltersMap, TaskState, TypedEmitter, withoutDefaults, } from '@wix/bex-core'; import { action, computed, makeObservable, reaction } from 'mobx'; import { ToolbarCollectionState } from './ToolbarCollectionState'; import { Column, ViewType } from '../model'; import type { CustomColumnsDragAndDrop } from '../components/CustomColumns/CustomColumnsDragAndDrop'; import { CustomColumnsHistoryState } from './CustomColumnsHistoryState'; import { indexOfCompareFn } from './indexOfCompareFn'; import { EventEmitter } from 'events'; import { cairoCustomizeColumnsInSidePanel, cairoFiltersPanelUsed, } from '@wix/bex-core/bi'; import type { CustomColumnsDragAndDropState } from './CustomColumnsDragAndDropState'; import { CustomColumnsPanelStateBase } from './CustomColumnsPanelStateBase'; export type { CustomColumnsDragAndDropState }; export type CustomColumnsPanelState< T = unknown, F extends FiltersMap = FiltersMap, > = CustomColumnsPanelStateBase; type StoredColumn = | { id?: string; name?: string; isSelected?: boolean } | null | undefined; // Legacy `custom-columns` stays bound to non-spreadsheet layouts so existing // stored values keep their meaning; spreadsheet uses a sibling key. export const CUSTOM_COLUMNS_KEY = 'custom-columns'; export const SPREADSHEET_CUSTOM_COLUMNS_KEY = 'spreadsheet-custom-columns'; const getCustomColumnsKeyForLayout = (layout: ViewType | undefined): string => layout === 'spreadsheet' ? SPREADSHEET_CUSTOM_COLUMNS_KEY : CUSTOM_COLUMNS_KEY; const asStoredColumnsOrNull = (customColumnsResult: unknown) => Array.isArray(customColumnsResult) ? (customColumnsResult as StoredColumn[]) : null; export interface CustomColumnsStateParams { readonly table: ToolbarCollectionState; readonly container: import('@wix/bex-core').WixPatternsContainer; readonly dragAndDrop?: typeof CustomColumnsDragAndDrop; readonly modalsContainerRef: { current: HTMLElement | null | undefined; }; } export type CustomColumnsActionName = 'visibility' | 'reorder'; export class CustomColumnsState< T, F extends FiltersMap, > extends CustomColumnsPanelStateBase { readonly table; readonly dragAndDrop: CustomColumnsDragAndDropState | undefined; readonly customColumnsHistory; _didFirstColumnsMerge?: boolean; _performedCustomColumnsActions: Map = new Map(); readonly initTask = new TaskState(); _touched = false; readonly events = new EventEmitter() as TypedEmitter<{ beforeInitialFetch: () => void; }>; constructor(params: CustomColumnsStateParams) { super({ container: params.container, queryName: [ params.container.environment.componentId, params.table.collection.queryName, 'customColumns', ] .filter(Boolean) .join('/'), }); this.table = params.table; if (params.dragAndDrop) { const { CustomColumnsDragAndDropState } = params.dragAndDrop; this.dragAndDrop = new CustomColumnsDragAndDropState({ customColumns: this, dataSource: { get columns() { return params.table.columns; }, selectedColumns: params.table.selectedColumns, }, components: params.dragAndDrop, modalsContainerRef: params.modalsContainerRef, }); } this.customColumnsHistory = this.table.collection.persistQueryToUrl ? new CustomColumnsHistoryState({ customColumns: this, history: params.container.history, }) : null; this.initBaseObservables(); makeObservable(this, { persistCustomColumns: action.bound, toSelectedColumns: computed, selectedOrderedColumns: computed, init: action, }); } get orderedColumns(): Column[] { const { dragAndDrop, table } = this; const { columns, customColumnsState } = table; if (customColumnsState?.dragAndDrop && dragAndDrop) { return dragAndDrop.orderedColumns; } return columns; } get selectedOrderedColumns() { const { orderedColumns, table: { selectedColumns: { value: selectedColumns }, }, } = this; return orderedColumns.filter((column) => selectedColumns.some( (selectedColumn) => selectedColumn.id === column.id && selectedColumn.isSelected, ), ); } async persistCustomColumns() { const { table } = this; const { dataCapsule, viewsProps } = table; // if views enabled columns persists inside the view instead of data capsule if (viewsProps) { return; } this._touched = true; await dataCapsule.setItem( getCustomColumnsKeyForLayout(table.layoutState?.viewType), table.selectedColumns.value, ); } private registerSelectedColumnChange() { const { table } = this; table.selectedColumns.events.on('refresh', this.persistCustomColumns); return () => { table.selectedColumns.events.off('refresh', this.persistCustomColumns); }; } mergeNewColumns() { const { table: { columns, selectedColumns, _toStoredColumn, customColumnsState }, _didFirstColumnsMerge, } = this; const defaultHidden = !_didFirstColumnsMerge && !!selectedColumns.value.length; const mergeTargetSelectedColumns = [...selectedColumns.value]; if (customColumnsState?.dragAndDrop == null) { return [ ...mergeTargetSelectedColumns.filter((s) => columns.every((c) => s.id !== c.id), ), ...columns.map((c) => { const existing = selectedColumns.value.find((s) => s.id === c.id); if (existing == null) { return _toStoredColumn(c); } return { ...existing, isSelected: c.hideable === false || c.hiddenFromCustomColumnsSelection || existing.isSelected, }; }), ]; } const diffColumns = columns .filter((c) => selectedColumns.value.every((s) => s.id !== c.id)) .map((c) => _toStoredColumn({ ...c, defaultHidden: defaultHidden || c.defaultHidden, }), ); // place new columns according to their order in the original columns prop diffColumns.forEach((d) => { const originalIndex = columns.findIndex((c) => c.id === d.id); const afterId = originalIndex > 0 ? columns[originalIndex - 1].id : null; const afterIndex = afterId != null ? mergeTargetSelectedColumns.findIndex((c) => c.id === afterId) + 1 : originalIndex === 0 ? 0 : mergeTargetSelectedColumns.length; mergeTargetSelectedColumns.splice(afterIndex, 0, d); }); this._didFirstColumnsMerge = true; return mergeTargetSelectedColumns; } getStickyColumnsCount({ stickyColumns }: { stickyColumns: number }) { const { dragAndDrop, table: { selectedColumns: { value: selectedColumns }, }, } = this; if (dragAndDrop == null) { return stickyColumns; } const { reorderDisabled: reorderDisabledColumns } = dragAndDrop.orderedColumnsByReorderDisabled; if (!reorderDisabledColumns.length) { return stickyColumns; } return dragAndDrop.orderedColumnsByReorderDisabled.reorderDisabled.filter( (column) => selectedColumns.some( (selectedColumn) => selectedColumn.id === column.id && selectedColumn.isSelected, ), ).length; } get toSelectedColumns() { const { select, table: { selectedColumns: { value }, }, } = this; return value.map(({ id }) => ({ id, isSelected: select.isChecked(id), })); } get selectionChanged() { const { toSelectedColumns, table: { selectedColumns: { value }, }, } = this; if (value.length !== toSelectedColumns.length) { return true; } for (let i = 0; i < value.length; i++) { const a = value[i]; const b = toSelectedColumns[i]; if (a.id !== b.id || a.isSelected !== b.isSelected) { return true; } } return false; } init() { const { table } = this; table.customColumnsStateInitialized = true; const disposers = [ reaction( () => this.table.selectedColumns.value, async (selectedColumns) => { const { select, columnsCollection } = this; select.clear(); select.forceSetMany( selectedColumns.map(({ id, isSelected }) => ({ value: { id }, status: isSelected ? select.finalStatus : select.startStatus, })), ); await columnsCollection.refreshAllPages(); }, { fireImmediately: true, }, ), reaction( () => this.select.selectedValues, () => { const { table: { selectedColumns }, } = this; if (!this.selectionChanged) { return; } selectedColumns.setValue(this.toSelectedColumns, { emitEvents: ['change', 'beforeRefresh', 'refresh'], }); }, ), this.registerSelectedColumnChange(), this.columnsCollection.init(), this.dragAndDrop?.init(), this.customColumnsHistory?.init(), reaction( () => this.table.layoutState?.viewType, async (newLayout, prevLayout) => { if (!newLayout || !prevLayout || newLayout === prevLayout) { return; } if (this.table._cacheRestoredLayout === newLayout) { return; } // Views path is handled by CollectionViewsState's own reaction. if (this.table.viewsProps) { return; } const stored = await this._getStoredCustomColumns(); if (stored == null) { this.table.selectedColumns.reset(); } else { await this._fetchFromStorage(); } }, ), ]; return () => { disposers.forEach((disposer) => disposer?.()); }; } async _getStoredCustomColumns() { const { dataCapsule } = this.table; try { const storedValue = await asStoredColumnsOrNull( await dataCapsule.getItem( getCustomColumnsKeyForLayout(this.table.layoutState?.viewType), ), ); this._touched = storedValue != null; return storedValue; } catch (e) { if (e instanceof Error && e.message === 'Key was not found in capsule') { return null; } throw e; } } _tableInit() { const { initTask, table: { multi: { collections }, }, } = this; initTask.runOnce(async () => { try { const beforeInitialFetchListeners = this.events.listeners('beforeInitialFetch'); await Promise.all(beforeInitialFetchListeners.map((l) => l())); await this._fetchFromStorage(); } catch (e) { console.error(e); } }); collections.forEach((collection) => { if (collection.query.refreshOnColumnsChange) { collection.hooks.addListener( 'beforeInitialFetch', () => initTask.status.promise, ); } }); } async _fetchFromStorage() { const { table: { viewsProps, columns, selectedColumns, customColumnsState, _toStoredColumn, }, } = this; if (viewsProps) { return; } if ( customColumnsState?.customColumnsHistory?.hasUrlColumnsForCurrentLayout ) { return; } const storedCustomColumns = await this._getStoredCustomColumns(); if (storedCustomColumns == null) { return; } const selectedColumnsFromStorage = columns.map((tableColumn) => { const existing = storedCustomColumns.find( (c) => c?.id === tableColumn.id, ); // If the column is not found in the stored columns, it means it was added after the last visit, so its "isSelected" state should be calculated based on the column prop passed via code if (existing == null) { return _toStoredColumn(tableColumn); } // If the column is found in the stored columns, it means it was already configured by the user, so its "isSelected" state should be taken from the stored columns const { id, name, hideable, hiddenFromCustomColumnsSelection } = tableColumn; return { id, name, isSelected: storedCustomColumns.some( (c) => c?.id === id && c?.isSelected !== false, ) || !hideable || hiddenFromCustomColumnsSelection === true, }; }); selectedColumns.setValue( customColumnsState?.dragAndDrop ? selectedColumnsFromStorage.sort( indexOfCompareFn(storedCustomColumns, (e) => e?.id), ) : selectedColumnsFromStorage, ); } customizeColumnsInSidePanelBi({ actionName, }: { actionName: CustomColumnsActionName; }) { if (this._performedCustomColumnsActions.get(actionName)) { return; } this._performedCustomColumnsActions.set(actionName, true); this.table.reportBi( withoutDefaults(cairoCustomizeColumnsInSidePanel)({ actionName, }), ); } get showFieldTypeIcons() { return this.table.showFieldTypeIcons; } get collectionFieldsSource() { return this.table.collectionFieldsSource; } get sidePanelTransition() { return this.table.sidePanel.transition; } onClose() { this.table.reportBi( withoutDefaults(cairoFiltersPanelUsed)({ isOpened: false, feature: 'Custom Columns', origin: 'X Button', }), ); this.table.sidePanel.close(); } }