@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,488 @@
|
|
|
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
|
+
* InsertPivotDialog — the SDK chrome's built-in "Insert pivot table" modal
|
|
19
|
+
* (dialog kind `'insert-pivot'`).
|
|
20
|
+
*
|
|
21
|
+
* Facade note / LIMITATION
|
|
22
|
+
* ------------------------
|
|
23
|
+
* Univer's first-class pivot-table feature lives in the commercial
|
|
24
|
+
* `@univerjs/sheets-pivot(-ui)` packages, which are NOT installed in this SDK
|
|
25
|
+
* (the workspace ships only the OSS `@univerjs/*` set — verified: no `pivot`
|
|
26
|
+
* package under `packages/sdk/node_modules/@univerjs/` or `.pnpm/`, and none in
|
|
27
|
+
* `package.json`). So there is no `univerAPI.newPivotTable()` / FRange pivot
|
|
28
|
+
* facade to call.
|
|
29
|
+
*
|
|
30
|
+
* Rather than stub, this dialog implements a REAL, self-contained group-by
|
|
31
|
+
* summary — the practical core of a pivot — entirely on the installed
|
|
32
|
+
* `@univerjs/sheets` facade:
|
|
33
|
+
* - reads the active source range's values via `FRange.getValues()`
|
|
34
|
+
* (f-range.d.ts L386),
|
|
35
|
+
* - groups rows by a chosen "Rows" column and aggregates a chosen "Values"
|
|
36
|
+
* column (Sum · Count · Average · Min · Max),
|
|
37
|
+
* - writes the resulting table to the destination:
|
|
38
|
+
* • New sheet → `FWorkbook.create(name, rows, cols)` (f-workbook.d.ts
|
|
39
|
+
* L178) then `FWorksheet.getRange(...).setValues(...)`.
|
|
40
|
+
* • This location → `FWorksheet.getRange(a1).setValues(...)`
|
|
41
|
+
* (f-worksheet.d.ts L279 / f-range.d.ts L953).
|
|
42
|
+
*
|
|
43
|
+
* When the real pivot packages are added later, swap `buildPivot`/`writePivot`
|
|
44
|
+
* for the `newPivotTable()` builder; the dialog's inputs already map 1:1 to a
|
|
45
|
+
* pivot's Rows / Values / aggregation configuration.
|
|
46
|
+
*
|
|
47
|
+
* Mounted by `<DialogHost>` when `openDialog('insert-pivot')` is called and no
|
|
48
|
+
* host override is registered.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
import { useMemo, useState, type CSSProperties } from 'react';
|
|
52
|
+
import type { DialogComponentProps } from './extensions';
|
|
53
|
+
import type { CasualSheetsAPI } from '../sheets/api';
|
|
54
|
+
import { Dialog } from './Dialog';
|
|
55
|
+
import {
|
|
56
|
+
DIALOG_BTN_PRIMARY_STYLE,
|
|
57
|
+
DIALOG_BTN_SECONDARY_STYLE,
|
|
58
|
+
DIALOG_FIELD_STYLE,
|
|
59
|
+
DIALOG_INPUT_STYLE,
|
|
60
|
+
DIALOG_LABEL_STYLE,
|
|
61
|
+
} from './dialog-styles';
|
|
62
|
+
|
|
63
|
+
/** Aggregation functions the summary can apply to the Values column. */
|
|
64
|
+
type AggFn = 'sum' | 'count' | 'average' | 'min' | 'max';
|
|
65
|
+
|
|
66
|
+
/** Where the resulting pivot table is written. */
|
|
67
|
+
type Destination = 'newSheet' | 'existing';
|
|
68
|
+
|
|
69
|
+
const AGG_OPTIONS: Array<{ value: AggFn; label: string }> = [
|
|
70
|
+
{ value: 'sum', label: 'Sum' },
|
|
71
|
+
{ value: 'count', label: 'Count' },
|
|
72
|
+
{ value: 'average', label: 'Average' },
|
|
73
|
+
{ value: 'min', label: 'Min' },
|
|
74
|
+
{ value: 'max', label: 'Max' },
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
/** A cell value as read off the facade — Univer returns primitives or null. */
|
|
78
|
+
type Cell = string | number | boolean | null;
|
|
79
|
+
|
|
80
|
+
/** The active FRange, or null when there is no selection. */
|
|
81
|
+
function activeRange(api: CasualSheetsAPI) {
|
|
82
|
+
return api.univer.getActiveWorkbook()?.getActiveSheet()?.getActiveRange() ?? null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Loosely-typed FRange view — only the facade methods this dialog calls. */
|
|
86
|
+
interface RangeView {
|
|
87
|
+
getValues: () => Cell[][];
|
|
88
|
+
getA1Notation?: () => string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Loosely-typed FWorksheet view — sheet name + a range handle by A1. */
|
|
92
|
+
interface SheetView {
|
|
93
|
+
getSheetName: () => string;
|
|
94
|
+
getRange: (a1: string) => RangeView & { setValues: (v: Cell[][]) => unknown };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Read the source grid once. Returns [] when there's no usable selection. */
|
|
98
|
+
function readSource(api: CasualSheetsAPI): Cell[][] {
|
|
99
|
+
const range = activeRange(api) as unknown as RangeView | null;
|
|
100
|
+
const grid = range?.getValues?.() ?? [];
|
|
101
|
+
return Array.isArray(grid) ? grid : [];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** A column label for the header dropdowns — "A", "B", or a header-row title. */
|
|
105
|
+
function columnLabel(index: number, headerRow: Cell[] | undefined, useHeader: boolean): string {
|
|
106
|
+
if (useHeader) {
|
|
107
|
+
const raw = headerRow?.[index];
|
|
108
|
+
if (raw != null && String(raw).trim().length > 0) return String(raw);
|
|
109
|
+
}
|
|
110
|
+
// Spreadsheet-style A, B, …, Z, AA — relative to the source's first column.
|
|
111
|
+
let n = index;
|
|
112
|
+
let label = '';
|
|
113
|
+
do {
|
|
114
|
+
label = String.fromCharCode(65 + (n % 26)) + label;
|
|
115
|
+
n = Math.floor(n / 26) - 1;
|
|
116
|
+
} while (n >= 0);
|
|
117
|
+
return `Column ${label}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function toNumber(v: Cell): number | null {
|
|
121
|
+
if (typeof v === 'number') return Number.isFinite(v) ? v : null;
|
|
122
|
+
if (typeof v === 'boolean') return v ? 1 : 0;
|
|
123
|
+
if (typeof v === 'string' && v.trim() !== '') {
|
|
124
|
+
const n = Number(v);
|
|
125
|
+
return Number.isFinite(n) ? n : null;
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Apply the chosen aggregation to a bucket of numeric values. */
|
|
131
|
+
function aggregate(fn: AggFn, values: number[], rowCount: number): number {
|
|
132
|
+
if (fn === 'count') return rowCount;
|
|
133
|
+
if (values.length === 0) return 0;
|
|
134
|
+
switch (fn) {
|
|
135
|
+
case 'sum':
|
|
136
|
+
return values.reduce((a, b) => a + b, 0);
|
|
137
|
+
case 'average':
|
|
138
|
+
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
139
|
+
case 'min':
|
|
140
|
+
return Math.min(...values);
|
|
141
|
+
case 'max':
|
|
142
|
+
return Math.max(...values);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
interface PivotResult {
|
|
147
|
+
/** Full 2D grid (header row + one row per group) ready for setValues. */
|
|
148
|
+
grid: Cell[][];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Build the group-by summary grid from the source values. Groups the data rows
|
|
153
|
+
* by `groupCol`, aggregates `valueCol` with `fn`, and returns a 2-column table:
|
|
154
|
+
* [group label, aggregated value], preceded by a header row.
|
|
155
|
+
*/
|
|
156
|
+
function buildPivot(
|
|
157
|
+
source: Cell[][],
|
|
158
|
+
useHeader: boolean,
|
|
159
|
+
groupCol: number,
|
|
160
|
+
valueCol: number,
|
|
161
|
+
fn: AggFn,
|
|
162
|
+
): PivotResult | null {
|
|
163
|
+
if (source.length === 0) return null;
|
|
164
|
+
const headerRow = useHeader ? source[0] : undefined;
|
|
165
|
+
const dataRows = useHeader ? source.slice(1) : source;
|
|
166
|
+
if (dataRows.length === 0) return null;
|
|
167
|
+
|
|
168
|
+
// Preserve first-seen group order for a stable, predictable output.
|
|
169
|
+
const order: string[] = [];
|
|
170
|
+
const buckets = new Map<string, number[]>();
|
|
171
|
+
const counts = new Map<string, number>();
|
|
172
|
+
|
|
173
|
+
for (const row of dataRows) {
|
|
174
|
+
const rawKey = row[groupCol];
|
|
175
|
+
const key = rawKey == null ? '' : String(rawKey);
|
|
176
|
+
if (!buckets.has(key)) {
|
|
177
|
+
buckets.set(key, []);
|
|
178
|
+
counts.set(key, 0);
|
|
179
|
+
order.push(key);
|
|
180
|
+
}
|
|
181
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
182
|
+
const num = toNumber(row[valueCol]);
|
|
183
|
+
if (num != null) buckets.get(key)!.push(num);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const groupHeader = columnLabel(groupCol, headerRow, useHeader);
|
|
187
|
+
const valueHeader = columnLabel(valueCol, headerRow, useHeader);
|
|
188
|
+
const aggLabel = AGG_OPTIONS.find((o) => o.value === fn)?.label ?? 'Sum';
|
|
189
|
+
|
|
190
|
+
const grid: Cell[][] = [[groupHeader, `${aggLabel} of ${valueHeader}`]];
|
|
191
|
+
for (const key of order) {
|
|
192
|
+
grid.push([key === '' ? '(blank)' : key, aggregate(fn, buckets.get(key)!, counts.get(key)!)]);
|
|
193
|
+
}
|
|
194
|
+
return { grid };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Convert a 0-based (row, col) to A1, e.g. (0,0) -> "A1". */
|
|
198
|
+
function toA1(row: number, col: number): string {
|
|
199
|
+
let n = col;
|
|
200
|
+
let letters = '';
|
|
201
|
+
do {
|
|
202
|
+
letters = String.fromCharCode(65 + (n % 26)) + letters;
|
|
203
|
+
n = Math.floor(n / 26) - 1;
|
|
204
|
+
} while (n >= 0);
|
|
205
|
+
return `${letters}${row + 1}`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
interface DialogState {
|
|
209
|
+
useHeader: boolean;
|
|
210
|
+
groupCol: number;
|
|
211
|
+
valueCol: number;
|
|
212
|
+
aggFn: AggFn;
|
|
213
|
+
destination: Destination;
|
|
214
|
+
/** New-sheet name (destination === 'newSheet'). */
|
|
215
|
+
sheetName: string;
|
|
216
|
+
/** Top-left anchor for an in-place write (destination === 'existing'). */
|
|
217
|
+
anchor: string;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const RANGE_NOTE_STYLE: CSSProperties = {
|
|
221
|
+
fontSize: 12,
|
|
222
|
+
color: 'var(--cs-chrome-muted, #605e5c)',
|
|
223
|
+
marginBottom: 12,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const CHECK_STYLE: CSSProperties = {
|
|
227
|
+
display: 'flex',
|
|
228
|
+
alignItems: 'center',
|
|
229
|
+
gap: 6,
|
|
230
|
+
marginBottom: 12,
|
|
231
|
+
cursor: 'pointer',
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const RADIO_ROW_STYLE: CSSProperties = {
|
|
235
|
+
display: 'flex',
|
|
236
|
+
alignItems: 'center',
|
|
237
|
+
gap: 6,
|
|
238
|
+
marginBottom: 6,
|
|
239
|
+
cursor: 'pointer',
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
export function InsertPivotDialog({ api, onClose }: DialogComponentProps) {
|
|
243
|
+
// Read the source grid + its A1 label once on mount.
|
|
244
|
+
const source = useMemo(() => readSource(api), [api]);
|
|
245
|
+
const rangeLabel = useMemo(() => {
|
|
246
|
+
const r = activeRange(api) as unknown as RangeView | null;
|
|
247
|
+
return r?.getA1Notation?.() ?? null;
|
|
248
|
+
}, [api]);
|
|
249
|
+
|
|
250
|
+
const hasSource = source.length > 0 && (source[0]?.length ?? 0) > 0;
|
|
251
|
+
const colCount = hasSource ? source[0].length : 0;
|
|
252
|
+
|
|
253
|
+
const [state, setState] = useState<DialogState>({
|
|
254
|
+
useHeader: true,
|
|
255
|
+
groupCol: 0,
|
|
256
|
+
valueCol: colCount > 1 ? 1 : 0,
|
|
257
|
+
aggFn: 'sum',
|
|
258
|
+
destination: 'newSheet',
|
|
259
|
+
sheetName: 'Pivot',
|
|
260
|
+
anchor: '',
|
|
261
|
+
});
|
|
262
|
+
const [error, setError] = useState<string | null>(null);
|
|
263
|
+
|
|
264
|
+
const update = <K extends keyof DialogState>(key: K, value: DialogState[K]) => {
|
|
265
|
+
setState((prev) => ({ ...prev, [key]: value }));
|
|
266
|
+
setError(null);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// Column choices, labelled from the header row when "first row is header".
|
|
270
|
+
const columnOptions = useMemo(() => {
|
|
271
|
+
const header = state.useHeader ? source[0] : undefined;
|
|
272
|
+
return Array.from({ length: colCount }, (_, i) => ({
|
|
273
|
+
value: i,
|
|
274
|
+
label: columnLabel(i, header, state.useHeader),
|
|
275
|
+
}));
|
|
276
|
+
}, [source, colCount, state.useHeader]);
|
|
277
|
+
|
|
278
|
+
const apply = () => {
|
|
279
|
+
const pivot = buildPivot(source, state.useHeader, state.groupCol, state.valueCol, state.aggFn);
|
|
280
|
+
if (!pivot) {
|
|
281
|
+
setError('The selection has no data rows to summarize.');
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const wb = api.univer.getActiveWorkbook();
|
|
286
|
+
if (!wb) {
|
|
287
|
+
setError('No active workbook.');
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
if (state.destination === 'newSheet') {
|
|
293
|
+
const name = state.sheetName.trim() || 'Pivot';
|
|
294
|
+
const rows = Math.max(pivot.grid.length + 2, 10);
|
|
295
|
+
// create(name, rows, columns) → FWorksheet (f-workbook.d.ts L178).
|
|
296
|
+
const sheet = (
|
|
297
|
+
wb as unknown as { create: (n: string, r: number, c: number) => SheetView }
|
|
298
|
+
).create(name, rows, 4);
|
|
299
|
+
const target = sheet.getRange(`A1:${toA1(pivot.grid.length - 1, 1)}`);
|
|
300
|
+
target.setValues(pivot.grid);
|
|
301
|
+
} else {
|
|
302
|
+
const anchor = state.anchor.trim().toUpperCase();
|
|
303
|
+
if (!/^[A-Z]+[0-9]+$/.test(anchor)) {
|
|
304
|
+
setError('Enter a valid top-left cell, e.g. E1.');
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const sheet = wb.getActiveSheet() as unknown as SheetView | null;
|
|
308
|
+
if (!sheet) {
|
|
309
|
+
setError('No active sheet to write into.');
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
// Anchor + grid extent -> destination A1 range, then setValues.
|
|
313
|
+
const m = /^([A-Z]+)([0-9]+)$/.exec(anchor)!;
|
|
314
|
+
let col = 0;
|
|
315
|
+
for (const ch of m[1]) col = col * 26 + (ch.charCodeAt(0) - 64);
|
|
316
|
+
col -= 1;
|
|
317
|
+
const startRow = Number(m[2]) - 1;
|
|
318
|
+
const end = toA1(startRow + pivot.grid.length - 1, col + 1);
|
|
319
|
+
sheet.getRange(`${anchor}:${end}`).setValues(pivot.grid);
|
|
320
|
+
}
|
|
321
|
+
} catch (e) {
|
|
322
|
+
setError(e instanceof Error ? e.message : 'Failed to create the pivot table.');
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
onClose();
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
return (
|
|
330
|
+
<Dialog
|
|
331
|
+
title="Insert pivot table"
|
|
332
|
+
onClose={onClose}
|
|
333
|
+
width={460}
|
|
334
|
+
data-testid="cs-insert-pivot-dialog"
|
|
335
|
+
footer={
|
|
336
|
+
<>
|
|
337
|
+
<button type="button" style={DIALOG_BTN_SECONDARY_STYLE} onClick={onClose}>
|
|
338
|
+
Cancel
|
|
339
|
+
</button>
|
|
340
|
+
<button
|
|
341
|
+
type="button"
|
|
342
|
+
style={DIALOG_BTN_PRIMARY_STYLE}
|
|
343
|
+
data-testid="cs-insert-pivot-create"
|
|
344
|
+
disabled={!hasSource}
|
|
345
|
+
onClick={apply}
|
|
346
|
+
>
|
|
347
|
+
Create
|
|
348
|
+
</button>
|
|
349
|
+
</>
|
|
350
|
+
}
|
|
351
|
+
>
|
|
352
|
+
{hasSource ? (
|
|
353
|
+
<div style={RANGE_NOTE_STYLE} data-testid="cs-insert-pivot-source">
|
|
354
|
+
Source data <strong>{rangeLabel ?? 'the current selection'}</strong> ({source.length} rows
|
|
355
|
+
× {colCount} columns)
|
|
356
|
+
</div>
|
|
357
|
+
) : (
|
|
358
|
+
<div style={RANGE_NOTE_STYLE} data-testid="cs-insert-pivot-no-selection">
|
|
359
|
+
Select the source data range first (including its header row), then reopen this dialog.
|
|
360
|
+
</div>
|
|
361
|
+
)}
|
|
362
|
+
|
|
363
|
+
<label style={CHECK_STYLE} data-testid="cs-insert-pivot-header-label">
|
|
364
|
+
<input
|
|
365
|
+
type="checkbox"
|
|
366
|
+
data-testid="cs-insert-pivot-header"
|
|
367
|
+
checked={state.useHeader}
|
|
368
|
+
onChange={(e) => update('useHeader', e.target.checked)}
|
|
369
|
+
disabled={!hasSource}
|
|
370
|
+
/>
|
|
371
|
+
<span>First row is a header</span>
|
|
372
|
+
</label>
|
|
373
|
+
|
|
374
|
+
<label style={DIALOG_FIELD_STYLE}>
|
|
375
|
+
<span style={DIALOG_LABEL_STYLE}>Rows (group by)</span>
|
|
376
|
+
<select
|
|
377
|
+
style={DIALOG_INPUT_STYLE}
|
|
378
|
+
data-testid="cs-insert-pivot-group-col"
|
|
379
|
+
value={state.groupCol}
|
|
380
|
+
disabled={!hasSource}
|
|
381
|
+
onChange={(e) => update('groupCol', Number(e.target.value))}
|
|
382
|
+
>
|
|
383
|
+
{columnOptions.map((opt) => (
|
|
384
|
+
<option key={opt.value} value={opt.value}>
|
|
385
|
+
{opt.label}
|
|
386
|
+
</option>
|
|
387
|
+
))}
|
|
388
|
+
</select>
|
|
389
|
+
</label>
|
|
390
|
+
|
|
391
|
+
<label style={DIALOG_FIELD_STYLE}>
|
|
392
|
+
<span style={DIALOG_LABEL_STYLE}>Values</span>
|
|
393
|
+
<select
|
|
394
|
+
style={DIALOG_INPUT_STYLE}
|
|
395
|
+
data-testid="cs-insert-pivot-value-col"
|
|
396
|
+
value={state.valueCol}
|
|
397
|
+
disabled={!hasSource}
|
|
398
|
+
onChange={(e) => update('valueCol', Number(e.target.value))}
|
|
399
|
+
>
|
|
400
|
+
{columnOptions.map((opt) => (
|
|
401
|
+
<option key={opt.value} value={opt.value}>
|
|
402
|
+
{opt.label}
|
|
403
|
+
</option>
|
|
404
|
+
))}
|
|
405
|
+
</select>
|
|
406
|
+
</label>
|
|
407
|
+
|
|
408
|
+
<label style={DIALOG_FIELD_STYLE}>
|
|
409
|
+
<span style={DIALOG_LABEL_STYLE}>Summarize by</span>
|
|
410
|
+
<select
|
|
411
|
+
style={DIALOG_INPUT_STYLE}
|
|
412
|
+
data-testid="cs-insert-pivot-agg"
|
|
413
|
+
value={state.aggFn}
|
|
414
|
+
disabled={!hasSource}
|
|
415
|
+
onChange={(e) => update('aggFn', e.target.value as AggFn)}
|
|
416
|
+
>
|
|
417
|
+
{AGG_OPTIONS.map((opt) => (
|
|
418
|
+
<option key={opt.value} value={opt.value}>
|
|
419
|
+
{opt.label}
|
|
420
|
+
</option>
|
|
421
|
+
))}
|
|
422
|
+
</select>
|
|
423
|
+
</label>
|
|
424
|
+
|
|
425
|
+
<div style={DIALOG_FIELD_STYLE}>
|
|
426
|
+
<span style={DIALOG_LABEL_STYLE}>Destination</span>
|
|
427
|
+
<label style={RADIO_ROW_STYLE}>
|
|
428
|
+
<input
|
|
429
|
+
type="radio"
|
|
430
|
+
name="cs-insert-pivot-destination"
|
|
431
|
+
data-testid="cs-insert-pivot-dest-new"
|
|
432
|
+
checked={state.destination === 'newSheet'}
|
|
433
|
+
onChange={() => update('destination', 'newSheet')}
|
|
434
|
+
/>
|
|
435
|
+
<span>New sheet</span>
|
|
436
|
+
</label>
|
|
437
|
+
<label style={RADIO_ROW_STYLE}>
|
|
438
|
+
<input
|
|
439
|
+
type="radio"
|
|
440
|
+
name="cs-insert-pivot-destination"
|
|
441
|
+
data-testid="cs-insert-pivot-dest-existing"
|
|
442
|
+
checked={state.destination === 'existing'}
|
|
443
|
+
onChange={() => update('destination', 'existing')}
|
|
444
|
+
/>
|
|
445
|
+
<span>Existing sheet (choose top-left cell)</span>
|
|
446
|
+
</label>
|
|
447
|
+
</div>
|
|
448
|
+
|
|
449
|
+
{state.destination === 'newSheet' ? (
|
|
450
|
+
<label style={DIALOG_FIELD_STYLE}>
|
|
451
|
+
<span style={DIALOG_LABEL_STYLE}>New sheet name</span>
|
|
452
|
+
<input
|
|
453
|
+
style={DIALOG_INPUT_STYLE}
|
|
454
|
+
data-testid="cs-insert-pivot-sheet-name"
|
|
455
|
+
value={state.sheetName}
|
|
456
|
+
placeholder="Pivot"
|
|
457
|
+
onChange={(e) => update('sheetName', e.target.value)}
|
|
458
|
+
/>
|
|
459
|
+
</label>
|
|
460
|
+
) : (
|
|
461
|
+
<label style={DIALOG_FIELD_STYLE}>
|
|
462
|
+
<span style={DIALOG_LABEL_STYLE}>Top-left cell (on the active sheet)</span>
|
|
463
|
+
<input
|
|
464
|
+
style={DIALOG_INPUT_STYLE}
|
|
465
|
+
data-testid="cs-insert-pivot-anchor"
|
|
466
|
+
value={state.anchor}
|
|
467
|
+
placeholder="e.g. E1"
|
|
468
|
+
onChange={(e) => update('anchor', e.target.value)}
|
|
469
|
+
/>
|
|
470
|
+
</label>
|
|
471
|
+
)}
|
|
472
|
+
|
|
473
|
+
{error && (
|
|
474
|
+
<div
|
|
475
|
+
style={{
|
|
476
|
+
...RANGE_NOTE_STYLE,
|
|
477
|
+
color: 'var(--cs-chrome-danger, #b3261e)',
|
|
478
|
+
marginBottom: 0,
|
|
479
|
+
}}
|
|
480
|
+
data-testid="cs-insert-pivot-error"
|
|
481
|
+
role="alert"
|
|
482
|
+
>
|
|
483
|
+
{error}
|
|
484
|
+
</div>
|
|
485
|
+
)}
|
|
486
|
+
</Dialog>
|
|
487
|
+
);
|
|
488
|
+
}
|