@tita-agent/dsh-data-table 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/README.md +172 -2
- package/cordis.patch.yml +7 -0
- package/lib/client.js +16766 -0
- package/lib/contract-envelope.d.ts +91 -0
- package/lib/contract-envelope.js +106 -0
- package/lib/contract-layout.d.ts +130 -0
- package/lib/contract-layout.js +138 -0
- package/lib/contract-refs.d.ts +39 -0
- package/lib/contract-refs.js +9 -0
- package/lib/contract-version.d.ts +311 -0
- package/lib/contract-version.js +270 -0
- package/lib/contract-wire.d.ts +147 -0
- package/lib/contract-wire.js +99 -0
- package/lib/contracts.d.ts +12 -0
- package/lib/contracts.js +12 -0
- package/lib/types/audit.d.ts +42 -0
- package/lib/types/audit.js +39 -0
- package/lib/types/client/ProjectImportMenu.d.ts +30 -0
- package/lib/types/client/ProjectImportMenu.js +70 -0
- package/lib/types/client/SheetPanel.d.ts +33 -0
- package/lib/types/client/SheetPanel.js +1766 -0
- package/lib/types/client/SheetToggle.d.ts +18 -0
- package/lib/types/client/SheetToggle.js +26 -0
- package/lib/types/client/index.d.ts +15 -0
- package/lib/types/client/index.js +110 -0
- package/lib/types/client/persistence/changes.d.ts +15 -0
- package/lib/types/client/persistence/changes.js +51 -0
- package/lib/types/client/persistence/ids.d.ts +34 -0
- package/lib/types/client/persistence/ids.js +48 -0
- package/lib/types/client/persistence/merge.d.ts +35 -0
- package/lib/types/client/persistence/merge.js +362 -0
- package/lib/types/client/store.d.ts +61 -0
- package/lib/types/client/store.js +152 -0
- package/lib/types/client/surface-context.d.ts +27 -0
- package/lib/types/client/surface-context.js +104 -0
- package/lib/types/client/surface-control.d.ts +2 -0
- package/lib/types/client/surface-control.js +12 -0
- package/lib/types/client/transport/workbookApi.d.ts +159 -0
- package/lib/types/client/transport/workbookApi.js +246 -0
- package/lib/types/client/univer/StandaloneWorkbookPage.d.ts +5 -0
- package/lib/types/client/univer/StandaloneWorkbookPage.js +7 -0
- package/lib/types/client/univer/WorkbookCanvas.d.ts +40 -0
- package/lib/types/client/univer/WorkbookCanvas.js +87 -0
- package/lib/types/client/univer/WorkbookView.d.ts +3 -0
- package/lib/types/client/univer/WorkbookView.js +2 -0
- package/lib/types/client/univer/loader.d.ts +18 -0
- package/lib/types/client/univer/loader.js +81 -0
- package/lib/types/client/univer/standalone-entry.d.ts +1 -0
- package/lib/types/client/univer/standalone-entry.js +19 -0
- package/lib/types/client/versions/VersionChrome.d.ts +33 -0
- package/lib/types/client/versions/VersionChrome.js +21 -0
- package/lib/types/client/versions/inspect.d.ts +70 -0
- package/lib/types/client/versions/inspect.js +88 -0
- package/lib/types/client/versions/preview-decoration.d.ts +11 -0
- package/lib/types/client/versions/preview-decoration.js +54 -0
- package/lib/types/client/versions/records.d.ts +19 -0
- package/lib/types/client/versions/records.js +70 -0
- package/lib/types/client/workspace-host.d.ts +61 -0
- package/lib/types/client/workspace-host.js +124 -0
- package/lib/types/collaboration-hub.d.ts +31 -0
- package/lib/types/collaboration-hub.js +69 -0
- package/lib/types/contract/ids.d.ts +24 -0
- package/lib/types/contract/ids.js +46 -0
- package/lib/types/contract/mutations.d.ts +25 -0
- package/lib/types/contract/mutations.js +782 -0
- package/lib/types/contract/operations.d.ts +54 -0
- package/lib/types/contract/operations.js +203 -0
- package/lib/types/contract/snapshots.d.ts +176 -0
- package/lib/types/contract/snapshots.js +351 -0
- package/lib/types/contract/univer-api-catalog.d.ts +29 -0
- package/lib/types/contract/univer-api-catalog.js +264 -0
- package/lib/types/errors.d.ts +8 -0
- package/lib/types/errors.js +12 -0
- package/lib/types/http/assets-routes.d.ts +3 -0
- package/lib/types/http/assets-routes.js +169 -0
- package/lib/types/http/collaboration.d.ts +10 -0
- package/lib/types/http/collaboration.js +154 -0
- package/lib/types/http/history.d.ts +15 -0
- package/lib/types/http/history.js +134 -0
- package/lib/types/http/project-files.d.ts +79 -0
- package/lib/types/http/project-files.js +206 -0
- package/lib/types/http/project-scope.d.ts +50 -0
- package/lib/types/http/project-scope.js +52 -0
- package/lib/types/http/routes.d.ts +98 -0
- package/lib/types/http/routes.js +323 -0
- package/lib/types/http/standalone.d.ts +9 -0
- package/lib/types/http/standalone.js +31 -0
- package/lib/types/http/trunk.d.ts +10 -0
- package/lib/types/http/trunk.js +322 -0
- package/lib/types/http/worktree.d.ts +9 -0
- package/lib/types/http/worktree.js +145 -0
- package/lib/types/index.d.ts +18 -0
- package/lib/types/index.js +156 -0
- package/lib/types/invariant.d.ts +15 -0
- package/lib/types/invariant.js +15 -0
- package/lib/types/request-params.d.ts +21 -0
- package/lib/types/request-params.js +76 -0
- package/lib/types/store/paths.d.ts +68 -0
- package/lib/types/store/paths.js +148 -0
- package/lib/types/store/project.d.ts +88 -0
- package/lib/types/store/project.js +120 -0
- package/lib/types/store/read.d.ts +110 -0
- package/lib/types/store/read.js +404 -0
- package/lib/types/store/retention.d.ts +27 -0
- package/lib/types/store/retention.js +111 -0
- package/lib/types/store/write.d.ts +22 -0
- package/lib/types/store/write.js +210 -0
- package/lib/types/tools/arguments.d.ts +40 -0
- package/lib/types/tools/arguments.js +130 -0
- package/lib/types/tools/cells.d.ts +50 -0
- package/lib/types/tools/cells.js +298 -0
- package/lib/types/tools/index.d.ts +2 -0
- package/lib/types/tools/index.js +250 -0
- package/lib/types/tools/render.d.ts +47 -0
- package/lib/types/tools/render.js +290 -0
- package/lib/types/tools/univer-api.d.ts +18 -0
- package/lib/types/tools/univer-api.js +198 -0
- package/lib/types/tools/workbook-catalog.d.ts +3 -0
- package/lib/types/tools/workbook-catalog.js +83 -0
- package/lib/types/tools/workbook-edit.d.ts +192 -0
- package/lib/types/tools/workbook-edit.js +912 -0
- package/lib/types/tools/workbook-find.d.ts +11 -0
- package/lib/types/tools/workbook-find.js +89 -0
- package/lib/types/tools/workbook-history.d.ts +2 -0
- package/lib/types/tools/workbook-history.js +59 -0
- package/lib/types/tools/workbook-read.d.ts +21 -0
- package/lib/types/tools/workbook-read.js +204 -0
- package/lib/types/types.d.ts +377 -0
- package/lib/types/types.js +34 -0
- package/lib/types/univer/apply-operation.d.ts +14 -0
- package/lib/types/univer/apply-operation.js +398 -0
- package/lib/types/univer/chunk.d.ts +68 -0
- package/lib/types/univer/chunk.js +16 -0
- package/lib/types/univer/decoration.d.ts +34 -0
- package/lib/types/univer/decoration.js +88 -0
- package/lib/types/univer/diagnostics.d.ts +31 -0
- package/lib/types/univer/diagnostics.js +141 -0
- package/lib/types/univer/engine.d.ts +33 -0
- package/lib/types/univer/engine.js +338 -0
- package/lib/types/univer/entry.d.ts +8 -0
- package/lib/types/univer/entry.js +11 -0
- package/lib/types/univer/facade.d.ts +107 -0
- package/lib/types/univer/facade.js +1 -0
- package/lib/types/univer/fonts.d.ts +202 -0
- package/lib/types/univer/fonts.js +140 -0
- package/lib/types/univer/plugins/collaboration.d.ts +23 -0
- package/lib/types/univer/plugins/collaboration.js +47 -0
- package/lib/types/univer/plugins/export.d.ts +39 -0
- package/lib/types/univer/plugins/export.js +246 -0
- package/lib/types/univer/plugins/history.d.ts +24 -0
- package/lib/types/univer/plugins/history.js +50 -0
- package/lib/types/univer/plugins/version.d.ts +13 -0
- package/lib/types/univer/plugins/version.js +13 -0
- package/lib/types/version/diff.d.ts +36 -0
- package/lib/types/version/diff.js +156 -0
- package/lib/types/version/grouping.d.ts +7 -0
- package/lib/types/version/grouping.js +94 -0
- package/lib/types/version/records.d.ts +17 -0
- package/lib/types/version/records.js +102 -0
- package/lib/types/version/session.d.ts +36 -0
- package/lib/types/version/session.js +89 -0
- package/lib/types/version/summary.d.ts +24 -0
- package/lib/types/version/summary.js +111 -0
- package/lib/types/workbook-manager.d.ts +191 -0
- package/lib/types/workbook-manager.js +935 -0
- package/lib/types/xlsx-export-worker.d.ts +5 -0
- package/lib/types/xlsx-export-worker.js +24 -0
- package/lib/types/xlsx-export.d.ts +21 -0
- package/lib/types/xlsx-export.js +371 -0
- package/lib/types/xlsx-import-worker.d.ts +1 -0
- package/lib/types/xlsx-import-worker.js +19 -0
- package/lib/univer-client.js +690 -0
- package/lib/workbook-page.js +23826 -0
- package/lib/xlsx-export-worker.js +104 -0
- package/lib/xlsx-import-worker.js +46 -0
- package/package.json +106 -3
- package/src/audit.ts +58 -0
- package/src/client/ProjectImportMenu.tsx +100 -0
- package/src/client/SheetPanel.module.css +1578 -0
- package/src/client/SheetPanel.tsx +1934 -0
- package/src/client/SheetToggle.module.css +73 -0
- package/src/client/SheetToggle.tsx +65 -0
- package/src/client/contract.d.ts +19 -0
- package/src/client/index.ts +131 -0
- package/src/client/persistence/changes.ts +58 -0
- package/src/client/persistence/ids.ts +60 -0
- package/src/client/persistence/merge.ts +392 -0
- package/src/client/store.ts +188 -0
- package/src/client/surface-context.ts +123 -0
- package/src/client/surface-control.ts +13 -0
- package/src/client/transport/workbookApi.ts +355 -0
- package/src/client/univer/SheetView.module.css +110 -0
- package/src/client/univer/StandaloneWorkbookPage.module.css +8 -0
- package/src/client/univer/StandaloneWorkbookPage.tsx +15 -0
- package/src/client/univer/WorkbookCanvas.tsx +128 -0
- package/src/client/univer/WorkbookView.module.css +30 -0
- package/src/client/univer/WorkbookView.tsx +3 -0
- package/src/client/univer/loader.ts +90 -0
- package/src/client/univer/react-dom-client.d.ts +10 -0
- package/src/client/univer/standalone-entry.tsx +19 -0
- package/src/client/univer/standalone.css +12 -0
- package/src/client/univer/univer-locales.d.ts +89 -0
- package/src/client/univer/univer-modules.d.ts +26 -0
- package/src/client/versions/VersionChrome.tsx +114 -0
- package/src/client/versions/inspect.ts +138 -0
- package/src/client/versions/preview-decoration.ts +54 -0
- package/src/client/versions/records.ts +78 -0
- package/src/client/workspace-host.ts +147 -0
- package/src/collaboration-hub.ts +83 -0
- package/src/contract/ids.ts +50 -0
- package/src/contract/mutations.ts +791 -0
- package/src/contract/operations.ts +221 -0
- package/src/contract/snapshots.ts +383 -0
- package/src/contract/univer-api-catalog.ts +289 -0
- package/src/css-modules.d.ts +6 -0
- package/src/errors.ts +25 -0
- package/src/http/assets-routes.ts +177 -0
- package/src/http/collaboration.ts +144 -0
- package/src/http/history.ts +151 -0
- package/src/http/project-files.ts +227 -0
- package/src/http/project-scope.ts +77 -0
- package/src/http/routes.ts +364 -0
- package/src/http/standalone.ts +32 -0
- package/src/http/trunk.ts +322 -0
- package/src/http/worktree.ts +119 -0
- package/src/index.ts +165 -0
- package/src/invariant.ts +17 -0
- package/src/request-params.ts +92 -0
- package/src/store/paths.ts +165 -0
- package/src/store/project.ts +137 -0
- package/src/store/read.ts +454 -0
- package/src/store/retention.ts +106 -0
- package/src/store/write.ts +230 -0
- package/src/tools/arguments.ts +128 -0
- package/src/tools/cells.ts +298 -0
- package/src/tools/index.ts +274 -0
- package/src/tools/render.ts +301 -0
- package/src/tools/univer-api.ts +243 -0
- package/src/tools/workbook-catalog.ts +94 -0
- package/src/tools/workbook-edit.ts +987 -0
- package/src/tools/workbook-find.ts +80 -0
- package/src/tools/workbook-history.ts +63 -0
- package/src/tools/workbook-read.ts +210 -0
- package/src/types.ts +432 -0
- package/src/univer/apply-operation.ts +369 -0
- package/src/univer/chunk.ts +77 -0
- package/src/univer/decoration.ts +111 -0
- package/src/univer/diagnostics.ts +136 -0
- package/src/univer/engine.ts +370 -0
- package/src/univer/entry.ts +14 -0
- package/src/univer/facade.ts +91 -0
- package/src/univer/fonts.ts +148 -0
- package/src/univer/overrides.css +33 -0
- package/src/univer/plugins/collaboration.ts +47 -0
- package/src/univer/plugins/export.ts +236 -0
- package/src/univer/plugins/history.ts +49 -0
- package/src/univer/plugins/version.ts +13 -0
- package/src/version/diff.ts +169 -0
- package/src/version/grouping.ts +96 -0
- package/src/version/records.ts +113 -0
- package/src/version/session.ts +90 -0
- package/src/version/summary.ts +117 -0
- package/src/workbook-manager.ts +1111 -0
- package/src/xlsx-export-worker.ts +34 -0
- package/src/xlsx-export.ts +373 -0
- package/src/xlsx-import-worker.ts +30 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Three-way merge for Univer workbook snapshots.
|
|
3
|
+
*
|
|
4
|
+
* The browser keeps the last server snapshot as the base. When a user save
|
|
5
|
+
* races an Agent/user write, this module compares base/local/remote at cell
|
|
6
|
+
* granularity, adopts one-sided changes automatically, and keeps local values
|
|
7
|
+
* for true conflicts while adding a visible conflict style.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { cellAddress } from '#contracts'
|
|
11
|
+
import { cloneSnapshot, normalizeCellFormula, stableJson, toCellValue } from '../../contract/snapshots.ts'
|
|
12
|
+
import type { WorkbookLocalChange, WorkbookOperation } from '../../types.ts'
|
|
13
|
+
|
|
14
|
+
type JsonRecord = Record<string, any>
|
|
15
|
+
type CellData = Record<string, Record<string, JsonRecord>>
|
|
16
|
+
|
|
17
|
+
export interface MergeConflict {
|
|
18
|
+
kind: 'cell' | 'structure' | 'sheet'
|
|
19
|
+
sheetId?: string
|
|
20
|
+
sheetName?: string
|
|
21
|
+
row?: number
|
|
22
|
+
column?: number
|
|
23
|
+
address?: string
|
|
24
|
+
baseValue?: unknown
|
|
25
|
+
localValue?: unknown
|
|
26
|
+
remoteValue?: unknown
|
|
27
|
+
description: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface WorkbookMergeResult {
|
|
31
|
+
snapshot: object
|
|
32
|
+
conflicts: MergeConflict[]
|
|
33
|
+
autoMergedCells: number
|
|
34
|
+
conflictStyleMap: Record<string, string | null>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface MergeSheetResult {
|
|
38
|
+
sheet: JsonRecord
|
|
39
|
+
conflicts: MergeConflict[]
|
|
40
|
+
autoMergedCells: number
|
|
41
|
+
conflictStyleMap: Record<string, string | null>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const MERGEABLE_SHEET_KEYS = [
|
|
45
|
+
'name',
|
|
46
|
+
'tabColor',
|
|
47
|
+
'hidden',
|
|
48
|
+
'zoomRatio',
|
|
49
|
+
'defaultColumnWidth',
|
|
50
|
+
'defaultRowHeight',
|
|
51
|
+
'freeze',
|
|
52
|
+
'mergeData',
|
|
53
|
+
'rowData',
|
|
54
|
+
'columnData',
|
|
55
|
+
'showGridlines',
|
|
56
|
+
'rowHeader',
|
|
57
|
+
'columnHeader',
|
|
58
|
+
'rightToLeft',
|
|
59
|
+
'defaultStyle',
|
|
60
|
+
'dataSheetMeta',
|
|
61
|
+
] as const
|
|
62
|
+
|
|
63
|
+
/** Merge local and remote snapshots against the last common base snapshot. */
|
|
64
|
+
export function mergeWorkbookSnapshots(baseSnapshot: object, localSnapshot: object, remoteSnapshot: object): WorkbookMergeResult {
|
|
65
|
+
const base = asRecord(baseSnapshot)
|
|
66
|
+
const local = asRecord(localSnapshot)
|
|
67
|
+
const remote = asRecord(remoteSnapshot)
|
|
68
|
+
const merged = cloneSnapshot(local)
|
|
69
|
+
const conflicts: MergeConflict[] = []
|
|
70
|
+
const conflictStyleMap: Record<string, string | null> = {}
|
|
71
|
+
const localStyles = asRecord(merged.styles)
|
|
72
|
+
const remoteStyles = asRecord(remote.styles)
|
|
73
|
+
const remoteStyleMap = new Map<string, string>()
|
|
74
|
+
|
|
75
|
+
const adoptRemoteCell = (cell: JsonRecord | undefined): JsonRecord | undefined => {
|
|
76
|
+
if (cell === undefined) return undefined
|
|
77
|
+
const next = cloneSnapshot(cell)
|
|
78
|
+
if (typeof next.s === 'string') next.s = remapRemoteStyle(next.s)
|
|
79
|
+
return next
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const remapRemoteStyle = (styleId: string): string => {
|
|
83
|
+
const known = remoteStyleMap.get(styleId)
|
|
84
|
+
if (known !== undefined) return known
|
|
85
|
+
const remoteStyle = remoteStyles[styleId]
|
|
86
|
+
if (remoteStyle === undefined) {
|
|
87
|
+
remoteStyleMap.set(styleId, styleId)
|
|
88
|
+
return styleId
|
|
89
|
+
}
|
|
90
|
+
const localStyle = localStyles[styleId]
|
|
91
|
+
if (localStyle === undefined || deepEqual(localStyle, remoteStyle)) {
|
|
92
|
+
localStyles[styleId] = cloneSnapshot(remoteStyle)
|
|
93
|
+
remoteStyleMap.set(styleId, styleId)
|
|
94
|
+
return styleId
|
|
95
|
+
}
|
|
96
|
+
let candidate = `${styleId}-remote`
|
|
97
|
+
let suffix = 1
|
|
98
|
+
while (localStyles[candidate] !== undefined) candidate = `${styleId}-remote-${suffix++}`
|
|
99
|
+
localStyles[candidate] = cloneSnapshot(remoteStyle)
|
|
100
|
+
remoteStyleMap.set(styleId, candidate)
|
|
101
|
+
return candidate
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const baseSheets = asRecord(base.sheets)
|
|
105
|
+
const localSheets = asRecord(local.sheets)
|
|
106
|
+
const remoteSheets = asRecord(remote.sheets)
|
|
107
|
+
const mergedSheets: JsonRecord = {}
|
|
108
|
+
const allSheetIds = new Set([...Object.keys(baseSheets), ...Object.keys(localSheets), ...Object.keys(remoteSheets)])
|
|
109
|
+
|
|
110
|
+
for (const sheetId of allSheetIds) {
|
|
111
|
+
const baseSheet = asOptionalRecord(baseSheets[sheetId])
|
|
112
|
+
const localSheet = asOptionalRecord(localSheets[sheetId])
|
|
113
|
+
const remoteSheet = asOptionalRecord(remoteSheets[sheetId])
|
|
114
|
+
if (localSheet === undefined && remoteSheet === undefined) continue
|
|
115
|
+
if (localSheet === undefined) {
|
|
116
|
+
if (baseSheet !== undefined && remoteSheet !== undefined && !deepEqual(remoteSheet, baseSheet)) {
|
|
117
|
+
conflicts.push(sheetConflict(sheetId, remoteSheet, '本地删除 sheet,但远端同时修改了它'))
|
|
118
|
+
}
|
|
119
|
+
// A sheet deleted locally remains deleted. A remote-only sheet is kept.
|
|
120
|
+
if (baseSheet === undefined && remoteSheet !== undefined) mergedSheets[sheetId] = cloneAndRemapSheet(remoteSheet, adoptRemoteCell)
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
if (remoteSheet === undefined) {
|
|
124
|
+
if (baseSheet !== undefined && !deepEqual(localSheet, baseSheet)) {
|
|
125
|
+
conflicts.push(sheetConflict(sheetId, localSheet, '远端删除 sheet,但本地同时修改了它'))
|
|
126
|
+
}
|
|
127
|
+
// A sheet deleted remotely is deleted only when local had not changed it.
|
|
128
|
+
if (baseSheet !== undefined && deepEqual(localSheet, baseSheet)) continue
|
|
129
|
+
mergedSheets[sheetId] = cloneSnapshot(localSheet)
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
const result = mergeSheet(sheetId, baseSheet, localSheet, remoteSheet, adoptRemoteCell, localStyles)
|
|
133
|
+
mergedSheets[sheetId] = result.sheet
|
|
134
|
+
conflicts.push(...result.conflicts)
|
|
135
|
+
Object.assign(conflictStyleMap, result.conflictStyleMap)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
merged.sheets = mergedSheets
|
|
139
|
+
merged.styles = localStyles
|
|
140
|
+
const baseOrder = asStringArray(base.sheetOrder)
|
|
141
|
+
const localOrder = asStringArray(local.sheetOrder)
|
|
142
|
+
const remoteOrder = asStringArray(remote.sheetOrder)
|
|
143
|
+
if (deepEqual(localOrder, baseOrder) && !deepEqual(remoteOrder, baseOrder)) merged.sheetOrder = remoteOrder.filter(id => mergedSheets[id] !== undefined)
|
|
144
|
+
else if (!deepEqual(localOrder, remoteOrder) && !deepEqual(remoteOrder, baseOrder)) {
|
|
145
|
+
conflicts.push({ kind: 'structure', description: '本地和远端同时调整了 sheet 顺序,当前保留本地顺序' })
|
|
146
|
+
}
|
|
147
|
+
merged.sheetOrder = normalizeSheetOrder(asStringArray(merged.sheetOrder), mergedSheets)
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
snapshot: merged,
|
|
151
|
+
conflicts,
|
|
152
|
+
autoMergedCells: countAutoMergedCells(base, local, remote, mergedSheets),
|
|
153
|
+
conflictStyleMap,
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Remove temporary conflict styles before a merged snapshot is persisted. */
|
|
158
|
+
export function stripConflictMarkers(snapshot: object, conflictStyleMap: Record<string, string | null>): object {
|
|
159
|
+
if (Object.keys(conflictStyleMap).length === 0) return snapshot
|
|
160
|
+
const next = cloneSnapshot(snapshot) as JsonRecord
|
|
161
|
+
const sheets = asRecord(next.sheets)
|
|
162
|
+
for (const sheet of Object.values(sheets)) {
|
|
163
|
+
const cellData = asRecord(sheet).cellData as CellData | undefined
|
|
164
|
+
if (cellData === undefined) continue
|
|
165
|
+
for (const [rowKey, row] of Object.entries(cellData)) {
|
|
166
|
+
for (const [columnKey, cell] of Object.entries(row)) {
|
|
167
|
+
const marker = typeof cell.s === 'string' ? conflictStyleMap[cell.s] : undefined
|
|
168
|
+
if (marker === undefined && !Object.prototype.hasOwnProperty.call(conflictStyleMap, cell.s)) continue
|
|
169
|
+
if (marker === null) delete cell.s
|
|
170
|
+
else cell.s = marker
|
|
171
|
+
if (Object.keys(cell).length === 1 && cell.v === null) delete row[columnKey]
|
|
172
|
+
}
|
|
173
|
+
if (Object.keys(row).length === 0) delete cellData[rowKey]
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const styles = asRecord(next.styles)
|
|
177
|
+
for (const markerId of Object.keys(conflictStyleMap)) delete styles[markerId]
|
|
178
|
+
next.styles = styles
|
|
179
|
+
return next
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Apply ordinary cell edits to the merge base without cloning the workbook. */
|
|
183
|
+
export function applyIncrementalChangesToSnapshot(snapshot: object, changes: WorkbookLocalChange[]): void {
|
|
184
|
+
const root = asRecord(snapshot)
|
|
185
|
+
const sheets = asRecord(root.sheets)
|
|
186
|
+
for (const change of changes) {
|
|
187
|
+
const sheet = sheets[change.payload.sheetId] ?? Object.values(sheets).find(candidate => asRecord(candidate).name === change.payload.sheetId)
|
|
188
|
+
if (sheet === undefined) continue
|
|
189
|
+
const cellData = (asRecord(sheet).cellData ??= {}) as CellData
|
|
190
|
+
for (const update of change.payload.cells) {
|
|
191
|
+
const row = (cellData[String(update.row)] ??= {})
|
|
192
|
+
if (update.value === null && (update.formula === undefined || update.formula.trim().length === 0)) {
|
|
193
|
+
delete row[String(update.column)]
|
|
194
|
+
if (Object.keys(row).length === 0) delete cellData[String(update.row)]
|
|
195
|
+
continue
|
|
196
|
+
}
|
|
197
|
+
const current = row[String(update.column)] ?? {}
|
|
198
|
+
row[String(update.column)] = { ...current, v: update.value }
|
|
199
|
+
const formula = normalizeCellFormula(update.formula)
|
|
200
|
+
if (formula === undefined) delete row[String(update.column)].f
|
|
201
|
+
else row[String(update.column)].f = formula
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Patch a merge base from a replayable operation log; false means fallback. */
|
|
207
|
+
export function applyIncrementalOperationsToSnapshot(snapshot: object, operations: WorkbookOperation[]): boolean {
|
|
208
|
+
const changes: WorkbookLocalChange[] = []
|
|
209
|
+
const collect = (operation: WorkbookOperation): boolean => {
|
|
210
|
+
if (operation.type === 'update_cells') {
|
|
211
|
+
const sheetId = operation.payload.sheetId ?? operation.payload.sheetName
|
|
212
|
+
if (sheetId === undefined || (operation.payload.cells ?? []).length === 0) return false
|
|
213
|
+
changes.push({ type: 'update_cells', payload: { sheetId, cells: operation.payload.cells ?? [] } })
|
|
214
|
+
return true
|
|
215
|
+
}
|
|
216
|
+
if (operation.type === 'batch') {
|
|
217
|
+
return (operation.payload.operations ?? []).every((child, index) => collect({
|
|
218
|
+
...operation,
|
|
219
|
+
opId: `${operation.opId}:${index}`,
|
|
220
|
+
type: child.type,
|
|
221
|
+
payload: child.payload,
|
|
222
|
+
}))
|
|
223
|
+
}
|
|
224
|
+
return false
|
|
225
|
+
}
|
|
226
|
+
if (!operations.every(collect)) return false
|
|
227
|
+
applyIncrementalChangesToSnapshot(snapshot, changes)
|
|
228
|
+
return true
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function mergeSheet(
|
|
232
|
+
sheetId: string,
|
|
233
|
+
baseSheet: JsonRecord | undefined,
|
|
234
|
+
localSheet: JsonRecord,
|
|
235
|
+
remoteSheet: JsonRecord,
|
|
236
|
+
adoptRemoteCell: (cell: JsonRecord | undefined) => JsonRecord | undefined,
|
|
237
|
+
styles: JsonRecord,
|
|
238
|
+
): MergeSheetResult {
|
|
239
|
+
const sheet = cloneSnapshot(localSheet)
|
|
240
|
+
const conflicts: MergeConflict[] = []
|
|
241
|
+
const conflictStyleMap: Record<string, string | null> = {}
|
|
242
|
+
let autoMergedCells = 0
|
|
243
|
+
const localRowCount = typeof localSheet.rowCount === 'number' ? localSheet.rowCount : 0
|
|
244
|
+
const remoteRowCount = typeof remoteSheet.rowCount === 'number' ? remoteSheet.rowCount : 0
|
|
245
|
+
const localColumnCount = typeof localSheet.columnCount === 'number' ? localSheet.columnCount : 0
|
|
246
|
+
const remoteColumnCount = typeof remoteSheet.columnCount === 'number' ? remoteSheet.columnCount : 0
|
|
247
|
+
sheet.rowCount = Math.max(localRowCount, remoteRowCount)
|
|
248
|
+
sheet.columnCount = Math.max(localColumnCount, remoteColumnCount)
|
|
249
|
+
|
|
250
|
+
for (const key of MERGEABLE_SHEET_KEYS) {
|
|
251
|
+
const baseValue = baseSheet?.[key]
|
|
252
|
+
const localValue = localSheet[key]
|
|
253
|
+
const remoteValue = remoteSheet[key]
|
|
254
|
+
if (deepEqual(localValue, remoteValue)) continue
|
|
255
|
+
if (deepEqual(localValue, baseValue)) {
|
|
256
|
+
if (remoteValue === undefined) delete sheet[key]
|
|
257
|
+
else sheet[key] = cloneSnapshot(remoteValue)
|
|
258
|
+
} else if (deepEqual(remoteValue, baseValue)) {
|
|
259
|
+
continue
|
|
260
|
+
} else {
|
|
261
|
+
conflicts.push({ kind: 'structure', sheetId, sheetName: sheetName(localSheet, sheetId), description: `sheet 属性 ${key} 在本地和远端均有修改,当前保留本地值` })
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const baseCells = asCellData(baseSheet?.cellData)
|
|
266
|
+
const localCells = asCellData(localSheet.cellData)
|
|
267
|
+
const remoteCells = asCellData(remoteSheet.cellData)
|
|
268
|
+
const mergedCells: CellData = {}
|
|
269
|
+
const rowKeys = new Set([...Object.keys(baseCells), ...Object.keys(localCells), ...Object.keys(remoteCells)])
|
|
270
|
+
for (const rowKey of rowKeys) {
|
|
271
|
+
const baseRow = baseCells[rowKey] ?? {}
|
|
272
|
+
const localRow = localCells[rowKey] ?? {}
|
|
273
|
+
const remoteRow = remoteCells[rowKey] ?? {}
|
|
274
|
+
const columnKeys = new Set([...Object.keys(baseRow), ...Object.keys(localRow), ...Object.keys(remoteRow)])
|
|
275
|
+
for (const columnKey of columnKeys) {
|
|
276
|
+
const baseCell = baseRow[columnKey]
|
|
277
|
+
const localCell = localRow[columnKey]
|
|
278
|
+
const remoteCell = remoteRow[columnKey]
|
|
279
|
+
let selected: JsonRecord | undefined
|
|
280
|
+
if (deepEqual(localCell, remoteCell)) selected = cloneOptional(localCell)
|
|
281
|
+
else if (deepEqual(localCell, baseCell)) {
|
|
282
|
+
selected = adoptRemoteCell(remoteCell)
|
|
283
|
+
autoMergedCells++
|
|
284
|
+
} else if (deepEqual(remoteCell, baseCell)) {
|
|
285
|
+
selected = cloneOptional(localCell)
|
|
286
|
+
autoMergedCells++
|
|
287
|
+
} else {
|
|
288
|
+
selected = cloneOptional(localCell) ?? { v: null }
|
|
289
|
+
const markerId = createConflictStyle(styles, selected, sheetId, Number(rowKey), Number(columnKey))
|
|
290
|
+
conflictStyleMap[markerId] = typeof localCell?.s === 'string' ? localCell.s : null
|
|
291
|
+
selected.s = markerId
|
|
292
|
+
conflicts.push({
|
|
293
|
+
kind: 'cell',
|
|
294
|
+
sheetId,
|
|
295
|
+
sheetName: sheetName(localSheet, sheetId),
|
|
296
|
+
row: Number(rowKey),
|
|
297
|
+
column: Number(columnKey),
|
|
298
|
+
address: `${sheetName(localSheet, sheetId)}!${cellAddress(Number(rowKey), Number(columnKey))}`,
|
|
299
|
+
baseValue: toCellValue(baseCell),
|
|
300
|
+
localValue: toCellValue(localCell),
|
|
301
|
+
remoteValue: toCellValue(remoteCell),
|
|
302
|
+
description: '本地和远端同时修改了同一单元格,当前保留本地值',
|
|
303
|
+
})
|
|
304
|
+
}
|
|
305
|
+
if (selected !== undefined) (mergedCells[rowKey] ??= {})[columnKey] = selected
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
sheet.cellData = mergedCells
|
|
309
|
+
return { sheet, conflicts, autoMergedCells, conflictStyleMap }
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function createConflictStyle(styles: JsonRecord, cell: JsonRecord, sheetId: string, row: number, column: number): string {
|
|
313
|
+
const original = typeof cell.s === 'string' ? asOptionalRecord(styles[cell.s]) ?? {} : {}
|
|
314
|
+
const baseId = `data-sheet-conflict-${sheetId}-${row}-${column}`
|
|
315
|
+
let id = baseId
|
|
316
|
+
let suffix = 1
|
|
317
|
+
while (styles[id] !== undefined) id = `${baseId}-${suffix++}`
|
|
318
|
+
styles[id] = { ...cloneSnapshot(original), bg: '#FFF2CC', cl: '#9C0006' }
|
|
319
|
+
return id
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function sheetConflict(sheetId: string, sheet: JsonRecord, description: string): MergeConflict {
|
|
323
|
+
return { kind: 'sheet', sheetId, sheetName: sheetName(sheet, sheetId), description }
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function sheetName(sheet: JsonRecord, fallback: string): string {
|
|
327
|
+
return typeof sheet.name === 'string' && sheet.name.length > 0 ? sheet.name : fallback
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function countAutoMergedCells(base: JsonRecord, local: JsonRecord, remote: JsonRecord, mergedSheets: JsonRecord): number {
|
|
331
|
+
let count = 0
|
|
332
|
+
const baseSheets = asRecord(base.sheets)
|
|
333
|
+
const localSheets = asRecord(local.sheets)
|
|
334
|
+
const remoteSheets = asRecord(remote.sheets)
|
|
335
|
+
for (const [sheetId, sheet] of Object.entries(mergedSheets)) {
|
|
336
|
+
const baseCells = asCellData(baseSheets[sheetId]?.cellData)
|
|
337
|
+
const localCells = asCellData(localSheets[sheetId]?.cellData)
|
|
338
|
+
const remoteCells = asCellData(remoteSheets[sheetId]?.cellData)
|
|
339
|
+
const mergedCells = asCellData(sheet.cellData)
|
|
340
|
+
for (const [rowKey, row] of Object.entries(mergedCells)) {
|
|
341
|
+
for (const columnKey of Object.keys(row)) {
|
|
342
|
+
const localCell = localCells[rowKey]?.[columnKey]
|
|
343
|
+
const remoteCell = remoteCells[rowKey]?.[columnKey]
|
|
344
|
+
const baseCell = baseCells[rowKey]?.[columnKey]
|
|
345
|
+
if (!deepEqual(localCell, remoteCell) && (deepEqual(localCell, baseCell) || deepEqual(remoteCell, baseCell))) count++
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return count
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function cloneAndRemapSheet(sheet: JsonRecord, adoptRemoteCell: (cell: JsonRecord | undefined) => JsonRecord | undefined): JsonRecord {
|
|
353
|
+
const next = cloneSnapshot(sheet)
|
|
354
|
+
const cellData = asCellData(next.cellData)
|
|
355
|
+
for (const [rowKey, row] of Object.entries(cellData)) {
|
|
356
|
+
for (const [columnKey, cell] of Object.entries(row)) row[columnKey] = adoptRemoteCell(cell) ?? cell
|
|
357
|
+
cellData[rowKey] = row
|
|
358
|
+
}
|
|
359
|
+
next.cellData = cellData
|
|
360
|
+
return next
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function normalizeSheetOrder(order: string[], sheets: JsonRecord): string[] {
|
|
364
|
+
const result = order.filter(id => sheets[id] !== undefined)
|
|
365
|
+
for (const id of Object.keys(sheets)) if (!result.includes(id)) result.push(id)
|
|
366
|
+
return result
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function asRecord(value: unknown): JsonRecord {
|
|
370
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as JsonRecord : {}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function asOptionalRecord(value: unknown): JsonRecord | undefined {
|
|
374
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as JsonRecord : undefined
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function asCellData(value: unknown): CellData {
|
|
378
|
+
return asRecord(value) as CellData
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function asStringArray(value: unknown): string[] {
|
|
382
|
+
return Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : []
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function cloneOptional<T>(value: T | undefined): T | undefined {
|
|
386
|
+
return value === undefined ? undefined : cloneSnapshot(value)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function deepEqual(left: unknown, right: unknown): boolean {
|
|
390
|
+
return stableJson(left) === stableJson(right)
|
|
391
|
+
}
|
|
392
|
+
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal sheet-panel store for dsh-data-table: the right-column
|
|
3
|
+
* open state. Workbook data lives server-side (node half, snapshot files);
|
|
4
|
+
* this store only tracks the panel toggle.
|
|
5
|
+
* @module dsh-data-table/client/store
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useSyncExternalStore } from 'react'
|
|
9
|
+
|
|
10
|
+
let sheetOpen = false
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Last workbook the user opened. DSH remounts the pane when the conversation
|
|
14
|
+
* tab changes, so the selection has to live outside React; localStorage keeps
|
|
15
|
+
* it across a reload too. Persistence is best-effort: a locked-down browser
|
|
16
|
+
* simply falls back to an in-memory value for this page life.
|
|
17
|
+
*/
|
|
18
|
+
const LAST_WORKBOOK_KEY = 'dsh.data-sheet.lastWorkbookId'
|
|
19
|
+
let lastWorkbookId = ''
|
|
20
|
+
let lastWorkbookLoaded = false
|
|
21
|
+
|
|
22
|
+
function readLastWorkbook(): string {
|
|
23
|
+
if (lastWorkbookLoaded) return lastWorkbookId
|
|
24
|
+
lastWorkbookLoaded = true
|
|
25
|
+
try {
|
|
26
|
+
lastWorkbookId = globalThis.localStorage?.getItem(LAST_WORKBOOK_KEY) ?? ''
|
|
27
|
+
} catch {
|
|
28
|
+
lastWorkbookId = ''
|
|
29
|
+
}
|
|
30
|
+
return lastWorkbookId
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The workbook to reopen when the pane mounts without an explicit target. */
|
|
34
|
+
export function lastSelectedWorkbookId(): string {
|
|
35
|
+
return readLastWorkbook()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Record the active workbook; call with an empty string when it is removed. */
|
|
39
|
+
export function rememberWorkbook(id: string): void {
|
|
40
|
+
readLastWorkbook()
|
|
41
|
+
if (lastWorkbookId === id) return
|
|
42
|
+
lastWorkbookId = id
|
|
43
|
+
try {
|
|
44
|
+
if (id === '') globalThis.localStorage?.removeItem(LAST_WORKBOOK_KEY)
|
|
45
|
+
else globalThis.localStorage?.setItem(LAST_WORKBOOK_KEY, id)
|
|
46
|
+
} catch {
|
|
47
|
+
// Ignore storage failures; the in-memory value still survives remounts.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The **unretained editing session** the next save joins, per workbook
|
|
52
|
+
* (PRD v3.3 D22).
|
|
53
|
+
*
|
|
54
|
+
* The session is not a saved Version: it keeps accumulating Steps and only
|
|
55
|
+
* becomes one when the idle window closes it or it reaches the Step cap. It has
|
|
56
|
+
* to outlive the React panel, because DSH remounts the pane whenever the
|
|
57
|
+
* conversation tab changes — losing the cursor there was what made "every visit
|
|
58
|
+
* is its own version", i.e. what users read as "leaving the artifact saved a
|
|
59
|
+
* version". localStorage is best-effort; the in-memory map covers a browser
|
|
60
|
+
* that refuses storage.
|
|
61
|
+
*/
|
|
62
|
+
export interface VersionSession {
|
|
63
|
+
/** The Version id the next save joins; `null` when there is nothing open. */
|
|
64
|
+
versionId: string
|
|
65
|
+
/** Steps already committed into that Version. */
|
|
66
|
+
stepCount: number
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const VERSION_SESSION_PREFIX = 'dsh.data-sheet.versionSession.'
|
|
70
|
+
const versionSessions = new Map<string, VersionSession>()
|
|
71
|
+
|
|
72
|
+
function versionSessionKey(workbookId: string): string {
|
|
73
|
+
return `${VERSION_SESSION_PREFIX}${workbookId}`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The open session for a workbook, or `null` when there is none. */
|
|
77
|
+
export function readVersionSession(workbookId: string): VersionSession | null {
|
|
78
|
+
if (workbookId === '') return null
|
|
79
|
+
const cached = versionSessions.get(workbookId)
|
|
80
|
+
if (cached !== undefined) return cached
|
|
81
|
+
try {
|
|
82
|
+
const raw = globalThis.localStorage?.getItem(versionSessionKey(workbookId))
|
|
83
|
+
if (raw === null || raw === undefined) return null
|
|
84
|
+
const parsed = JSON.parse(raw) as Partial<VersionSession>
|
|
85
|
+
if (typeof parsed.versionId !== 'string' || parsed.versionId === '') return null
|
|
86
|
+
const session: VersionSession = { versionId: parsed.versionId, stepCount: typeof parsed.stepCount === 'number' && parsed.stepCount > 0 ? parsed.stepCount : 0 }
|
|
87
|
+
versionSessions.set(workbookId, session)
|
|
88
|
+
return session
|
|
89
|
+
} catch {
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Record (or with `null`, forget) the open session for a workbook. */
|
|
95
|
+
export function rememberVersionSession(workbookId: string, session: VersionSession | null): void {
|
|
96
|
+
if (workbookId === '') return
|
|
97
|
+
if (session === null) versionSessions.delete(workbookId)
|
|
98
|
+
else versionSessions.set(workbookId, session)
|
|
99
|
+
try {
|
|
100
|
+
if (session === null) globalThis.localStorage?.removeItem(versionSessionKey(workbookId))
|
|
101
|
+
else globalThis.localStorage?.setItem(versionSessionKey(workbookId), JSON.stringify(session))
|
|
102
|
+
} catch {
|
|
103
|
+
// Ignore storage failures; the in-memory value still survives remounts.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const listeners = new Set<() => void>()
|
|
108
|
+
|
|
109
|
+
function emit(): void {
|
|
110
|
+
for (const listener of [...listeners]) listener()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function subscribe(listener: () => void): () => void {
|
|
114
|
+
listeners.add(listener)
|
|
115
|
+
return () => { listeners.delete(listener) }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Whether the right column is open. */
|
|
119
|
+
export function isSheetOpen(): boolean {
|
|
120
|
+
return sheetOpen
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Open/close the right column. */
|
|
124
|
+
export function setSheetOpen(value: boolean): void {
|
|
125
|
+
sheetOpen = value
|
|
126
|
+
emit()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** React hook: right-column open state. */
|
|
130
|
+
export function useSheetOpen(): boolean {
|
|
131
|
+
return useSyncExternalStore(subscribe, () => sheetOpen)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Whether the Table tab is currently on screen.
|
|
136
|
+
*
|
|
137
|
+
* The workspace is a resident tree that outlives the pane, so its own `open`
|
|
138
|
+
* flag stays true for the life of the page. Anything that must react only to a
|
|
139
|
+
* *visible* table — the window-level Ctrl/Cmd+S shortcut in particular — reads
|
|
140
|
+
* this instead.
|
|
141
|
+
*/
|
|
142
|
+
let sheetVisible = false
|
|
143
|
+
|
|
144
|
+
/** Record whether the pane is showing the Table tab. */
|
|
145
|
+
export function setSheetVisible(value: boolean): void {
|
|
146
|
+
if (sheetVisible === value) return
|
|
147
|
+
sheetVisible = value
|
|
148
|
+
emit()
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Whether the Table tab is on screen. */
|
|
152
|
+
export function isSheetVisible(): boolean {
|
|
153
|
+
return sheetVisible
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** React hook: whether the Table tab is on screen. */
|
|
157
|
+
export function useSheetVisible(): boolean {
|
|
158
|
+
return useSyncExternalStore(subscribe, () => sheetVisible)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Last workbook projection kept outside React. DSH unmounts the right pane when
|
|
163
|
+
* the conversation tab changes, so caching here lets a remount paint the
|
|
164
|
+
* previous content immediately instead of showing an empty panel while the
|
|
165
|
+
* request is in flight. The panel still revalidates against the Host, so edits
|
|
166
|
+
* made in another browser tab are not lost.
|
|
167
|
+
*/
|
|
168
|
+
export interface WorkbookProjection {
|
|
169
|
+
id: string
|
|
170
|
+
name: string
|
|
171
|
+
snapshot: object
|
|
172
|
+
revision: number
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let cachedProjection: WorkbookProjection | null = null
|
|
176
|
+
|
|
177
|
+
export function cachedWorkbookProjection(id: string): WorkbookProjection | null {
|
|
178
|
+
return cachedProjection !== null && cachedProjection.id === id ? cachedProjection : null
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function rememberWorkbookProjection(projection: WorkbookProjection): void {
|
|
182
|
+
cachedProjection = projection
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Drop the cache when the workbook is deleted or fails to load. */
|
|
186
|
+
export function forgetWorkbookProjection(id?: string): void {
|
|
187
|
+
if (id === undefined || cachedProjection?.id === id) cachedProjection = null
|
|
188
|
+
}
|