import { QTableColumn } from 'quasar'; import { INITIAL_SETTINGS } from '../stores/main/constants'; import type { Settings, Table } from '../stores/main/models'; /** * "Is this settings tab configured?" — the source of the primary dot next to a * tab label in TheTableSettings.vue. A tab counts as configured when any of the * settings it owns differs from the factory baseline, i.e. from exactly what * the Reset button would produce. */ /** * Factory-default settings for a table: INITIAL_SETTINGS with every column * visible. Deep-cloned on the way out: a spread copy would still alias the * nested members of the shared constant. */ export function factorySettings(table: Table): Settings { const allColumns: string[] = table.source?.columns?.map((el: QTableColumn) => el.name) ?? []; return JSON.parse( JSON.stringify({ ...INITIAL_SETTINGS, columns: { ...INITIAL_SETTINGS.columns, visibleColumns: allColumns, hiddenColumns: [], }, }) ) as Settings; } /** Structural equality for settings values. */ export function deepEqual(a: unknown, b: unknown): boolean { if (a === b) return true; if (a === null || b === null) return false; if (typeof a !== 'object' || typeof b !== 'object') return false; if (Array.isArray(a) || Array.isArray(b)) { if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) { return false; } return a.every((el, i) => deepEqual(el, b[i])); } const objA = a as Record; const objB = b as Record; const keysA = Object.keys(objA); const keysB = Object.keys(objB); if (keysA.length !== keysB.length) return false; return keysA.every( (key) => Object.prototype.hasOwnProperty.call(objB, key) && deepEqual(objA[key], objB[key]) ); } /** * Settings paths each tab owns, keyed by the tab names used in * TheTableSettings.vue. Deliberately excluded: controls the `style` panel * duplicates from other tabs (top.hideHeader, bottom.hidePagination) — one * toggle must not light two dots. */ export const TAB_SETTING_PATHS: Record = { dimensions: ['dimensions.tableHeight', 'dimensions.tableMaxWidth'], top: ['top.hideHeader', 'top.hideTitle', 'top.stickyHeader'], style: [ 'general.backgroundColor', 'general.textColor', 'general.dense', 'general.flat', 'general.separator', 'top.headerBackgroundColor', 'top.headerTextColor', 'top.textColor', 'top.topBackgroundColor', 'bottom.backgroundColor', 'bottom.textColor', 'content.buttonBgColor', 'content.buttonTextColor', 'content.linkTextColor', ], reset: [], }; function readPath(source: unknown, path: string): unknown { return path .split('.') .reduce( (acc, key) => acc && typeof acc === 'object' ? (acc as Record)[key] : undefined, source ); } /** True when any setting owned by `tab` differs from the factory baseline. */ export function isTabConfigured( tab: string, settings: Settings | null, baseline: Settings | null ): boolean { if (!settings || !baseline) return false; return (TAB_SETTING_PATHS[tab] ?? []).some((path) => { const current = readPath(settings, path); // Settings saved before a field existed simply carry no value — nothing the // user configured, so nothing to flag. if (current === undefined) return false; return !deepEqual(current, readPath(baseline, path)); }); }