import { addEventListener, FiltersMap, TaskState, TypedEmitter, WixPatternsContainer, WixPatternsError, } from '@wix/bex-core'; import { ViewsState } from './ViewsState'; import type { ViewsProps } from '../index'; import { action, computed, makeObservable, reaction, runInAction } from 'mobx'; import { AllItemsViewProps, Category, getViewSelectedColumnsForLayout, PrivateView, View, ViewType, } from '../model'; import { ToolbarCollectionState } from './ToolbarCollectionState'; import { CollectionViewsStateBIReporter } from './CollectionViewsStateBIReporter'; import { EventEmitter } from 'events'; import { indexOfCompareFn } from './indexOfCompareFn'; import { CollectionViewsQueryCompareHelper } from './CollectionViewsQueryCompareHelper'; import { ViewsHistoryState } from './ViewsHistoryState'; import { MigrateOrdersViewsState } from '../hooks/useMigrateOrdersViews'; import { FixMissingContactsViewsState } from '../hooks/useFixMissingContactsViews'; export interface CollectionViewsStateParams { presets?: Category[]; container: WixPatternsContainer; table: ToolbarCollectionState; allItemsViewProps?: AllItemsViewProps; onDeleteView?: (view: View) => void | Promise; onSaveViewChanges?: (view: View) => void | Promise; onSaveView?: (view: View) => void | Promise; onRenameView?: (view: View) => void | Promise; onSetAsDefaultView?: ( view: View, isDefault: boolean, ) => void | Promise; learnMoreLink?: ViewsProps['learnMoreLink']; viewNamePlaceholder?: ViewsProps['viewNamePlaceholder']; } export class CollectionViewsState { readonly views: ViewsState; readonly table: ToolbarCollectionState; readonly initTask = new TaskState(); readonly emitter = new EventEmitter() as TypedEmitter<{ openManageViewPopoverMenu: () => unknown; }>; readonly queryCompareHelper: CollectionViewsQueryCompareHelper; readonly onDeleteView?: (view: View) => void | Promise; readonly onSaveViewChanges?: (view: View) => void | Promise; readonly onSaveView?: (view: View) => void | Promise; readonly onRenameView?: (view: View) => void | Promise; readonly onSetAsDefaultView?: ( view: View, isDefault: boolean, ) => void | Promise; readonly learnMoreLink?: string; readonly viewNamePlaceholder?: string; viewsStateBIReporter: CollectionViewsStateBIReporter; getLayout(): ViewType | undefined { return this.table.layoutState?.viewType; } constructor(params: CollectionViewsStateParams) { this.table = params.table; this.onDeleteView = params.onDeleteView; this.onSaveViewChanges = params.onSaveViewChanges; this.onSaveView = params.onSaveView; this.onRenameView = params.onRenameView; this.onSetAsDefaultView = params.onSetAsDefaultView; this.learnMoreLink = params.learnMoreLink; this.viewNamePlaceholder = params.viewNamePlaceholder; this.views = new ViewsState({ oldNamespace: this.table.namespace, container: params.container, presets: params.presets, fqdn: this.table.collection.queryName, allItemsViewProps: params.allItemsViewProps, reportBI: this.table.reportBi, }); this.views._migrateOrdersViews = (viewsState) => { ( this.table.collection as MigrateOrdersViewsState )._migrateOrdersViews?.(viewsState); }; this.views._fixMissingContactsViews = (viewsState) => { ( this.table.collection as FixMissingContactsViewsState )._fixMissingContactsViews?.(viewsState); }; this.queryCompareHelper = new CollectionViewsQueryCompareHelper({ table: this.table, collectionViews: this, }); this.viewsStateBIReporter = new CollectionViewsStateBIReporter({ reportBI: this.table.reportBi, collectionViewState: this, }); makeObservable(this, { _setSelectedViewToCollectionQuery: action.bound, _onSelectViewAndRefresh: action.bound, _selectInitialView: action.bound, isViewChanged: computed, currentViewColumns: computed, currentViewColumnsOrDefaultColumns: computed, }); } get isViewChanged() { const { views: { currentView }, } = this; return ( !currentView || this.queryCompareHelper.isSelectedColumnsChanged || this.queryCompareHelper.isSortChanged || this.queryCompareHelper.isFiltersChanged ); } get _doNotWaitForViewsExperimentEnabled() { const { table: { container: { experiments, internalExperiments }, }, } = this; return ( internalExperiments?.enabled('specs.cairo.doNotWaitForViews') || experiments.enabled('specs.cairo.stores.doNotWaitForViews') ); } init() { const { views, initTask, table: { collection, container: { history }, }, } = this; if (this._doNotWaitForViewsExperimentEnabled) { this._selectInitialView(views._getAllItemsAndPresetViews()); } const disposers = [ addEventListener( collection.hooks, 'beforeInitialFetch', action(async () => { if ( !this._doNotWaitForViewsExperimentEnabled && !collection.query._initializedFromHistory ) { await this.initTask.status.promise?.catch(() => {}); } }), ), views.init(), ...this._subscribeOnSelectView(), ...this._subscribeOnViewAction(), this.viewsStateBIReporter.init(), new ViewsHistoryState({ views: this.views, history, }).init(), reaction( () => this.table.layoutState?.viewType, (newLayout, prevLayout) => { if (!newLayout || !prevLayout || newLayout === prevLayout) { return; } if (this.table._cacheRestoredLayout === newLayout) { return; } if (!this.views.currentView) { return; } const cols = this.currentViewColumns; if (cols) { this.table.selectedColumns.setValue(cols); } else { this.table.selectedColumns.reset(); } }, ), ]; initTask.runOnce(async () => { await views.initTask.status.promise; runInAction(() => { if (this._selectInitialView(views.collection.result.items)) { // when not waiting for views, we need to refresh the collection as the initialView after views loaded might have changed the query if (this._doNotWaitForViewsExperimentEnabled) { this.table.collection.clearResultAndMoveToStart(); } } }); }); return () => { disposers.forEach((disposer) => disposer()); }; } _selectInitialView(items: PrivateView[]): void | boolean { const { views, table: { collection }, } = this; if (this.views.currentView) { return; } const initialSelectItem = views._getInitialSelectItem(items); if (initialSelectItem == null) { return; } if ( !views._initialViewIdFromUrl && collection.query._initializedFromHistory ) { return; } const view = views.getView(initialSelectItem.id); views.currentView = view ?? null; if (!collection.query._initializedFromHistory) { this._setSelectedViewToCollectionQuery(); return true; } } private _subscribeOnViewAction() { const { views } = this; return [ addEventListener( views.events, 'undoAction', this.viewsStateBIReporter.undoActionBI.bind(this.viewsStateBIReporter), ), addEventListener( views.events, 'tryAgainAction', this.viewsStateBIReporter.tryAgainBI.bind(this.viewsStateBIReporter), ), addEventListener( views.events, 'errorAction', this.viewsStateBIReporter.errorBI.bind(this.viewsStateBIReporter), ), ]; } private _subscribeOnSelectView() { const { views } = this; return [ addEventListener( views.events, 'selectAndRefresh', action(this._onSelectViewAndRefresh), ), ]; } _onSelectViewAndRefresh(needFiltersReset = true) { if (!needFiltersReset) { return; } const { table: { collection }, views: { currentView }, } = this; const { query } = collection; // Layout must settle first — _setSelectedViewToCollectionQuery reads layout-aware columns. if (this.table.layoutState && currentView?.layout) { if (currentView.layout !== this.table.layoutState.viewType) { this.table._skipNextLayoutCoordinator = true; } this.table.layoutState.viewType = currentView.layout; } this._setSelectedViewToCollectionQuery(); query.queryHistory?.persistAll(); query.events.emit('refresh'); // A view selection is a clean reset of column config for every layout. this.table._clearColumnsCache(); } get currentViewColumns() { const { views, table } = this; const { columns, customColumnsState } = table; const { currentView } = views; const viewSelectedColumns = getViewSelectedColumnsForLayout( currentView, this.getLayout(), ); if (!viewSelectedColumns?.length) { return null; } const selectedColumnsToSet = columns.map( ({ id, hideable, hiddenFromCustomColumnsSelection }) => ({ id, isSelected: viewSelectedColumns.some( (c) => c.id === id && c.isSelected !== false, ) || !hideable || hiddenFromCustomColumnsSelection === true, }), ); return customColumnsState?.dragAndDrop ? selectedColumnsToSet.sort( indexOfCompareFn(viewSelectedColumns, (e) => e?.id), ) : selectedColumnsToSet; } get currentViewColumnsOrDefaultColumns() { return this.currentViewColumns ?? this.table.selectedColumns.defaultValue; } _setSelectedViewToCollectionQuery() { const { views, table } = this; const { selectedColumns, collection } = table; const { currentView } = views; const { query } = collection; query.resetFilters(); if (currentView?.filters) { Object.entries(currentView.filters).forEach(([filterName, filter]) => { const queryFilter = query.filters[filterName]; if (queryFilter && queryFilter.value !== filter?.value) { queryFilter.setValue(queryFilter.decode(filter?.value)); } }); } query.resetSorting(); if (currentView?.sortDirections && currentView.sortDirections.length > 0) { query.sort.setSortQuery( currentView.sortDirections.map((sortInView) => ({ field: sortInView.columnId, direction: sortInView.direction === 'Ascending' ? 'asc' : 'desc', })), { emitChangeEvent: true }, ); } else { query.resetSorting({ emitChangeEvent: true }); } if (this.currentViewColumns) { selectedColumns.setValue(this.currentViewColumns); } else { selectedColumns.reset(); } query.setExtra(currentView?.extra); } async deleteView(view?: PrivateView) { try { const viewToDelete = view ?? this.views.currentView; if (!viewToDelete) { return; } this.viewsStateBIReporter.deleteViewSelectedBI(); this.onDeleteView?.(viewToDelete); await this.views.deleteView(viewToDelete); this.viewsStateBIReporter.deleteViewBI(viewToDelete); } catch (e) { throw new WixPatternsError({ originalException: e, errorCode: 'DeleteViewFailed', }); } } async saveViewChanges() { this.viewsStateBIReporter.saveViewChangesSelectedBI(); const oldView = this.views.currentView ? ({ ...this.views.currentView } as PrivateView) : undefined; const newView = await this.views.saveViewChanges( this.queryCompareHelper.currentViewBase, ); if (newView) { this.onSaveViewChanges?.(newView); this.viewsStateBIReporter.saveViewChangesBI(newView, oldView); } // Every cached layout was just flushed into the saved view by currentViewBase. this.table._clearColumnsCache(); } sendSaveNewViewSelectedBI() { this.viewsStateBIReporter.saveNewViewSelectedBI(); } async saveNewView(name: string) { try { const newView = await this.views.saveView({ newView: this.views.createNewView( name, this.queryCompareHelper.currentViewBase, ), isNew: true, oldViewId: this.views.currentView?.id, }); this.onSaveView?.(newView); this.viewsStateBIReporter.saveNewViewBI(newView); // The persisted baseline now matches every cached layout (currentViewBase // flushed each one into the saved view), so the whole cache is stale. this.table._clearColumnsCache(); } catch (err) { throw new WixPatternsError({ errorCode: 'SaveViewFailed', originalException: err, }); } } sendRenameViewSelectedBI() { this.viewsStateBIReporter.renameViewSelectedBI(); } async renameView(name: string) { try { if (!this.views.currentView) { return; } const oldName = this.views.currentView.name; const newView = await this.views.renameView( { ...this.views.currentView, name } as PrivateView, this.views.currentView?.name ?? '', ); this.onRenameView?.(newView); this.viewsStateBIReporter.renameViewBI(newView, oldName); } catch (e) { throw new WixPatternsError({ originalException: e, errorCode: 'RenameViewFailed', }); } } async setAsDefaultView() { try { if (!this.views.currentView) { return; } const isDefault = this.views.defaultViewId !== this.views.currentView.id; this.viewsStateBIReporter.setAsDefaultViewSelectedBI(isDefault); const newView = await this.views.setAsDefaultView( this.views.currentView, isDefault, ); this.onSetAsDefaultView?.(newView, isDefault); this.viewsStateBIReporter.setAsDefaultViewBI(isDefault, newView); } catch (e) { throw new WixPatternsError({ originalException: e, errorCode: 'SetAsDefaultViewFailed', }); } } }