@casualoffice/sheets 0.17.0 → 0.19.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/dist/chrome.cjs +3339 -244
- package/dist/chrome.cjs.map +1 -1
- package/dist/chrome.js +3298 -203
- package/dist/chrome.js.map +1 -1
- package/dist/embed/embed-runtime.js +203 -157
- package/dist/index.cjs +5971 -1572
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +5961 -1548
- package/dist/index.js.map +1 -1
- package/dist/sheets.cjs +4937 -538
- package/dist/sheets.cjs.map +1 -1
- package/dist/sheets.js +4945 -532
- package/dist/sheets.js.map +1 -1
- package/package.json +8 -7
- package/src/charts/ChartContextMenu.tsx +264 -0
- package/src/charts/ChartLayer.tsx +333 -0
- package/src/charts/ChartOverlay.tsx +293 -0
- package/src/charts/ChartsPanel.tsx +211 -0
- package/src/charts/FormatChartDialog.tsx +419 -0
- package/src/charts/InsertChartDialog.tsx +478 -0
- package/src/charts/build-option.ts +476 -0
- package/src/charts/charts-context.tsx +205 -0
- package/src/charts/echarts-init.ts +58 -0
- package/src/charts/hit-test.ts +130 -0
- package/src/charts/insert-chart.ts +106 -0
- package/src/charts/naming.ts +38 -0
- package/src/charts/render-to-png.ts +117 -0
- package/src/charts/resources.ts +108 -0
- package/src/charts/types.ts +239 -0
- package/src/charts/univer-dom.ts +102 -0
- package/src/chrome/CommentsPanel.tsx +427 -0
- package/src/chrome/ConditionalFormattingDialog.tsx +534 -0
- package/src/chrome/CustomSortDialog.tsx +357 -0
- package/src/chrome/DataValidationDialog.tsx +536 -0
- package/src/chrome/DeleteCellsDialog.tsx +183 -0
- package/src/chrome/GoalSeekDialog.tsx +370 -0
- package/src/chrome/HistoryPanel.tsx +319 -0
- package/src/chrome/InsertCellsDialog.tsx +185 -0
- package/src/chrome/InsertChartDialog.tsx +490 -0
- package/src/chrome/InsertFunctionDialog.tsx +493 -0
- package/src/chrome/InsertPivotDialog.tsx +488 -0
- package/src/chrome/InsertSparklineDialog.tsx +344 -0
- package/src/chrome/NameManagerDialog.tsx +378 -0
- package/src/chrome/PanelHost.tsx +55 -0
- package/src/chrome/PanelRail.tsx +90 -0
- package/src/chrome/PasteSpecialDialog.tsx +286 -0
- package/src/chrome/PivotFieldsPanel.tsx +1052 -0
- package/src/chrome/TablesPanel.tsx +301 -0
- package/src/chrome/dialog-context.tsx +24 -0
- package/src/chrome/panel-context.tsx +55 -0
- package/src/chrome/panel-registry.ts +48 -0
- package/src/sheets/CasualSheets.tsx +60 -34
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 Casual Office
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License").
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* History / Activity side panel — a self-contained, in-memory activity feed
|
|
9
|
+
* of user-facing edits in the current session.
|
|
10
|
+
*
|
|
11
|
+
* Ported from the standalone app's HistoryPanel, but DECOUPLED from every
|
|
12
|
+
* app-only coupling: the app version drove off two host-owned sources (a
|
|
13
|
+
* shared Yjs op-log and IndexedDB-backed snapshot version history via
|
|
14
|
+
* `useCollab`/`usePresence`/`local-history`). None of that exists in the SDK,
|
|
15
|
+
* and persistent version history stays HOST-OWNED. So instead this panel
|
|
16
|
+
* subscribes to the live `ICommandService.onCommandExecuted` stream, keeps a
|
|
17
|
+
* bounded in-memory ring (last 100) of user-facing command executions, maps
|
|
18
|
+
* each command id to a friendly label, and renders them newest-first.
|
|
19
|
+
*
|
|
20
|
+
* Ordering is derived from a monotonically incrementing sequence counter
|
|
21
|
+
* (NOT wall-clock time) so nothing calls `Date.now()`/`new Date()` at module
|
|
22
|
+
* load; a per-entry relative-age string is computed lazily at render time from
|
|
23
|
+
* a capture timestamp taken inside the (post-mount) event handler.
|
|
24
|
+
*
|
|
25
|
+
* The app-only `useUI().toggleHistoryPanel` is replaced by the panel host's
|
|
26
|
+
* `onClose`; the Univer facade + services are reached through the `api` prop's
|
|
27
|
+
* `_injector`, matching StatusBar / FormulaBar precedent.
|
|
28
|
+
*/
|
|
29
|
+
import { useEffect, useMemo, useRef, useState, type CSSProperties } from 'react';
|
|
30
|
+
import { CommandType, ICommandService, type ICommandInfo } from '@univerjs/core';
|
|
31
|
+
|
|
32
|
+
import type { PanelComponentProps } from './extensions';
|
|
33
|
+
import { Icon } from './Icon';
|
|
34
|
+
|
|
35
|
+
const RING_CAP = 100;
|
|
36
|
+
|
|
37
|
+
interface ActivityEntry {
|
|
38
|
+
/** Monotonic sequence — sole source of ordering (no wall-clock at module load). */
|
|
39
|
+
seq: number;
|
|
40
|
+
/** Command id that fired (e.g. `sheet.command.set-range-values`). */
|
|
41
|
+
id: string;
|
|
42
|
+
/** Friendly one-line label. */
|
|
43
|
+
label: string;
|
|
44
|
+
/** Icon glyph for the row. */
|
|
45
|
+
icon: string;
|
|
46
|
+
/** Capture time (ms). Taken inside the handler, post-mount — safe. */
|
|
47
|
+
t: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Map a user-facing COMMAND id to a friendly label + icon. Commands (not
|
|
52
|
+
* mutations) are the user-intent layer, so one command == one activity row —
|
|
53
|
+
* no de-duping a single paste into a dozen mutation rows. Anything not mapped
|
|
54
|
+
* still records with a de-namespaced fallback label so the feed stays honest.
|
|
55
|
+
*/
|
|
56
|
+
const COMMAND_LABELS: Record<string, { label: string; icon: string }> = {
|
|
57
|
+
'sheet.command.set-range-values': { label: 'Edited cells', icon: 'edit' },
|
|
58
|
+
'sheet.command.set-range-bold': { label: 'Toggled bold', icon: 'format_bold' },
|
|
59
|
+
'sheet.command.set-range-italic': { label: 'Toggled italic', icon: 'format_italic' },
|
|
60
|
+
'sheet.command.set-range-underline': { label: 'Toggled underline', icon: 'format_underlined' },
|
|
61
|
+
'sheet.command.set-range-stroke-through': {
|
|
62
|
+
label: 'Toggled strikethrough',
|
|
63
|
+
icon: 'strikethrough_s',
|
|
64
|
+
},
|
|
65
|
+
'sheet.command.set-range-font-family': { label: 'Changed font', icon: 'font_download' },
|
|
66
|
+
'sheet.command.set-range-font-size': { label: 'Changed font size', icon: 'format_size' },
|
|
67
|
+
'sheet.command.set-range-text-color': { label: 'Changed text color', icon: 'format_color_text' },
|
|
68
|
+
'sheet.command.set-background-color': { label: 'Changed fill color', icon: 'format_color_fill' },
|
|
69
|
+
'sheet.command.set-range-text-align': { label: 'Changed alignment', icon: 'format_align_left' },
|
|
70
|
+
'sheet.command.set-range-vertical-align': {
|
|
71
|
+
label: 'Changed alignment',
|
|
72
|
+
icon: 'vertical_align_center',
|
|
73
|
+
},
|
|
74
|
+
'sheet.command.set-border-command': { label: 'Changed borders', icon: 'border_all' },
|
|
75
|
+
'sheet.command.clear-selection-content': { label: 'Cleared cell contents', icon: 'backspace' },
|
|
76
|
+
'sheet.command.clear-selection-format': { label: 'Cleared formatting', icon: 'format_clear' },
|
|
77
|
+
'sheet.command.clear-selection-all': { label: 'Cleared cells', icon: 'clear_all' },
|
|
78
|
+
'sheet.command.insert-row': { label: 'Inserted row(s)', icon: 'add_row_above' },
|
|
79
|
+
'sheet.command.insert-row-before': { label: 'Inserted row above', icon: 'add_row_above' },
|
|
80
|
+
'sheet.command.insert-row-after': { label: 'Inserted row below', icon: 'add_row_below' },
|
|
81
|
+
'sheet.command.insert-col': { label: 'Inserted column(s)', icon: 'add_column_left' },
|
|
82
|
+
'sheet.command.insert-col-before': { label: 'Inserted column left', icon: 'add_column_left' },
|
|
83
|
+
'sheet.command.insert-col-after': { label: 'Inserted column right', icon: 'add_column_right' },
|
|
84
|
+
'sheet.command.remove-row': { label: 'Deleted row(s)', icon: 'delete' },
|
|
85
|
+
'sheet.command.remove-col': { label: 'Deleted column(s)', icon: 'delete' },
|
|
86
|
+
'sheet.command.delete-range-move-up': { label: 'Deleted cells', icon: 'delete' },
|
|
87
|
+
'sheet.command.delete-range-move-left': { label: 'Deleted cells', icon: 'delete' },
|
|
88
|
+
'sheet.command.insert-range-move-down': { label: 'Inserted cells', icon: 'add' },
|
|
89
|
+
'sheet.command.insert-range-move-right': { label: 'Inserted cells', icon: 'add' },
|
|
90
|
+
'sheet.command.move-range': { label: 'Moved cells', icon: 'open_with' },
|
|
91
|
+
'sheet.command.move-rows': { label: 'Moved row(s)', icon: 'swap_vert' },
|
|
92
|
+
'sheet.command.move-cols': { label: 'Moved column(s)', icon: 'swap_horiz' },
|
|
93
|
+
'sheet.command.add-worksheet-merge-all': { label: 'Merged cells', icon: 'cell_merge' },
|
|
94
|
+
'sheet.command.remove-worksheet-merge': { label: 'Unmerged cells', icon: 'grid_on' },
|
|
95
|
+
'sheet.command.set-worksheet-row-height': { label: 'Resized row(s)', icon: 'height' },
|
|
96
|
+
'sheet.command.set-worksheet-col-width': { label: 'Resized column(s)', icon: 'width_normal' },
|
|
97
|
+
'sheet.command.set-row-hidden': { label: 'Hid row(s)', icon: 'visibility_off' },
|
|
98
|
+
'sheet.command.set-specific-rows-visible': { label: 'Showed row(s)', icon: 'visibility' },
|
|
99
|
+
'sheet.command.set-col-hidden': { label: 'Hid column(s)', icon: 'visibility_off' },
|
|
100
|
+
'sheet.command.set-specific-cols-visible': { label: 'Showed column(s)', icon: 'visibility' },
|
|
101
|
+
'sheet.command.set-frozen': { label: 'Changed freeze panes', icon: 'ac_unit' },
|
|
102
|
+
'sheet.command.insert-sheet': { label: 'Added a sheet', icon: 'add_box' },
|
|
103
|
+
'sheet.command.remove-sheet': { label: 'Removed a sheet', icon: 'delete' },
|
|
104
|
+
'sheet.command.set-worksheet-name': {
|
|
105
|
+
label: 'Renamed a sheet',
|
|
106
|
+
icon: 'drive_file_rename_outline',
|
|
107
|
+
},
|
|
108
|
+
'sheet.command.set-worksheet-order': { label: 'Reordered sheets', icon: 'reorder' },
|
|
109
|
+
'sheet.command.set-tab-color': { label: 'Changed tab color', icon: 'palette' },
|
|
110
|
+
'sheet.command.paste': { label: 'Pasted', icon: 'content_paste' },
|
|
111
|
+
'sheet.command.copy': { label: 'Copied', icon: 'content_copy' },
|
|
112
|
+
'sheet.command.cut': { label: 'Cut', icon: 'content_cut' },
|
|
113
|
+
'sheet.command.add-table': { label: 'Inserted a table', icon: 'table' },
|
|
114
|
+
'sheet.command.delete-table': { label: 'Deleted a table', icon: 'delete' },
|
|
115
|
+
'sheet.command.numfmt.set.numfmt': { label: 'Changed number format', icon: 'tag' },
|
|
116
|
+
'univer.command.undo': { label: 'Undo', icon: 'undo' },
|
|
117
|
+
'univer.command.redo': { label: 'Redo', icon: 'redo' },
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Which command ids count as "user-facing activity". We only record
|
|
122
|
+
* `CommandType.COMMAND` (intent) executions, and we skip pure selection /
|
|
123
|
+
* navigation / scroll operations even though some of those are dispatched as
|
|
124
|
+
* COMMANDs — they're noise, not edits. The allow-list above IS the primary
|
|
125
|
+
* signal; unmapped COMMANDs still record with a fallback label unless they
|
|
126
|
+
* match an obvious noise prefix.
|
|
127
|
+
*/
|
|
128
|
+
const NOISE_PREFIXES = [
|
|
129
|
+
'sheet.operation.',
|
|
130
|
+
'sheet.command.set-selection',
|
|
131
|
+
'sheet.command.scroll',
|
|
132
|
+
'sheet.command.set-zoom',
|
|
133
|
+
'sheet.command.set-activate-cell-edit',
|
|
134
|
+
'sheet.command.set-cell-edit-visible',
|
|
135
|
+
'sheet.command.set-editor',
|
|
136
|
+
'formula.',
|
|
137
|
+
'univer.command.set-current-locale',
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
function isNoise(id: string): boolean {
|
|
141
|
+
return NOISE_PREFIXES.some((p) => id.startsWith(p));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function labelFor(id: string): { label: string; icon: string } {
|
|
145
|
+
const known = COMMAND_LABELS[id];
|
|
146
|
+
if (known) return known;
|
|
147
|
+
// De-namespace the id into something human-ish.
|
|
148
|
+
const tail = id
|
|
149
|
+
.replace(/^sheet\.command\./, '')
|
|
150
|
+
.replace(/^univer\.command\./, '')
|
|
151
|
+
.replace(/[.-]/g, ' ')
|
|
152
|
+
.trim();
|
|
153
|
+
return { label: tail ? tail.charAt(0).toUpperCase() + tail.slice(1) : 'Change', icon: 'bolt' };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function relativeTime(t: number, now: number): string {
|
|
157
|
+
const delta = now - t;
|
|
158
|
+
if (delta < 5_000) return 'just now';
|
|
159
|
+
if (delta < 60_000) return `${Math.floor(delta / 1000)}s ago`;
|
|
160
|
+
if (delta < 3_600_000) return `${Math.floor(delta / 60_000)}m ago`;
|
|
161
|
+
return `${Math.floor(delta / 3_600_000)}h ago`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const headerStyle: CSSProperties = {
|
|
165
|
+
display: 'flex',
|
|
166
|
+
alignItems: 'center',
|
|
167
|
+
gap: 8,
|
|
168
|
+
padding: '10px 12px',
|
|
169
|
+
borderBottom: '1px solid var(--cs-chrome-border, #edeff3)',
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export function HistoryPanel({ api, onClose }: PanelComponentProps) {
|
|
173
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
174
|
+
const univer = (api as any).univer;
|
|
175
|
+
const [entries, setEntries] = useState<ActivityEntry[]>([]);
|
|
176
|
+
// Ordering counter — monotonic, initialised at 0 (no wall-clock at load).
|
|
177
|
+
const seqRef = useRef(0);
|
|
178
|
+
// Re-render tick so relative times refresh even without new activity.
|
|
179
|
+
const [nowTick, setNowTick] = useState(0);
|
|
180
|
+
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
if (!univer) return;
|
|
183
|
+
const injector = (univer as { _injector?: { get(t: unknown): unknown } })._injector;
|
|
184
|
+
const cmd = injector?.get(ICommandService) as
|
|
185
|
+
| {
|
|
186
|
+
onCommandExecuted: (cb: (info: ICommandInfo, options?: unknown) => void) => {
|
|
187
|
+
dispose: () => void;
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
| undefined;
|
|
191
|
+
if (!cmd) return;
|
|
192
|
+
|
|
193
|
+
const sub = cmd.onCommandExecuted((info) => {
|
|
194
|
+
// Only intent-level COMMANDs; mutations/operations would spam the feed.
|
|
195
|
+
// `type` is optional on ICommandInfo but the dispatcher populates it —
|
|
196
|
+
// treat an explicit non-COMMAND type as a reject, and let undefined
|
|
197
|
+
// fall through to the id-based allow-list below.
|
|
198
|
+
if (info.type !== undefined && info.type !== CommandType.COMMAND) return;
|
|
199
|
+
if (isNoise(info.id)) return;
|
|
200
|
+
// Ignore anything not on the allow-list unless it looks like a real
|
|
201
|
+
// sheet command (keeps the feed to genuine edits, not internal churn).
|
|
202
|
+
if (!COMMAND_LABELS[info.id] && !info.id.startsWith('sheet.command.')) return;
|
|
203
|
+
|
|
204
|
+
const { label, icon } = labelFor(info.id);
|
|
205
|
+
const seq = ++seqRef.current;
|
|
206
|
+
const entry: ActivityEntry = { seq, id: info.id, label, icon, t: Date.now() };
|
|
207
|
+
setEntries((prev) => {
|
|
208
|
+
const next = prev.length >= RING_CAP ? prev.slice(prev.length - RING_CAP + 1) : prev;
|
|
209
|
+
return [...next, entry];
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
return () => sub.dispose();
|
|
213
|
+
}, [univer]);
|
|
214
|
+
|
|
215
|
+
// Refresh relative timestamps once a minute while the panel is open.
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
const h = setInterval(() => setNowTick((n) => n + 1), 60_000);
|
|
218
|
+
return () => clearInterval(h);
|
|
219
|
+
}, []);
|
|
220
|
+
|
|
221
|
+
// Newest-first. Ordering is purely by seq — the render-time `now` only
|
|
222
|
+
// affects the human-readable age string, never the order.
|
|
223
|
+
const sorted = useMemo(() => [...entries].sort((a, b) => b.seq - a.seq), [entries]);
|
|
224
|
+
const now = useMemo(() => Date.now(), [nowTick, entries.length]);
|
|
225
|
+
|
|
226
|
+
const empty = sorted.length === 0;
|
|
227
|
+
|
|
228
|
+
return (
|
|
229
|
+
<div
|
|
230
|
+
data-testid="cs-history-panel"
|
|
231
|
+
style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
|
|
232
|
+
>
|
|
233
|
+
<header style={headerStyle}>
|
|
234
|
+
<Icon name="history" size={18} />
|
|
235
|
+
<span style={{ fontWeight: 600, flex: 1 }}>Activity</span>
|
|
236
|
+
{!empty && (
|
|
237
|
+
<span data-testid="cs-history-count" style={{ opacity: 0.6, fontSize: 12 }}>
|
|
238
|
+
{sorted.length}
|
|
239
|
+
</span>
|
|
240
|
+
)}
|
|
241
|
+
<button
|
|
242
|
+
type="button"
|
|
243
|
+
aria-label="Close activity panel"
|
|
244
|
+
onClick={onClose}
|
|
245
|
+
style={{ border: 'none', background: 'transparent', cursor: 'pointer', color: 'inherit' }}
|
|
246
|
+
>
|
|
247
|
+
<Icon name="close" size={18} />
|
|
248
|
+
</button>
|
|
249
|
+
</header>
|
|
250
|
+
|
|
251
|
+
<div style={{ flex: 1, overflow: 'auto', padding: 12 }}>
|
|
252
|
+
{empty ? (
|
|
253
|
+
<div
|
|
254
|
+
data-testid="cs-history-panel-empty"
|
|
255
|
+
style={{ textAlign: 'center', opacity: 0.75, padding: '24px 8px' }}
|
|
256
|
+
>
|
|
257
|
+
<Icon name="history" size={40} style={{ opacity: 0.4 }} />
|
|
258
|
+
<div style={{ fontWeight: 600, marginTop: 8 }}>No activity yet</div>
|
|
259
|
+
<div style={{ fontSize: 13, marginTop: 4 }}>
|
|
260
|
+
Edits you make this session show up here.
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
) : (
|
|
264
|
+
<ol
|
|
265
|
+
role="list"
|
|
266
|
+
style={{
|
|
267
|
+
listStyle: 'none',
|
|
268
|
+
margin: 0,
|
|
269
|
+
padding: 0,
|
|
270
|
+
display: 'flex',
|
|
271
|
+
flexDirection: 'column',
|
|
272
|
+
gap: 2,
|
|
273
|
+
}}
|
|
274
|
+
>
|
|
275
|
+
{sorted.map((e) => (
|
|
276
|
+
<li
|
|
277
|
+
key={e.seq}
|
|
278
|
+
data-testid="cs-history-row"
|
|
279
|
+
style={{
|
|
280
|
+
display: 'flex',
|
|
281
|
+
alignItems: 'center',
|
|
282
|
+
gap: 10,
|
|
283
|
+
padding: '8px 8px',
|
|
284
|
+
borderRadius: 8,
|
|
285
|
+
}}
|
|
286
|
+
>
|
|
287
|
+
<span
|
|
288
|
+
aria-hidden
|
|
289
|
+
style={{
|
|
290
|
+
display: 'inline-flex',
|
|
291
|
+
alignItems: 'center',
|
|
292
|
+
justifyContent: 'center',
|
|
293
|
+
width: 28,
|
|
294
|
+
height: 28,
|
|
295
|
+
flex: '0 0 auto',
|
|
296
|
+
borderRadius: 6,
|
|
297
|
+
background: 'var(--cs-chrome-subtle-bg, #f4f5f8)',
|
|
298
|
+
opacity: 0.9,
|
|
299
|
+
}}
|
|
300
|
+
>
|
|
301
|
+
<Icon name={e.icon} size={16} />
|
|
302
|
+
</span>
|
|
303
|
+
<span style={{ flex: 1, minWidth: 0, fontSize: 13, fontWeight: 500 }}>
|
|
304
|
+
{e.label}
|
|
305
|
+
</span>
|
|
306
|
+
<time
|
|
307
|
+
dateTime={new Date(e.t).toISOString()}
|
|
308
|
+
style={{ opacity: 0.6, fontSize: 12, whiteSpace: 'nowrap' }}
|
|
309
|
+
>
|
|
310
|
+
{relativeTime(e.t, now)}
|
|
311
|
+
</time>
|
|
312
|
+
</li>
|
|
313
|
+
))}
|
|
314
|
+
</ol>
|
|
315
|
+
)}
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 Casual Office
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* InsertCellsDialog — the SDK chrome's built-in Insert Cells modal.
|
|
19
|
+
*
|
|
20
|
+
* Follows the DataValidationDialog / FormatCellsDialog pattern: reads the active
|
|
21
|
+
* A1 selection off the FUniver facade, offers the four Excel/Sheets shift
|
|
22
|
+
* choices, and applies via real, installed facade methods:
|
|
23
|
+
*
|
|
24
|
+
* - Shift cells right → `FRange.insertCells(Dimension.COLUMNS)`
|
|
25
|
+
* - Shift cells down → `FRange.insertCells(Dimension.ROWS)`
|
|
26
|
+
* - Entire row → `FWorksheet.insertRows(startRow, rowCount)`
|
|
27
|
+
* - Entire column → `FWorksheet.insertColumns(startColumn, colCount)`
|
|
28
|
+
*
|
|
29
|
+
* The shift-direction ↔ Dimension mapping is grounded in the `insertCells`
|
|
30
|
+
* doc-example in `@univerjs/sheets/lib/types/facade/f-range.d.ts` (line 1484):
|
|
31
|
+
* `insertCells(Dimension.COLUMNS)` pushes existing data to the RIGHT, and
|
|
32
|
+
* `insertCells(Dimension.ROWS)` pushes it DOWN. `Dimension` is verified in
|
|
33
|
+
* `@univerjs/core/lib/types/types/enum/dimension.d.ts` (COLUMNS=0, ROWS=1).
|
|
34
|
+
* `FWorksheet.insertRows` / `insertColumns` are verified in
|
|
35
|
+
* `@univerjs/sheets/lib/types/facade/f-worksheet.d.ts` (lines 355 / 679), and
|
|
36
|
+
* the range extents come from `FRange.getRow/getColumn/getLastRow/getLastColumn`
|
|
37
|
+
* (f-range.d.ts lines 115/141/128/154).
|
|
38
|
+
*
|
|
39
|
+
* Mounted by `<DialogHost>` when `openDialog('insert-cells')` is called and no
|
|
40
|
+
* host override is registered.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import { useMemo, useState, type CSSProperties } from 'react';
|
|
44
|
+
import { Dimension } from '@univerjs/core';
|
|
45
|
+
import type { DialogComponentProps } from './extensions';
|
|
46
|
+
import type { CasualSheetsAPI } from '../sheets/api';
|
|
47
|
+
import { Dialog } from './Dialog';
|
|
48
|
+
import { DIALOG_BTN_PRIMARY_STYLE, DIALOG_BTN_SECONDARY_STYLE } from './dialog-styles';
|
|
49
|
+
|
|
50
|
+
/** The four shift choices, matching Excel / Google Sheets "Insert cells". */
|
|
51
|
+
type ShiftChoice = 'right' | 'down' | 'row' | 'column';
|
|
52
|
+
|
|
53
|
+
const SHIFT_OPTIONS: Array<{ value: ShiftChoice; label: string }> = [
|
|
54
|
+
{ value: 'right', label: 'Shift cells right' },
|
|
55
|
+
{ value: 'down', label: 'Shift cells down' },
|
|
56
|
+
{ value: 'row', label: 'Entire row' },
|
|
57
|
+
{ value: 'column', label: 'Entire column' },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
/** The active FRange, or null when there is no selection. */
|
|
61
|
+
function activeRange(api: CasualSheetsAPI) {
|
|
62
|
+
return api.univer.getActiveWorkbook()?.getActiveSheet()?.getActiveRange() ?? null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Insert cells at the active selection with the chosen shift. Returns false when
|
|
67
|
+
* there is no active range (nothing to insert against).
|
|
68
|
+
*/
|
|
69
|
+
function applyInsert(api: CasualSheetsAPI, shift: ShiftChoice): boolean {
|
|
70
|
+
const range = activeRange(api);
|
|
71
|
+
if (!range) return false;
|
|
72
|
+
|
|
73
|
+
switch (shift) {
|
|
74
|
+
case 'right':
|
|
75
|
+
// COLUMNS dimension pushes existing data to the right (f-range.d.ts:1484).
|
|
76
|
+
range.insertCells(Dimension.COLUMNS);
|
|
77
|
+
return true;
|
|
78
|
+
case 'down':
|
|
79
|
+
// ROWS dimension pushes existing data down.
|
|
80
|
+
range.insertCells(Dimension.ROWS);
|
|
81
|
+
return true;
|
|
82
|
+
case 'row': {
|
|
83
|
+
const sheet = api.univer.getActiveWorkbook()?.getActiveSheet();
|
|
84
|
+
if (!sheet) return false;
|
|
85
|
+
const startRow = range.getRow();
|
|
86
|
+
const rowCount = range.getLastRow() - startRow + 1;
|
|
87
|
+
sheet.insertRows(startRow, rowCount);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
case 'column': {
|
|
91
|
+
const sheet = api.univer.getActiveWorkbook()?.getActiveSheet();
|
|
92
|
+
if (!sheet) return false;
|
|
93
|
+
const startColumn = range.getColumn();
|
|
94
|
+
const colCount = range.getLastColumn() - startColumn + 1;
|
|
95
|
+
sheet.insertColumns(startColumn, colCount);
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const RANGE_NOTE_STYLE: CSSProperties = {
|
|
102
|
+
fontSize: 12,
|
|
103
|
+
color: 'var(--cs-chrome-muted, #605e5c)',
|
|
104
|
+
marginBottom: 14,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const RADIO_STYLE: CSSProperties = {
|
|
108
|
+
display: 'flex',
|
|
109
|
+
alignItems: 'center',
|
|
110
|
+
gap: 8,
|
|
111
|
+
marginBottom: 10,
|
|
112
|
+
cursor: 'pointer',
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export function InsertCellsDialog({ api, onClose }: DialogComponentProps) {
|
|
116
|
+
const [shift, setShift] = useState<ShiftChoice>('down');
|
|
117
|
+
|
|
118
|
+
// Read the selection once for the header hint. `getA1Notation` off the live
|
|
119
|
+
// FRange (verified in @univerjs/sheets/facade f-range.d.ts) gives the
|
|
120
|
+
// user-facing A1 label, e.g. "A1:B2".
|
|
121
|
+
const rangeLabel = useMemo(() => {
|
|
122
|
+
const fRange = activeRange(api) as unknown as { getA1Notation?: () => string } | null;
|
|
123
|
+
return fRange?.getA1Notation?.() ?? null;
|
|
124
|
+
}, [api]);
|
|
125
|
+
|
|
126
|
+
const hasSelection = activeRange(api) !== null;
|
|
127
|
+
|
|
128
|
+
const apply = () => {
|
|
129
|
+
if (applyInsert(api, shift)) onClose();
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<Dialog
|
|
134
|
+
title="Insert cells"
|
|
135
|
+
onClose={onClose}
|
|
136
|
+
width={380}
|
|
137
|
+
data-testid="cs-insert-cells-dialog"
|
|
138
|
+
footer={
|
|
139
|
+
<>
|
|
140
|
+
<button type="button" style={DIALOG_BTN_SECONDARY_STYLE} onClick={onClose}>
|
|
141
|
+
Cancel
|
|
142
|
+
</button>
|
|
143
|
+
<button
|
|
144
|
+
type="button"
|
|
145
|
+
style={DIALOG_BTN_PRIMARY_STYLE}
|
|
146
|
+
data-testid="cs-insert-cells-apply"
|
|
147
|
+
disabled={!hasSelection}
|
|
148
|
+
onClick={apply}
|
|
149
|
+
>
|
|
150
|
+
Insert
|
|
151
|
+
</button>
|
|
152
|
+
</>
|
|
153
|
+
}
|
|
154
|
+
>
|
|
155
|
+
{hasSelection ? (
|
|
156
|
+
<div style={RANGE_NOTE_STYLE} data-testid="cs-insert-cells-range">
|
|
157
|
+
Insert at <strong>{rangeLabel ?? 'the current selection'}</strong>
|
|
158
|
+
</div>
|
|
159
|
+
) : (
|
|
160
|
+
<div style={RANGE_NOTE_STYLE} data-testid="cs-insert-cells-no-selection">
|
|
161
|
+
Select one or more cells first, then reopen this dialog.
|
|
162
|
+
</div>
|
|
163
|
+
)}
|
|
164
|
+
|
|
165
|
+
<div role="radiogroup" aria-label="Shift direction">
|
|
166
|
+
{SHIFT_OPTIONS.map((opt) => (
|
|
167
|
+
<label
|
|
168
|
+
key={opt.value}
|
|
169
|
+
style={RADIO_STYLE}
|
|
170
|
+
data-testid={`cs-insert-cells-shift-${opt.value}-label`}
|
|
171
|
+
>
|
|
172
|
+
<input
|
|
173
|
+
type="radio"
|
|
174
|
+
name="cs-insert-cells-shift"
|
|
175
|
+
data-testid={`cs-insert-cells-shift-${opt.value}`}
|
|
176
|
+
checked={shift === opt.value}
|
|
177
|
+
onChange={() => setShift(opt.value)}
|
|
178
|
+
/>
|
|
179
|
+
<span>{opt.label}</span>
|
|
180
|
+
</label>
|
|
181
|
+
))}
|
|
182
|
+
</div>
|
|
183
|
+
</Dialog>
|
|
184
|
+
);
|
|
185
|
+
}
|