@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.
Files changed (267) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -2
  3. package/cordis.patch.yml +7 -0
  4. package/lib/client.js +16766 -0
  5. package/lib/contract-envelope.d.ts +91 -0
  6. package/lib/contract-envelope.js +106 -0
  7. package/lib/contract-layout.d.ts +130 -0
  8. package/lib/contract-layout.js +138 -0
  9. package/lib/contract-refs.d.ts +39 -0
  10. package/lib/contract-refs.js +9 -0
  11. package/lib/contract-version.d.ts +311 -0
  12. package/lib/contract-version.js +270 -0
  13. package/lib/contract-wire.d.ts +147 -0
  14. package/lib/contract-wire.js +99 -0
  15. package/lib/contracts.d.ts +12 -0
  16. package/lib/contracts.js +12 -0
  17. package/lib/types/audit.d.ts +42 -0
  18. package/lib/types/audit.js +39 -0
  19. package/lib/types/client/ProjectImportMenu.d.ts +30 -0
  20. package/lib/types/client/ProjectImportMenu.js +70 -0
  21. package/lib/types/client/SheetPanel.d.ts +33 -0
  22. package/lib/types/client/SheetPanel.js +1766 -0
  23. package/lib/types/client/SheetToggle.d.ts +18 -0
  24. package/lib/types/client/SheetToggle.js +26 -0
  25. package/lib/types/client/index.d.ts +15 -0
  26. package/lib/types/client/index.js +110 -0
  27. package/lib/types/client/persistence/changes.d.ts +15 -0
  28. package/lib/types/client/persistence/changes.js +51 -0
  29. package/lib/types/client/persistence/ids.d.ts +34 -0
  30. package/lib/types/client/persistence/ids.js +48 -0
  31. package/lib/types/client/persistence/merge.d.ts +35 -0
  32. package/lib/types/client/persistence/merge.js +362 -0
  33. package/lib/types/client/store.d.ts +61 -0
  34. package/lib/types/client/store.js +152 -0
  35. package/lib/types/client/surface-context.d.ts +27 -0
  36. package/lib/types/client/surface-context.js +104 -0
  37. package/lib/types/client/surface-control.d.ts +2 -0
  38. package/lib/types/client/surface-control.js +12 -0
  39. package/lib/types/client/transport/workbookApi.d.ts +159 -0
  40. package/lib/types/client/transport/workbookApi.js +246 -0
  41. package/lib/types/client/univer/StandaloneWorkbookPage.d.ts +5 -0
  42. package/lib/types/client/univer/StandaloneWorkbookPage.js +7 -0
  43. package/lib/types/client/univer/WorkbookCanvas.d.ts +40 -0
  44. package/lib/types/client/univer/WorkbookCanvas.js +87 -0
  45. package/lib/types/client/univer/WorkbookView.d.ts +3 -0
  46. package/lib/types/client/univer/WorkbookView.js +2 -0
  47. package/lib/types/client/univer/loader.d.ts +18 -0
  48. package/lib/types/client/univer/loader.js +81 -0
  49. package/lib/types/client/univer/standalone-entry.d.ts +1 -0
  50. package/lib/types/client/univer/standalone-entry.js +19 -0
  51. package/lib/types/client/versions/VersionChrome.d.ts +33 -0
  52. package/lib/types/client/versions/VersionChrome.js +21 -0
  53. package/lib/types/client/versions/inspect.d.ts +70 -0
  54. package/lib/types/client/versions/inspect.js +88 -0
  55. package/lib/types/client/versions/preview-decoration.d.ts +11 -0
  56. package/lib/types/client/versions/preview-decoration.js +54 -0
  57. package/lib/types/client/versions/records.d.ts +19 -0
  58. package/lib/types/client/versions/records.js +70 -0
  59. package/lib/types/client/workspace-host.d.ts +61 -0
  60. package/lib/types/client/workspace-host.js +124 -0
  61. package/lib/types/collaboration-hub.d.ts +31 -0
  62. package/lib/types/collaboration-hub.js +69 -0
  63. package/lib/types/contract/ids.d.ts +24 -0
  64. package/lib/types/contract/ids.js +46 -0
  65. package/lib/types/contract/mutations.d.ts +25 -0
  66. package/lib/types/contract/mutations.js +782 -0
  67. package/lib/types/contract/operations.d.ts +54 -0
  68. package/lib/types/contract/operations.js +203 -0
  69. package/lib/types/contract/snapshots.d.ts +176 -0
  70. package/lib/types/contract/snapshots.js +351 -0
  71. package/lib/types/contract/univer-api-catalog.d.ts +29 -0
  72. package/lib/types/contract/univer-api-catalog.js +264 -0
  73. package/lib/types/errors.d.ts +8 -0
  74. package/lib/types/errors.js +12 -0
  75. package/lib/types/http/assets-routes.d.ts +3 -0
  76. package/lib/types/http/assets-routes.js +169 -0
  77. package/lib/types/http/collaboration.d.ts +10 -0
  78. package/lib/types/http/collaboration.js +154 -0
  79. package/lib/types/http/history.d.ts +15 -0
  80. package/lib/types/http/history.js +134 -0
  81. package/lib/types/http/project-files.d.ts +79 -0
  82. package/lib/types/http/project-files.js +206 -0
  83. package/lib/types/http/project-scope.d.ts +50 -0
  84. package/lib/types/http/project-scope.js +52 -0
  85. package/lib/types/http/routes.d.ts +98 -0
  86. package/lib/types/http/routes.js +323 -0
  87. package/lib/types/http/standalone.d.ts +9 -0
  88. package/lib/types/http/standalone.js +31 -0
  89. package/lib/types/http/trunk.d.ts +10 -0
  90. package/lib/types/http/trunk.js +322 -0
  91. package/lib/types/http/worktree.d.ts +9 -0
  92. package/lib/types/http/worktree.js +145 -0
  93. package/lib/types/index.d.ts +18 -0
  94. package/lib/types/index.js +156 -0
  95. package/lib/types/invariant.d.ts +15 -0
  96. package/lib/types/invariant.js +15 -0
  97. package/lib/types/request-params.d.ts +21 -0
  98. package/lib/types/request-params.js +76 -0
  99. package/lib/types/store/paths.d.ts +68 -0
  100. package/lib/types/store/paths.js +148 -0
  101. package/lib/types/store/project.d.ts +88 -0
  102. package/lib/types/store/project.js +120 -0
  103. package/lib/types/store/read.d.ts +110 -0
  104. package/lib/types/store/read.js +404 -0
  105. package/lib/types/store/retention.d.ts +27 -0
  106. package/lib/types/store/retention.js +111 -0
  107. package/lib/types/store/write.d.ts +22 -0
  108. package/lib/types/store/write.js +210 -0
  109. package/lib/types/tools/arguments.d.ts +40 -0
  110. package/lib/types/tools/arguments.js +130 -0
  111. package/lib/types/tools/cells.d.ts +50 -0
  112. package/lib/types/tools/cells.js +298 -0
  113. package/lib/types/tools/index.d.ts +2 -0
  114. package/lib/types/tools/index.js +250 -0
  115. package/lib/types/tools/render.d.ts +47 -0
  116. package/lib/types/tools/render.js +290 -0
  117. package/lib/types/tools/univer-api.d.ts +18 -0
  118. package/lib/types/tools/univer-api.js +198 -0
  119. package/lib/types/tools/workbook-catalog.d.ts +3 -0
  120. package/lib/types/tools/workbook-catalog.js +83 -0
  121. package/lib/types/tools/workbook-edit.d.ts +192 -0
  122. package/lib/types/tools/workbook-edit.js +912 -0
  123. package/lib/types/tools/workbook-find.d.ts +11 -0
  124. package/lib/types/tools/workbook-find.js +89 -0
  125. package/lib/types/tools/workbook-history.d.ts +2 -0
  126. package/lib/types/tools/workbook-history.js +59 -0
  127. package/lib/types/tools/workbook-read.d.ts +21 -0
  128. package/lib/types/tools/workbook-read.js +204 -0
  129. package/lib/types/types.d.ts +377 -0
  130. package/lib/types/types.js +34 -0
  131. package/lib/types/univer/apply-operation.d.ts +14 -0
  132. package/lib/types/univer/apply-operation.js +398 -0
  133. package/lib/types/univer/chunk.d.ts +68 -0
  134. package/lib/types/univer/chunk.js +16 -0
  135. package/lib/types/univer/decoration.d.ts +34 -0
  136. package/lib/types/univer/decoration.js +88 -0
  137. package/lib/types/univer/diagnostics.d.ts +31 -0
  138. package/lib/types/univer/diagnostics.js +141 -0
  139. package/lib/types/univer/engine.d.ts +33 -0
  140. package/lib/types/univer/engine.js +338 -0
  141. package/lib/types/univer/entry.d.ts +8 -0
  142. package/lib/types/univer/entry.js +11 -0
  143. package/lib/types/univer/facade.d.ts +107 -0
  144. package/lib/types/univer/facade.js +1 -0
  145. package/lib/types/univer/fonts.d.ts +202 -0
  146. package/lib/types/univer/fonts.js +140 -0
  147. package/lib/types/univer/plugins/collaboration.d.ts +23 -0
  148. package/lib/types/univer/plugins/collaboration.js +47 -0
  149. package/lib/types/univer/plugins/export.d.ts +39 -0
  150. package/lib/types/univer/plugins/export.js +246 -0
  151. package/lib/types/univer/plugins/history.d.ts +24 -0
  152. package/lib/types/univer/plugins/history.js +50 -0
  153. package/lib/types/univer/plugins/version.d.ts +13 -0
  154. package/lib/types/univer/plugins/version.js +13 -0
  155. package/lib/types/version/diff.d.ts +36 -0
  156. package/lib/types/version/diff.js +156 -0
  157. package/lib/types/version/grouping.d.ts +7 -0
  158. package/lib/types/version/grouping.js +94 -0
  159. package/lib/types/version/records.d.ts +17 -0
  160. package/lib/types/version/records.js +102 -0
  161. package/lib/types/version/session.d.ts +36 -0
  162. package/lib/types/version/session.js +89 -0
  163. package/lib/types/version/summary.d.ts +24 -0
  164. package/lib/types/version/summary.js +111 -0
  165. package/lib/types/workbook-manager.d.ts +191 -0
  166. package/lib/types/workbook-manager.js +935 -0
  167. package/lib/types/xlsx-export-worker.d.ts +5 -0
  168. package/lib/types/xlsx-export-worker.js +24 -0
  169. package/lib/types/xlsx-export.d.ts +21 -0
  170. package/lib/types/xlsx-export.js +371 -0
  171. package/lib/types/xlsx-import-worker.d.ts +1 -0
  172. package/lib/types/xlsx-import-worker.js +19 -0
  173. package/lib/univer-client.js +690 -0
  174. package/lib/workbook-page.js +23826 -0
  175. package/lib/xlsx-export-worker.js +104 -0
  176. package/lib/xlsx-import-worker.js +46 -0
  177. package/package.json +106 -3
  178. package/src/audit.ts +58 -0
  179. package/src/client/ProjectImportMenu.tsx +100 -0
  180. package/src/client/SheetPanel.module.css +1578 -0
  181. package/src/client/SheetPanel.tsx +1934 -0
  182. package/src/client/SheetToggle.module.css +73 -0
  183. package/src/client/SheetToggle.tsx +65 -0
  184. package/src/client/contract.d.ts +19 -0
  185. package/src/client/index.ts +131 -0
  186. package/src/client/persistence/changes.ts +58 -0
  187. package/src/client/persistence/ids.ts +60 -0
  188. package/src/client/persistence/merge.ts +392 -0
  189. package/src/client/store.ts +188 -0
  190. package/src/client/surface-context.ts +123 -0
  191. package/src/client/surface-control.ts +13 -0
  192. package/src/client/transport/workbookApi.ts +355 -0
  193. package/src/client/univer/SheetView.module.css +110 -0
  194. package/src/client/univer/StandaloneWorkbookPage.module.css +8 -0
  195. package/src/client/univer/StandaloneWorkbookPage.tsx +15 -0
  196. package/src/client/univer/WorkbookCanvas.tsx +128 -0
  197. package/src/client/univer/WorkbookView.module.css +30 -0
  198. package/src/client/univer/WorkbookView.tsx +3 -0
  199. package/src/client/univer/loader.ts +90 -0
  200. package/src/client/univer/react-dom-client.d.ts +10 -0
  201. package/src/client/univer/standalone-entry.tsx +19 -0
  202. package/src/client/univer/standalone.css +12 -0
  203. package/src/client/univer/univer-locales.d.ts +89 -0
  204. package/src/client/univer/univer-modules.d.ts +26 -0
  205. package/src/client/versions/VersionChrome.tsx +114 -0
  206. package/src/client/versions/inspect.ts +138 -0
  207. package/src/client/versions/preview-decoration.ts +54 -0
  208. package/src/client/versions/records.ts +78 -0
  209. package/src/client/workspace-host.ts +147 -0
  210. package/src/collaboration-hub.ts +83 -0
  211. package/src/contract/ids.ts +50 -0
  212. package/src/contract/mutations.ts +791 -0
  213. package/src/contract/operations.ts +221 -0
  214. package/src/contract/snapshots.ts +383 -0
  215. package/src/contract/univer-api-catalog.ts +289 -0
  216. package/src/css-modules.d.ts +6 -0
  217. package/src/errors.ts +25 -0
  218. package/src/http/assets-routes.ts +177 -0
  219. package/src/http/collaboration.ts +144 -0
  220. package/src/http/history.ts +151 -0
  221. package/src/http/project-files.ts +227 -0
  222. package/src/http/project-scope.ts +77 -0
  223. package/src/http/routes.ts +364 -0
  224. package/src/http/standalone.ts +32 -0
  225. package/src/http/trunk.ts +322 -0
  226. package/src/http/worktree.ts +119 -0
  227. package/src/index.ts +165 -0
  228. package/src/invariant.ts +17 -0
  229. package/src/request-params.ts +92 -0
  230. package/src/store/paths.ts +165 -0
  231. package/src/store/project.ts +137 -0
  232. package/src/store/read.ts +454 -0
  233. package/src/store/retention.ts +106 -0
  234. package/src/store/write.ts +230 -0
  235. package/src/tools/arguments.ts +128 -0
  236. package/src/tools/cells.ts +298 -0
  237. package/src/tools/index.ts +274 -0
  238. package/src/tools/render.ts +301 -0
  239. package/src/tools/univer-api.ts +243 -0
  240. package/src/tools/workbook-catalog.ts +94 -0
  241. package/src/tools/workbook-edit.ts +987 -0
  242. package/src/tools/workbook-find.ts +80 -0
  243. package/src/tools/workbook-history.ts +63 -0
  244. package/src/tools/workbook-read.ts +210 -0
  245. package/src/types.ts +432 -0
  246. package/src/univer/apply-operation.ts +369 -0
  247. package/src/univer/chunk.ts +77 -0
  248. package/src/univer/decoration.ts +111 -0
  249. package/src/univer/diagnostics.ts +136 -0
  250. package/src/univer/engine.ts +370 -0
  251. package/src/univer/entry.ts +14 -0
  252. package/src/univer/facade.ts +91 -0
  253. package/src/univer/fonts.ts +148 -0
  254. package/src/univer/overrides.css +33 -0
  255. package/src/univer/plugins/collaboration.ts +47 -0
  256. package/src/univer/plugins/export.ts +236 -0
  257. package/src/univer/plugins/history.ts +49 -0
  258. package/src/univer/plugins/version.ts +13 -0
  259. package/src/version/diff.ts +169 -0
  260. package/src/version/grouping.ts +96 -0
  261. package/src/version/records.ts +113 -0
  262. package/src/version/session.ts +90 -0
  263. package/src/version/summary.ts +117 -0
  264. package/src/workbook-manager.ts +1111 -0
  265. package/src/xlsx-export-worker.ts +34 -0
  266. package/src/xlsx-export.ts +373 -0
  267. 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
+ }