@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.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +1 -0
- package/dist/backend/DataTable.js +167 -8
- package/dist/backend/DataTable.js.map +3 -3
- package/dist/backend/perspectiveDirty.js +55 -0
- package/dist/backend/perspectiveDirty.js.map +7 -0
- package/package.json +3 -3
- package/src/backend/DataTable.tsx +309 -6
- package/src/backend/__tests__/DataTable.saveViewApi.test.tsx +474 -0
- package/src/backend/__tests__/perspectiveDirty.test.ts +86 -0
- package/src/backend/perspectiveDirty.ts +136 -0
|
@@ -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
|
+
}
|