import { DataCapsule, TaskState } from '@wix/bex-core'; import { action, computed, makeObservable, observable, runInAction, } from 'mobx'; import { indexOfCompareFn } from '../indexOfCompareFn'; const STORAGE_KEY = 'schema-card-layout'; export interface SchemaViewConfig { id: string; isVisible?: boolean; hideable?: boolean; // not persisted — consumer-only; false = always visible } type StoredLayout = { fields: Pick[]; }; export interface SchemaFieldsStateParams { dataCapsule: DataCapsule; fields: SchemaViewConfig[]; } export class SchemaFieldsState { _storedFields: SchemaViewConfig[] = []; readonly initTask = new TaskState(); private _persistQueue: Promise = Promise.resolve(); private readonly params: SchemaFieldsStateParams; constructor(params: SchemaFieldsStateParams) { this.params = params; makeObservable(this, { _storedFields: observable, orderedFields: computed, setFieldVisibility: action.bound, setFieldOrder: action.bound, }); } init() { this.initTask.runOnce(async () => { try { await this._fetchFromStorage(); } catch (e) { console.error(e); } }); } get orderedFields(): SchemaViewConfig[] { if (!this._storedFields.length) { return this.params.fields; } // Fields not in storage all get index -1; stable sort preserves their declaration order return [...this.params.fields].sort( indexOfCompareFn(this._storedFields, (f) => f?.id), ); } isFieldVisible(id: string): boolean { const field = this.params.fields.find((f) => f.id === id); if (!field) { return true; } // unknown id — render by default if (field.hideable === false) { return true; } // non-hideable — always visible const stored = this._storedFields.find((f) => f.id === id); return stored ? stored.isVisible ?? true : field.isVisible ?? true; } setFieldVisibility(id: string, isVisible: boolean) { const idx = this._storedFields.findIndex((f) => f.id === id); if (idx >= 0) { // Replace element (not mutate in-place) so MobX tracks the array change this._storedFields[idx] = { ...this._storedFields[idx], isVisible }; } else { this._storedFields.push({ id, isVisible }); } // Wait for init to complete before persisting — prevents overwriting the // full stored layout when called before _fetchFromStorage resolves. // Serialize writes: chain onto the queue so rapid toggles don't race. this._persistQueue = this._persistQueue .then(() => this.initTask.status.promise) .then(() => this.persistFields()) .catch((e) => console.error(e)); } setFieldOrder(orderedFields: SchemaViewConfig[]) { const storedMap = new Map(this._storedFields.map((f) => [f.id, f])); const declaredMap = new Map(this.params.fields.map((f) => [f.id, f])); // Rebuild _storedFields in the new order, preserving existing visibility. // Only include fields that are still declared; unknown ids are silently dropped. this._storedFields = orderedFields .filter((f) => declaredMap.has(f.id)) .map( (f) => storedMap.get(f.id) ?? { id: f.id, isVisible: declaredMap.get(f.id)!.isVisible ?? true, }, ); this._persistQueue = this._persistQueue .then(() => this.initTask.status.promise) .then(() => this.persistFields()) .catch((e) => console.error(e)); } private async persistFields() { const fields = this._storedFields.map(({ id, isVisible }) => ({ id, isVisible, })); await this.params.dataCapsule.setItem(STORAGE_KEY, { fields }); } async _fetchFromStorage() { const { dataCapsule, fields } = this.params; let storedLayout: StoredLayout | null = null; try { const raw = await dataCapsule.getItem(STORAGE_KEY); storedLayout = raw && typeof raw === 'object' && Array.isArray(raw.fields) ? raw : null; } catch (e) { if (e instanceof Error && e.message === 'Key was not found in capsule') { storedLayout = null; } else { throw e; } } if (!storedLayout) { return; } // Merge: keep stored order/visibility; append new fields; drop removed ones. // Dedup by id — keeps the first occurrence, discarding any corrupt duplicates. const declaredIds = new Set(fields.map((f) => f.id)); const seenIds = new Set(); const merged: SchemaViewConfig[] = storedLayout.fields.filter((f) => { if (!f?.id || !declaredIds.has(f.id) || seenIds.has(f.id)) { return false; } seenIds.add(f.id); return true; }); fields.forEach((f) => { if (!merged.some((s) => s.id === f.id)) { merged.push({ id: f.id, isVisible: f.isVisible ?? true }); } }); runInAction(() => { this._storedFields = merged; }); } }