@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,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operations: their shape, their expansion into Steps, and which Univer
|
|
3
|
+
* commands may become one (docs/issue/0022 P0).
|
|
4
|
+
*
|
|
5
|
+
* The mutation whitelist lives here rather than beside the engine because it is
|
|
6
|
+
* a policy about the *version model* — which canvas commands are durable
|
|
7
|
+
* content and which are derived UI state — not an engine detail. The engine
|
|
8
|
+
* asks; this module decides.
|
|
9
|
+
*/
|
|
10
|
+
import type { WorkbookOperation } from '../types.ts'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Univer emits a small number of mutation commands for derived layout or UI
|
|
14
|
+
* state. They are useful inside the live canvas, but must not become DSH
|
|
15
|
+
* workbook revisions: they are either no-ops, recalculation markers, or state
|
|
16
|
+
* that is reconstructed when the workbook is mounted again. In particular,
|
|
17
|
+
* selection, focus, scrolling, and editor-panel commands are emitted while a
|
|
18
|
+
* user is merely navigating the canvas and must not dirty the workbook.
|
|
19
|
+
*/
|
|
20
|
+
const NON_PERSISTED_MUTATION_IDS = new Set([
|
|
21
|
+
'sheet.mutation.empty',
|
|
22
|
+
'sheet.mutation.mark-dirty-filter-change',
|
|
23
|
+
'sheet.mutation.re-calc-filter',
|
|
24
|
+
'sheet.mutation.set-worksheet-row-auto-height',
|
|
25
|
+
'sheet.mutation.toggle-note-popup',
|
|
26
|
+
'sheet.mutation.update-note-position',
|
|
27
|
+
|
|
28
|
+
// View-only operations emitted by clicks, keyboard navigation, and panels.
|
|
29
|
+
'sheet.operation.set-selections',
|
|
30
|
+
'sheet.operation.set-worksheet-active',
|
|
31
|
+
'sheet.operation.set-scroll',
|
|
32
|
+
'sheet.operation.scroll-to-cell',
|
|
33
|
+
'sheet.operation.scroll-to-range',
|
|
34
|
+
'sheet.operation.set-zoom-ratio',
|
|
35
|
+
'sheet.operation.set-activate-cell-edit',
|
|
36
|
+
'sheet.operation.set-cell-edit-visible',
|
|
37
|
+
'sheet.operation.set-cell-edit-visible-arrow',
|
|
38
|
+
'sheet.operation.set-cell-edit-visible-f2',
|
|
39
|
+
'sheet.operation.set-format-painter',
|
|
40
|
+
'sheet.operation.open.numfmt.panel',
|
|
41
|
+
'sheet.operation.close.numfmt.panel',
|
|
42
|
+
'sheet.command.apply-format-painter',
|
|
43
|
+
'sheet.command.set-infinite-format-painter',
|
|
44
|
+
'sheet.command.set-once-format-painter',
|
|
45
|
+
'sheet.command.move-selection',
|
|
46
|
+
'sheet.command.move-selection-enter-tab',
|
|
47
|
+
'sheet.command.expand-selection',
|
|
48
|
+
'sheet.command.select-range',
|
|
49
|
+
'sheet.command.select-all',
|
|
50
|
+
'sheet.command.set-selection-frozen',
|
|
51
|
+
'sheet.command.scroll-to-cell',
|
|
52
|
+
'sheet.command.scroll-view',
|
|
53
|
+
'sheet.command.scroll-view-reset',
|
|
54
|
+
'sheet.command.set-scroll-relative',
|
|
55
|
+
'sheet.command.change-zoom-ratio',
|
|
56
|
+
'sheet.command.set-zoom-ratio',
|
|
57
|
+
'sheet.command.set-worksheet-activate',
|
|
58
|
+
])
|
|
59
|
+
|
|
60
|
+
const NON_PERSISTED_MUTATION_PREFIXES = [
|
|
61
|
+
// Univer appends editor-specific suffixes to these UI operations.
|
|
62
|
+
'sheet.operation.editor-',
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
// Only these command ids are known to describe durable workbook state. The
|
|
66
|
+
// previous deny-list treated every new Univer command as a save candidate;
|
|
67
|
+
// that made a navigation command capable of setting the dirty bit and caused
|
|
68
|
+
// an unnecessary full snapshot request. Unknown commands are deliberately
|
|
69
|
+
// ignored until they have an explicit persistence contract and a parser.
|
|
70
|
+
const PERSISTED_MUTATION_IDS = new Set([
|
|
71
|
+
'sheet.mutation.set-range-values',
|
|
72
|
+
'sheet.mutation.set-row-data',
|
|
73
|
+
'sheet.mutation.set-col-data',
|
|
74
|
+
'sheet.mutation.set-gridlines-color',
|
|
75
|
+
'sheet.mutation.set-worksheet-name',
|
|
76
|
+
'sheet.mutation.add-worksheet-merge',
|
|
77
|
+
'sheet.mutation.remove-worksheet-merge',
|
|
78
|
+
'sheet.mutation.set-style',
|
|
79
|
+
'sheet.mutation.set-number-format',
|
|
80
|
+
'sheet.command.clear-selection-content',
|
|
81
|
+
'sheet.command.set-bold',
|
|
82
|
+
'sheet.command.set-italic',
|
|
83
|
+
'sheet.command.set-underline',
|
|
84
|
+
'sheet.command.set-strikethrough',
|
|
85
|
+
'sheet.command.set-style',
|
|
86
|
+
'sheet.command.set-range-values',
|
|
87
|
+
'sheet.command.set-number-format',
|
|
88
|
+
'sheet.command.set-font-color',
|
|
89
|
+
'sheet.command.set-font-family',
|
|
90
|
+
'sheet.command.set-font-size',
|
|
91
|
+
'sheet.command.set-font-weight',
|
|
92
|
+
'sheet.command.set-font-style',
|
|
93
|
+
'sheet.command.set-horizontal-alignment',
|
|
94
|
+
'sheet.command.set-vertical-alignment',
|
|
95
|
+
])
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The v3 tier of the Version these operations make up.
|
|
99
|
+
*
|
|
100
|
+
* Explicit when the log carries one. Operations written before v3 carry no
|
|
101
|
+
* `versionKind`, so the tier is derived from the facts that already separated a
|
|
102
|
+
* deliberate Version from editing-session churn — a summary, an Agent actor, a
|
|
103
|
+
* restore. Reading a missing tier as its schema default (`named`) instead is what
|
|
104
|
+
* once made every legacy Version unprunable.
|
|
105
|
+
*
|
|
106
|
+
* It is here because two layers need it and neither may import the other: the
|
|
107
|
+
* projection in `version/grouping.ts` and the retention policy in
|
|
108
|
+
* `store/retention.ts`, which the directory guard keeps below `version/`. Same
|
|
109
|
+
* reason `operationSummary` is here. It was written out twice, in different
|
|
110
|
+
* words that happened to agree.
|
|
111
|
+
*/
|
|
112
|
+
export function versionKindOfOperations(operations: WorkbookOperation[]): 'named' | 'auto' {
|
|
113
|
+
const explicit = operations.map(operation => operation.versionKind).find(kind => kind !== undefined)
|
|
114
|
+
if (explicit !== undefined) return explicit
|
|
115
|
+
const first = operations[0]
|
|
116
|
+
const last = operations.at(-1)
|
|
117
|
+
const deliberate = first?.versionSummary !== undefined || last?.versionSummary !== undefined
|
|
118
|
+
|| first?.actor === 'agent'
|
|
119
|
+
|| first?.restoredFromRevision !== undefined || last?.restoredFromRevision !== undefined
|
|
120
|
+
return deliberate ? 'named' : 'auto'
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* How many proposals a workbook keeps.
|
|
125
|
+
*
|
|
126
|
+
* One constant because the bound was applied in two layers — the manager when it
|
|
127
|
+
* created a proposal, and the store on every write — which is two policies for
|
|
128
|
+
* one array, and the layer that runs less often is the one that gets forgotten.
|
|
129
|
+
* The store owns it now, since it runs on every write.
|
|
130
|
+
*/
|
|
131
|
+
export const PROPOSAL_LOG_LIMIT = 20
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* What `append_rows` says when its columns do not match the sheet's header.
|
|
135
|
+
*
|
|
136
|
+
* It lives here, not in `contract/mutations.ts` where the check is made, because
|
|
137
|
+
* the canvas applier raises the same error and `mutations.ts` imports
|
|
138
|
+
* `node:crypto` — the moment a browser-bundled module reached it, esbuild failed
|
|
139
|
+
* on a built-in Node module. This module is the one both halves already share.
|
|
140
|
+
*/
|
|
141
|
+
export const APPEND_HEADER_MISMATCH = '追加数据的列结构与现有表头不一致;若真实列头不在第 0 行,请用 headerRowIndex 指向它,或改用 workbook_edit 的 update_cells command'
|
|
142
|
+
|
|
143
|
+
export type UniverMutationPersistence = 'ignore' | 'snapshot'
|
|
144
|
+
|
|
145
|
+
/** Classify a Univer command before it is allowed to dirty a workbook. */
|
|
146
|
+
export function classifyUniverMutation(commandId: string): UniverMutationPersistence {
|
|
147
|
+
// Three rules, in order, each written as its own statement.
|
|
148
|
+
//
|
|
149
|
+
// It used to be two, the second of which read
|
|
150
|
+
// `A || B && !PERSISTED.has(id)`. `&&` binds tighter than `||`, so that was
|
|
151
|
+
// `A || (B && !has)`, not `(A || B) && !has` — and since the last line is an
|
|
152
|
+
// allow-list, the `B && !has` half could never change an answer. It is still
|
|
153
|
+
// unreachable today (no `sheet.operation.*` id is in the allow-list), but it
|
|
154
|
+
// would have flipped the moment one was added, in a file whose whole purpose
|
|
155
|
+
// is to say which commands may dirty a workbook.
|
|
156
|
+
if (NON_PERSISTED_MUTATION_IDS.has(commandId)
|
|
157
|
+
|| NON_PERSISTED_MUTATION_PREFIXES.some(prefix => commandId.startsWith(prefix))) return 'ignore'
|
|
158
|
+
// The `sheet.operation.*` namespace is view state — selections, scrolling,
|
|
159
|
+
// panel visibility. It is ignored *whatever* the allow-list later says, which
|
|
160
|
+
// is what the accidental `A ||` did and what is meant here.
|
|
161
|
+
if (commandId.startsWith('sheet.operation.')) return 'ignore'
|
|
162
|
+
// Everything else is durable only with an explicit contract. Unknown commands
|
|
163
|
+
// are ignored on purpose: treating a new Univer command as a save candidate is
|
|
164
|
+
// how a navigation keystroke marks a workbook dirty.
|
|
165
|
+
return PERSISTED_MUTATION_IDS.has(commandId) ? 'snapshot' : 'ignore'
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Return whether a Univer mutation represents durable workbook state. */
|
|
169
|
+
export function shouldPersistUniverMutation(commandId: string): boolean {
|
|
170
|
+
return classifyUniverMutation(commandId) !== 'ignore'
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Expand a persisted operation into the ordered steps of one Version. A
|
|
174
|
+
* normal operation is one step; a batch keeps each child visible for history
|
|
175
|
+
* and replay without introducing a second persistence model. */
|
|
176
|
+
export function workbookOperationSteps(operation: WorkbookOperation): WorkbookOperation[] {
|
|
177
|
+
if (operation.type !== 'batch') return [operation]
|
|
178
|
+
return (operation.payload.operations ?? []).map((child, index) => ({
|
|
179
|
+
opId: `${operation.opId}:${index + 1}`,
|
|
180
|
+
workbookId: operation.workbookId,
|
|
181
|
+
revision: operation.revision,
|
|
182
|
+
parentRevision: operation.parentRevision,
|
|
183
|
+
baseRevision: operation.baseRevision,
|
|
184
|
+
versionId: operation.versionId,
|
|
185
|
+
versionStatus: operation.versionStatus,
|
|
186
|
+
versionSummary: operation.versionSummary,
|
|
187
|
+
versionKind: operation.versionKind,
|
|
188
|
+
stepSummary: operation.stepSummary,
|
|
189
|
+
versionIntent: operation.versionIntent,
|
|
190
|
+
agentRunId: operation.agentRunId,
|
|
191
|
+
lineage: operation.lineage,
|
|
192
|
+
restoredFromRevision: operation.restoredFromRevision,
|
|
193
|
+
actor: operation.actor,
|
|
194
|
+
type: child.type,
|
|
195
|
+
timestamp: operation.timestamp,
|
|
196
|
+
payload: child.payload,
|
|
197
|
+
}))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function operationSummary(operation: WorkbookOperation): string {
|
|
201
|
+
// A derived summary wins: the server knows what a generic operation changed
|
|
202
|
+
// (see `describeSnapshotReplace`). This used to exist twice — here and in
|
|
203
|
+
// `index.ts` — under a comment asking the two copies to stay in step.
|
|
204
|
+
if (operation.stepSummary !== undefined) return operation.stepSummary
|
|
205
|
+
const payload = operation.payload
|
|
206
|
+
switch (operation.type) {
|
|
207
|
+
case 'update_cells': return `更新 ${payload.cells?.length ?? 0} 个单元格`
|
|
208
|
+
case 'append_rows': return `追加 ${payload.rows?.length ?? 0} 行`
|
|
209
|
+
case 'set_hyperlink': return '设置超链接'
|
|
210
|
+
case 'remove_hyperlink': return '移除超链接'
|
|
211
|
+
case 'create_sheet': return `新增 sheet${payload.name === undefined ? '' : `:${payload.name}`}`
|
|
212
|
+
case 'delete_sheet': return '删除 sheet'
|
|
213
|
+
case 'rename_sheet': return `重命名 sheet${payload.name === undefined ? '' : `:${payload.name}`}`
|
|
214
|
+
case 'batch': return `批量操作(${payload.operations?.length ?? 0} 个 step)`
|
|
215
|
+
// Only pre-v3.1 records reach this: their payload was compacted away, so
|
|
216
|
+
// the change they made can no longer be derived. Say so, instead of a
|
|
217
|
+
// neutral label that reads like a normal save.
|
|
218
|
+
case 'snapshot_replace': return '保存整表内容(旧记录)'
|
|
219
|
+
default: return operation.type
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot vocabulary: the workbook snapshot shape, the matrix tools, and the
|
|
3
|
+
* one range reader (docs/issue/0022 P0).
|
|
4
|
+
*
|
|
5
|
+
* The readers here are pure functions of their arguments. The two **accessors**
|
|
6
|
+
* are not, and the distinction is worth stating because the module used to claim
|
|
7
|
+
* otherwise:
|
|
8
|
+
*
|
|
9
|
+
* - `asWorkbookSnapshot` fills in a missing `sheetOrder` by writing it into the
|
|
10
|
+
* object it was handed. That is how a caller's `payload.snapshot` could be
|
|
11
|
+
* modified as a side effect of reading it, which is what made a stored
|
|
12
|
+
* operation's payload accumulate later revisions. `applyWorkbookOperation`
|
|
13
|
+
* now clones before returning, so the write lands on a copy — but this
|
|
14
|
+
* function still writes to its argument, and a caller must not assume
|
|
15
|
+
* otherwise.
|
|
16
|
+
* - `resolveSheet` materializes `cellData` on the sheet it returns. That is
|
|
17
|
+
* load-bearing, not incidental: writers mutate the returned sheet and expect
|
|
18
|
+
* the change to land in the snapshot, so handing back a copy would silently
|
|
19
|
+
* drop every edit.
|
|
20
|
+
*
|
|
21
|
+
* `cloneSnapshot` is the point of the move: it existed twice — once in the model
|
|
22
|
+
* layer and once again inside `client/SheetPanel.tsx` — which is exactly the
|
|
23
|
+
* drift a contract module exists to prevent.
|
|
24
|
+
*/
|
|
25
|
+
import {
|
|
26
|
+
SHEET_MAX_COLUMNS,
|
|
27
|
+
SHEET_MAX_PHYSICAL_ROWS,
|
|
28
|
+
SHEET_MAX_ROWS,
|
|
29
|
+
type SheetColumn,
|
|
30
|
+
type SheetSchema,
|
|
31
|
+
type WorkbookCellStyle,
|
|
32
|
+
type WorkbookCellValue,
|
|
33
|
+
type WorkbookRange,
|
|
34
|
+
} from '../types.ts'
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The snapshot shape. It is declared here rather than in `types.ts` because it
|
|
38
|
+
* is the *input* of these functions and of nothing else on the wire; the
|
|
39
|
+
* tool-facing types stay in `types.ts`.
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* The font family a blank sheet is created with.
|
|
43
|
+
*
|
|
44
|
+
* It lives in the contract because it is part of the stored snapshot's default
|
|
45
|
+
* style: the model, the xlsx reader and the engine all have to agree on it.
|
|
46
|
+
* Before the 0022 refactor it lived in the engine's font module, which made the
|
|
47
|
+
* model layer import *upward* into the engine (docs/issue/0022 §2).
|
|
48
|
+
*/
|
|
49
|
+
export const DEFAULT_SHEET_FONT_FAMILY = 'PingFang SC'
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The grid a new sheet starts with.
|
|
53
|
+
*
|
|
54
|
+
* One constant because *two* layers create sheets — the snapshot mutator and the
|
|
55
|
+
* Univer canvas — and a sheet's declared extent has to match the grid the user
|
|
56
|
+
* is looking at. It was written out as `1000` and `26` in seven places across
|
|
57
|
+
* five files, which is how the two drift apart without anyone editing either
|
|
58
|
+
* number. It is also a *starting* size, not a read limit: writes grow the sheet
|
|
59
|
+
* (`SHEET_MAX_ROWS` is the ceiling), and reads take the range they are given.
|
|
60
|
+
*/
|
|
61
|
+
export const DEFAULT_SHEET_ROWS = 1000
|
|
62
|
+
export const DEFAULT_SHEET_COLUMNS = 26
|
|
63
|
+
|
|
64
|
+
export interface WorkbookSnapshot {
|
|
65
|
+
sheetOrder: string[]
|
|
66
|
+
sheets: Record<string, WorkbookSheet>
|
|
67
|
+
[key: string]: unknown
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface WorkbookSheet {
|
|
71
|
+
id: string
|
|
72
|
+
name: string
|
|
73
|
+
rowCount: number
|
|
74
|
+
columnCount: number
|
|
75
|
+
cellData: Record<string, Record<string, StoredCell>>
|
|
76
|
+
[key: string]: unknown
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface StoredCell {
|
|
80
|
+
v?: unknown
|
|
81
|
+
f?: unknown
|
|
82
|
+
[key: string]: unknown
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Deterministic JSON, so two equal values serialize identically.
|
|
87
|
+
*
|
|
88
|
+
* Object keys are sorted, which `JSON.stringify` does not do: it follows
|
|
89
|
+
* insertion order, so two snapshots holding the same content in a different
|
|
90
|
+
* order would compare unequal. Used for content hashes and for deciding whether
|
|
91
|
+
* a merge actually changed anything.
|
|
92
|
+
*
|
|
93
|
+
* It lived in the model layer *and* in the browser's merge code, byte for byte.
|
|
94
|
+
*/
|
|
95
|
+
export function stableJson(value: unknown): string {
|
|
96
|
+
if (value === undefined) return 'undefined'
|
|
97
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value)
|
|
98
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`
|
|
99
|
+
return `{${Object.entries(value as Record<string, unknown>).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`).join(',')}}`
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Deep copy through JSON; snapshots are plain JSON by construction. */
|
|
103
|
+
export function cloneSnapshot<T>(value: T): T {
|
|
104
|
+
return JSON.parse(JSON.stringify(value)) as T
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Narrow `value` to a snapshot, defaulting `sheetOrder` from `sheets`.
|
|
109
|
+
*
|
|
110
|
+
* **Writes to its argument** when `sheetOrder` is absent (see the module note).
|
|
111
|
+
* Every caller here uses the return value, and `applyWorkbookOperation` clones,
|
|
112
|
+
* but a new caller should not read a snapshot *and* hand the same object
|
|
113
|
+
* somewhere else without knowing that.
|
|
114
|
+
*/
|
|
115
|
+
export function asWorkbookSnapshot(value: unknown): WorkbookSnapshot {
|
|
116
|
+
if (typeof value !== 'object' || value === null) throw new Error('工作簿快照格式无效')
|
|
117
|
+
const root = value as Partial<WorkbookSnapshot>
|
|
118
|
+
if (typeof root.sheets !== 'object' || root.sheets === null) throw new Error('工作簿缺少 sheets')
|
|
119
|
+
if (!Array.isArray(root.sheetOrder)) root.sheetOrder = Object.keys(root.sheets)
|
|
120
|
+
return root as WorkbookSnapshot
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function resolveSheet(root: WorkbookSnapshot, ref: string | undefined): WorkbookSheet {
|
|
124
|
+
const requested = ref?.trim()
|
|
125
|
+
const sheet = requested === undefined || requested.length === 0
|
|
126
|
+
? root.sheets[root.sheetOrder[0] ?? '']
|
|
127
|
+
: root.sheets[requested] ?? Object.values(root.sheets).find(candidate => candidate.name === requested)
|
|
128
|
+
if (sheet === undefined) throw new Error(`sheet 不存在:${requested ?? '(第一个 sheet)'}`)
|
|
129
|
+
sheet.cellData ??= {}
|
|
130
|
+
return sheet
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function validateIndex(value: number, label: string): void {
|
|
134
|
+
if (!Number.isInteger(value) || value < 0) throw new Error(`${label} 必须是大于等于 0 的整数`)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function validateRange(range: WorkbookRange | undefined): asserts range is WorkbookRange {
|
|
138
|
+
if (range === undefined) throw new Error('必须提供 range')
|
|
139
|
+
validateIndex(range.startRow, 'range.startRow')
|
|
140
|
+
validateIndex(range.startColumn, 'range.startColumn')
|
|
141
|
+
validateIndex(range.endRow, 'range.endRow')
|
|
142
|
+
validateIndex(range.endColumn, 'range.endColumn')
|
|
143
|
+
if (range.endRow < range.startRow || range.endColumn < range.startColumn) throw new Error('range 结束位置不能小于开始位置')
|
|
144
|
+
if (range.endRow >= SHEET_MAX_PHYSICAL_ROWS) throw new Error(`range 行号超过上限(最多 ${SHEET_MAX_ROWS} 条数据行 + 表头)`)
|
|
145
|
+
if (range.endColumn >= SHEET_MAX_COLUMNS) throw new Error(`range 列号超过上限(最多 ${SHEET_MAX_COLUMNS} 列)`)
|
|
146
|
+
// No span check: `startRow`/`startColumn` are non-negative (above) and the end
|
|
147
|
+
// bounds are already inside the sheet, so the span cannot exceed it. Two
|
|
148
|
+
// checks that could never fire used to sit here, which is worse than noise —
|
|
149
|
+
// they read as a rule that something enforces.
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* A cell's formula in the one form this repository stores.
|
|
154
|
+
*
|
|
155
|
+
* `=SUM(A1:A2)` and `SUM(A1:A2)` are the same formula — the leading `=` is how a
|
|
156
|
+
* spreadsheet *displays* it, not part of it — and the whitespace around it is not
|
|
157
|
+
* part of it either. An input that reduces to nothing is not a formula at all.
|
|
158
|
+
*
|
|
159
|
+
* Four places wrote cells and each decided this for itself, so they disagreed:
|
|
160
|
+
* `update_cells` with `formula: '=SUM(A1:A2)'` stored `SUM(A1:A2)` in the
|
|
161
|
+
* snapshot and drew `=SUM(A1:A2)` on the canvas; `formula: ''` deleted the cell
|
|
162
|
+
* in the snapshot and left `{v:null,f:''}` on the canvas; `'='` was dropped by
|
|
163
|
+
* the snapshot and kept by the browser's merge. The snapshot and the canvas are
|
|
164
|
+
* two views of one workbook, so they cannot disagree about what is in it.
|
|
165
|
+
*/
|
|
166
|
+
export function normalizeCellFormula(formula: unknown): string | undefined {
|
|
167
|
+
if (typeof formula !== 'string') return undefined
|
|
168
|
+
const normalized = formula.trim().replace(/^=/, '')
|
|
169
|
+
return normalized.length === 0 ? undefined : normalized
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function toCellValue(cell: StoredCell | undefined): WorkbookCellValue | null {
|
|
173
|
+
if (cell === undefined) return null
|
|
174
|
+
if (typeof cell.e === 'string') return cell.e
|
|
175
|
+
if (cell.v === undefined || cell.v === null) return null
|
|
176
|
+
if (typeof cell.v === 'string' || typeof cell.v === 'number' || typeof cell.v === 'boolean') return cell.v
|
|
177
|
+
return String(cell.v)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function cellType(value: WorkbookCellValue | null): string {
|
|
181
|
+
if (value === null) return 'blank'
|
|
182
|
+
if (typeof value === 'number') return 'number'
|
|
183
|
+
if (typeof value === 'boolean') return 'boolean'
|
|
184
|
+
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}(?:T|$)/.test(value)) return 'date'
|
|
185
|
+
return 'string'
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The last row and column that actually carry something, or `-1` when the sheet
|
|
190
|
+
* is empty.
|
|
191
|
+
*
|
|
192
|
+
* A whole-sheet read needs this: the caller asking for "the sheet" must get the
|
|
193
|
+
* sheet, and the only way to know how big that is, is to look. A hard-coded
|
|
194
|
+
* window cannot know, and a window that is too small loses data silently.
|
|
195
|
+
*
|
|
196
|
+
* A cell that holds only a style is skipped — formatting an empty range writes
|
|
197
|
+
* style ids into every cell it covers, and those must not stretch a read.
|
|
198
|
+
*/
|
|
199
|
+
export function usedExtent(snapshot: unknown, sheetRef: string | undefined): { endRow: number; endColumn: number } {
|
|
200
|
+
const sheet = resolveSheet(asWorkbookSnapshot(snapshot), sheetRef)
|
|
201
|
+
let endRow = -1
|
|
202
|
+
let endColumn = -1
|
|
203
|
+
for (const [rowKey, line] of Object.entries(sheet.cellData ?? {})) {
|
|
204
|
+
const row = Number(rowKey)
|
|
205
|
+
if (!Number.isInteger(row) || row < 0) continue
|
|
206
|
+
let rowHasContent = false
|
|
207
|
+
for (const [columnKey, cell] of Object.entries(line ?? {})) {
|
|
208
|
+
const column = Number(columnKey)
|
|
209
|
+
if (!Number.isInteger(column) || column < 0) continue
|
|
210
|
+
const hasValue = toCellValue(cell) !== null
|
|
211
|
+
const hasFormula = typeof cell?.f === 'string' && cell.f !== ''
|
|
212
|
+
if (!hasValue && !hasFormula) continue
|
|
213
|
+
endColumn = Math.max(endColumn, column)
|
|
214
|
+
rowHasContent = true
|
|
215
|
+
}
|
|
216
|
+
if (rowHasContent) endRow = Math.max(endRow, row)
|
|
217
|
+
}
|
|
218
|
+
return { endRow, endColumn }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* A style object out of a snapshot, narrowed to a plain record.
|
|
223
|
+
*
|
|
224
|
+
* Both storage shapes are real and both are read here: the browser's Univer
|
|
225
|
+
* residency keeps the object in the workbook's `styles` map and writes only its
|
|
226
|
+
* id onto the cell, while `format_cells` on the host writes the object inline.
|
|
227
|
+
* A resolver that knew one shape would silently drop the other's formatting.
|
|
228
|
+
*/
|
|
229
|
+
function styleRecord(value: unknown): WorkbookCellStyle {
|
|
230
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return {}
|
|
231
|
+
return { ...(value as WorkbookCellStyle) }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* The style a cell actually paints with: the sheet's default under the cell's
|
|
236
|
+
* own, field by field.
|
|
237
|
+
*
|
|
238
|
+
* The sheet default is load-bearing, not decoration. `DEFAULT_SHEET_FONT_FAMILY`
|
|
239
|
+
* lives in every sheet's `defaultStyle` and is the **only** place a font family
|
|
240
|
+
* is recorded for a cell the user never formatted — a reader that skipped it
|
|
241
|
+
* would report "no font" for most of a sheet, which is how the Sheet Block
|
|
242
|
+
* preview could not show the font the workbook is actually set in.
|
|
243
|
+
*
|
|
244
|
+
* `null` means there is nothing to paint, so the wire carries a compact matrix
|
|
245
|
+
* instead of an object per empty cell.
|
|
246
|
+
*/
|
|
247
|
+
export function effectiveCellStyle(root: WorkbookSnapshot, sheet: WorkbookSheet, cell: StoredCell | undefined): WorkbookCellStyle | null {
|
|
248
|
+
const inline = cell === undefined ? {} : styleRecord(cell.s)
|
|
249
|
+
const named = typeof cell?.s === 'string' ? styleRecord((root.styles as Record<string, unknown> | undefined)?.[cell.s]) : {}
|
|
250
|
+
const merged = { ...styleRecord(sheet.defaultStyle), ...named, ...inline }
|
|
251
|
+
return Object.keys(merged).length === 0 ? null : merged
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function readWorkbookRange(snapshot: unknown, sheetRef: string, range: WorkbookRange, options: { styles?: boolean } = {}): {
|
|
255
|
+
sheetId: string
|
|
256
|
+
sheetName: string
|
|
257
|
+
range: WorkbookRange
|
|
258
|
+
values: Array<Array<WorkbookCellValue | null>>
|
|
259
|
+
formulas: Array<Array<string | null>>
|
|
260
|
+
cells: Array<Array<{ value: WorkbookCellValue | null; formula: string | null; type: string; calculation?: 'cached' | 'unavailable'; error?: string }>>
|
|
261
|
+
/**
|
|
262
|
+
* The effective style of every cell in `values`, when the caller asked for it.
|
|
263
|
+
*
|
|
264
|
+
* Opt-in, because the two readers of this range want different things: the SQL
|
|
265
|
+
* loader materializes values as a relation and would pay for a second matrix
|
|
266
|
+
* it never reads, while the Sheet Block preview exists to show the range the
|
|
267
|
+
* way the sheet draws it.
|
|
268
|
+
*/
|
|
269
|
+
styles?: Array<Array<WorkbookCellStyle | null>>
|
|
270
|
+
} {
|
|
271
|
+
const root = asWorkbookSnapshot(snapshot)
|
|
272
|
+
const sheet = resolveSheet(root, sheetRef)
|
|
273
|
+
validateRange(range)
|
|
274
|
+
const withStyles = options.styles === true
|
|
275
|
+
const values: Array<Array<WorkbookCellValue | null>> = []
|
|
276
|
+
const formulas: Array<Array<string | null>> = []
|
|
277
|
+
const cells: Array<Array<{ value: WorkbookCellValue | null; formula: string | null; type: string; calculation?: 'cached' | 'unavailable'; error?: string }>> = []
|
|
278
|
+
const styles: Array<Array<WorkbookCellStyle | null>> = []
|
|
279
|
+
for (let row = range.startRow; row <= range.endRow; row += 1) {
|
|
280
|
+
const line: Array<WorkbookCellValue | null> = []
|
|
281
|
+
const formulaLine: Array<string | null> = []
|
|
282
|
+
const cellLine: Array<{ value: WorkbookCellValue | null; formula: string | null; type: string; calculation?: 'cached' | 'unavailable'; error?: string }> = []
|
|
283
|
+
const styleLine: Array<WorkbookCellStyle | null> = []
|
|
284
|
+
for (let column = range.startColumn; column <= range.endColumn; column += 1) {
|
|
285
|
+
const cell = sheet.cellData[String(row)]?.[String(column)]
|
|
286
|
+
const value = toCellValue(cell)
|
|
287
|
+
const formula = typeof cell?.f === 'string' ? cell.f : null
|
|
288
|
+
line.push(value)
|
|
289
|
+
formulaLine.push(formula)
|
|
290
|
+
cellLine.push({
|
|
291
|
+
value,
|
|
292
|
+
formula,
|
|
293
|
+
type: formula !== null ? 'formula' : cellType(value),
|
|
294
|
+
...(formula === null ? {} : { calculation: value === null ? 'unavailable' as const : 'cached' as const }),
|
|
295
|
+
...(typeof value === 'string' && value.startsWith('#') ? { error: value } : {}),
|
|
296
|
+
})
|
|
297
|
+
if (withStyles) styleLine.push(effectiveCellStyle(root, sheet, cell))
|
|
298
|
+
}
|
|
299
|
+
values.push(line)
|
|
300
|
+
formulas.push(formulaLine)
|
|
301
|
+
cells.push(cellLine)
|
|
302
|
+
if (withStyles) styles.push(styleLine)
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
sheetId: sheet.id,
|
|
306
|
+
sheetName: sheet.name,
|
|
307
|
+
range,
|
|
308
|
+
values,
|
|
309
|
+
formulas,
|
|
310
|
+
cells,
|
|
311
|
+
...(withStyles ? { styles } : {}),
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Return original sheet row indexes that are duplicates within a rectangular
|
|
317
|
+
* range. This follows Excel's default behavior: compare the selected columns,
|
|
318
|
+
* keep the first occurrence, and leave the header untouched when requested.
|
|
319
|
+
*/
|
|
320
|
+
export function findDuplicateRows(
|
|
321
|
+
values: ReadonlyArray<ReadonlyArray<unknown>>,
|
|
322
|
+
range: WorkbookRange,
|
|
323
|
+
columns?: readonly number[],
|
|
324
|
+
hasHeader = false,
|
|
325
|
+
keep: 'first' | 'last' = 'first',
|
|
326
|
+
): number[] {
|
|
327
|
+
const selectedColumns = columns === undefined || columns.length === 0
|
|
328
|
+
? Array.from({ length: range.endColumn - range.startColumn + 1 }, (_value, index) => range.startColumn + index)
|
|
329
|
+
: [...new Set(columns)]
|
|
330
|
+
for (const column of selectedColumns) {
|
|
331
|
+
if (!Number.isInteger(column) || column < range.startColumn || column > range.endColumn) {
|
|
332
|
+
throw new Error('remove_duplicates 的 columns 必须位于 range 内')
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (keep !== 'first' && keep !== 'last') throw new Error('remove_duplicates.keep 必须是 first 或 last')
|
|
336
|
+
|
|
337
|
+
const duplicateRows: number[] = []
|
|
338
|
+
const seen = new Set<string>()
|
|
339
|
+
const rowIndexes = Array.from({ length: values.length }, (_value, index) => index)
|
|
340
|
+
if (keep === 'last') rowIndexes.reverse()
|
|
341
|
+
for (const offset of rowIndexes) {
|
|
342
|
+
if (hasHeader && offset === 0) continue
|
|
343
|
+
const row = values[offset] ?? []
|
|
344
|
+
const key = JSON.stringify(selectedColumns.map(column => canonicalValue(row[column - range.startColumn])))
|
|
345
|
+
if (seen.has(key)) duplicateRows.push(range.startRow + offset)
|
|
346
|
+
else seen.add(key)
|
|
347
|
+
}
|
|
348
|
+
return duplicateRows.sort((left, right) => left - right)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function canonicalValue(value: unknown): unknown {
|
|
352
|
+
if (value === undefined || value === null) return null
|
|
353
|
+
if (typeof value === 'number' && Number.isNaN(value)) return 'NaN'
|
|
354
|
+
if (typeof value === 'object') return JSON.stringify(value)
|
|
355
|
+
return value
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function isSheetSchema(value: unknown): value is SheetSchema {
|
|
359
|
+
if (typeof value !== 'object' || value === null) return false
|
|
360
|
+
const schema = value as Partial<SheetSchema>
|
|
361
|
+
if (!Number.isInteger(schema.version) || !Array.isArray(schema.columns) || schema.columns.length === 0 || schema.columns.length > SHEET_MAX_COLUMNS
|
|
362
|
+
|| !Number.isInteger(schema.rowCount) || (schema.rowCount as number) < 0 || (schema.rowCount as number) > SHEET_MAX_ROWS
|
|
363
|
+
|| typeof schema.updatedAt !== 'string') return false
|
|
364
|
+
const keys = new Set<string>()
|
|
365
|
+
return schema.columns.every(column => {
|
|
366
|
+
if (typeof column !== 'object' || column === null) return false
|
|
367
|
+
const candidate = column as Partial<SheetColumn>
|
|
368
|
+
if (typeof candidate.key !== 'string' || !/^[A-Za-z0-9_]+$/.test(candidate.key) || keys.has(candidate.key)
|
|
369
|
+
|| typeof candidate.label !== 'string' || candidate.label.length === 0
|
|
370
|
+
|| !['string', 'number', 'boolean', 'date', 'currency'].includes(candidate.type ?? '')) return false
|
|
371
|
+
keys.add(candidate.key)
|
|
372
|
+
return true
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Force a snapshot's `name` field to the workbook's user-facing name.
|
|
377
|
+
* Both the read and the write path need it, which is why it is vocabulary
|
|
378
|
+
* rather than a store helper. */
|
|
379
|
+
|
|
380
|
+
export function normalizeSnapshotName(snapshot: unknown, name: string): unknown {
|
|
381
|
+
if (typeof snapshot !== 'object' || snapshot === null) return snapshot
|
|
382
|
+
return { ...(snapshot as Record<string, unknown>), name }
|
|
383
|
+
}
|