@open-mercato/ui 0.6.8-develop.6914.1.6c7a5dcb6b → 0.6.8-develop.6915.1.ac6031c503

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,136 @@
1
+ import type { PerspectiveSettings } from '@open-mercato/shared/modules/perspectives/types'
2
+
3
+ /**
4
+ * Setting groups the unsaved-changes comparison tracks.
5
+ *
6
+ * `pageSize` is deliberately absent: `DataTable` never emits it from
7
+ * `getCurrentSettings()`, so comparing it would report a permanent
8
+ * false-positive for every saved view that carries one.
9
+ */
10
+ export type DataTableViewSettingKey =
11
+ | 'columnOrder'
12
+ | 'columnVisibility'
13
+ | 'columnSizing'
14
+ | 'sorting'
15
+ | 'filters'
16
+ | 'searchValue'
17
+
18
+ /** Read-only view state handed to `DataTableProps.onColumnsDirtyChange`. */
19
+ export type DataTableViewDirtyState = {
20
+ /** True when the live table settings differ from the last applied/saved view. */
21
+ isDirty: boolean
22
+ /** Which setting groups differ, in a stable order. */
23
+ changedKeys: DataTableViewSettingKey[]
24
+ /** `changedKeys.length` — ready to render as a badge counter. */
25
+ changedCount: number
26
+ /** The currently applied perspective (personal or role), if any. */
27
+ activePerspectiveId: string | null
28
+ /**
29
+ * True when a save can target the active personal view without asking for a
30
+ * name. Role perspectives and "No view" both yield false — those need the
31
+ * views sidebar (or an explicit `name`) to save.
32
+ */
33
+ canSaveToActiveView: boolean
34
+ }
35
+
36
+ function stableStringify(value: unknown): string {
37
+ if (value === null || typeof value !== 'object') return JSON.stringify(value ?? null)
38
+ if (Array.isArray(value)) return `[${value.map((entry) => stableStringify(entry)).join(',')}]`
39
+ const entries = Object.entries(value as Record<string, unknown>)
40
+ .filter(([, entryValue]) => entryValue !== undefined)
41
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
42
+ return `{${entries.map(([key, entryValue]) => `${JSON.stringify(key)}:${stableStringify(entryValue)}`).join(',')}}`
43
+ }
44
+
45
+ function normalizeOrder(order: string[] | undefined, fallback: string[]): string[] {
46
+ return Array.isArray(order) && order.length > 0 ? order : fallback
47
+ }
48
+
49
+ /**
50
+ * `true` entries are equivalent to an absent entry: the column chooser deletes a
51
+ * column's record when it is turned back on, so `{ name: true }` and `{}` both
52
+ * mean "name is visible".
53
+ */
54
+ function normalizeVisibility(visibility: Record<string, boolean> | undefined): Record<string, boolean> {
55
+ if (!visibility) return {}
56
+ const result: Record<string, boolean> = {}
57
+ for (const [key, value] of Object.entries(visibility)) {
58
+ if (value === false) result[key] = false
59
+ }
60
+ return result
61
+ }
62
+
63
+ function normalizeSizing(sizing: Record<string, number> | undefined): Record<string, number> {
64
+ if (!sizing) return {}
65
+ const result: Record<string, number> = {}
66
+ for (const [key, value] of Object.entries(sizing)) {
67
+ if (typeof value === 'number' && Number.isFinite(value)) result[key] = Math.round(value)
68
+ }
69
+ return result
70
+ }
71
+
72
+ function normalizeSorting(sorting: PerspectiveSettings['sorting']): Array<{ id: string; desc: boolean }> {
73
+ if (!Array.isArray(sorting)) return []
74
+ return sorting.map((entry) => ({ id: String(entry?.id ?? ''), desc: entry?.desc === true }))
75
+ }
76
+
77
+ function normalizeSearch(searchValue: string | undefined): string {
78
+ return typeof searchValue === 'string' ? searchValue.trim() : ''
79
+ }
80
+
81
+ export type DiffPerspectiveSettingsOptions = {
82
+ /**
83
+ * Column ids in their natural (unordered) sequence. Used as the baseline for
84
+ * `columnOrder` when neither side stored one, so a table that merely
85
+ * materialized its default order is not reported as dirty.
86
+ */
87
+ defaultColumnOrder?: string[]
88
+ }
89
+
90
+ /**
91
+ * Compare the live table settings against the settings of the last applied or
92
+ * saved view and return the setting groups that differ.
93
+ *
94
+ * Both sides are expected to be sanitized (`sanitizePerspectiveSettings`), and
95
+ * the comparison is intentionally value-based rather than reference-based so a
96
+ * re-render alone never flips the result.
97
+ */
98
+ export function diffPerspectiveSettings(
99
+ base: PerspectiveSettings | null | undefined,
100
+ current: PerspectiveSettings | null | undefined,
101
+ options?: DiffPerspectiveSettingsOptions,
102
+ ): DataTableViewSettingKey[] {
103
+ const baseSettings = base ?? {}
104
+ const currentSettings = current ?? {}
105
+ const defaultColumnOrder = options?.defaultColumnOrder ?? []
106
+ const changed: DataTableViewSettingKey[] = []
107
+
108
+ const baseOrder = normalizeOrder(baseSettings.columnOrder, defaultColumnOrder)
109
+ const currentOrder = normalizeOrder(currentSettings.columnOrder, defaultColumnOrder)
110
+ if (stableStringify(baseOrder) !== stableStringify(currentOrder)) changed.push('columnOrder')
111
+
112
+ if (
113
+ stableStringify(normalizeVisibility(baseSettings.columnVisibility))
114
+ !== stableStringify(normalizeVisibility(currentSettings.columnVisibility))
115
+ ) changed.push('columnVisibility')
116
+
117
+ if (
118
+ stableStringify(normalizeSizing(baseSettings.columnSizing))
119
+ !== stableStringify(normalizeSizing(currentSettings.columnSizing))
120
+ ) changed.push('columnSizing')
121
+
122
+ if (
123
+ stableStringify(normalizeSorting(baseSettings.sorting))
124
+ !== stableStringify(normalizeSorting(currentSettings.sorting))
125
+ ) changed.push('sorting')
126
+
127
+ if (stableStringify(baseSettings.filters ?? null) !== stableStringify(currentSettings.filters ?? null)) {
128
+ changed.push('filters')
129
+ }
130
+
131
+ if (normalizeSearch(baseSettings.searchValue) !== normalizeSearch(currentSettings.searchValue)) {
132
+ changed.push('searchValue')
133
+ }
134
+
135
+ return changed
136
+ }