@casualoffice/sheets 0.16.0 → 0.18.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.
@@ -54,52 +54,24 @@ import type { CasualSheetsAPI } from '../sheets/api';
54
54
  import { ensurePluginByName } from '../univer';
55
55
  import { Icon } from './Icon';
56
56
  import { ensureChromeFonts } from './fonts';
57
- import { useDialogs, type DialogKind } from './dialog-context';
58
- import type { ChromeExtensions, MenuExtension } from './extensions';
59
-
60
- type MenuId = 'file' | 'edit' | 'view' | 'insert' | 'format' | 'data' | 'help';
57
+ import { useDialogs } from './dialog-context';
58
+ import type { ChromeExtensions } from './extensions';
59
+ import {
60
+ computeVisibleMenus,
61
+ type MenuDef,
62
+ type MenuId,
63
+ type MenuItemDef,
64
+ type RunFn,
65
+ } from './menu-model';
66
+
67
+ export type { MenuDialogKind } from './menu-model';
61
68
 
62
69
  /**
63
- * Dialog kinds the host can choose to render via `onDialogRequest`. These are
64
- * the actions the SDK chrome can't fulfil on its own (no built-in modal). The
65
- * string is passed straight to the host hook; the `context` (when present)
66
- * carries the pre-resolved A1 selection so the host doesn't have to re-read it.
70
+ * The menu data model + pure gating engine live in `./menu-model` (univer-free,
71
+ * so unit-testable). `RunFn` / `MenuItemDef` / `MenuDef` / `MenuId` /
72
+ * `MenuDialogKind` are re-imported above; the `run` handlers that populate
73
+ * `MENUS` are defined inline below against the `@univerjs/core` facade.
67
74
  */
68
- export type MenuDialogKind = DialogKind;
69
-
70
- type RunFn = (api: CasualSheetsAPI) => void;
71
-
72
- type MenuItemDef =
73
- | {
74
- kind: 'item';
75
- id: string;
76
- label: string;
77
- icon?: string;
78
- shortcut?: string;
79
- /** Dispatch a command / facade call directly. */
80
- run?: RunFn;
81
- /** Route through the host's `onDialogRequest`. Omitted if no host hook. */
82
- dialog?: MenuDialogKind;
83
- /** Feature gate — item hidden when `features[feature] === false`. */
84
- feature?: string;
85
- }
86
- | { kind: 'separator'; id: string; feature?: string }
87
- | {
88
- kind: 'submenu';
89
- id: string;
90
- label: string;
91
- icon?: string;
92
- items: MenuItemDef[];
93
- feature?: string;
94
- };
95
-
96
- interface MenuDef {
97
- id: MenuId;
98
- label: string;
99
- /** Feature gate for the whole menu. */
100
- feature?: string;
101
- items: MenuItemDef[];
102
- }
103
75
 
104
76
  /**
105
77
  * Pretty-print a `Ctrl+Shift+X` shortcut for the current platform. The SDK
@@ -559,6 +531,7 @@ const MENUS: MenuDef[] = [
559
531
  label: 'About casual sheets',
560
532
  icon: 'help_outline',
561
533
  dialog: 'about',
534
+ feature: 'branding',
562
535
  },
563
536
  ],
564
537
  },
@@ -1106,6 +1079,12 @@ const MENUS: MenuDef[] = [
1106
1079
  {
1107
1080
  id: 'help',
1108
1081
  label: 'Help',
1082
+ // Whole-menu gate: an embedded host passes `features={{ help: false }}` to
1083
+ // drop the Help menu entirely (no editor-branded surface). Individual
1084
+ // branding links below also carry `feature: 'branding'` so a host that
1085
+ // keeps Help (for keyboard shortcuts) can still hide the About / GitHub
1086
+ // links with `features={{ branding: false }}`.
1087
+ feature: 'help',
1109
1088
  items: [
1110
1089
  {
1111
1090
  kind: 'item',
@@ -1115,14 +1094,22 @@ const MENUS: MenuDef[] = [
1115
1094
  shortcut: 'Ctrl+/',
1116
1095
  dialog: 'keyboard-shortcuts',
1117
1096
  },
1118
- { kind: 'separator', id: 'sep-help' },
1119
- { kind: 'item', id: 'about', label: 'About casual sheets', icon: 'info', dialog: 'about' },
1097
+ { kind: 'separator', id: 'sep-help', feature: 'branding' },
1098
+ {
1099
+ kind: 'item',
1100
+ id: 'about',
1101
+ label: 'About casual sheets',
1102
+ icon: 'info',
1103
+ dialog: 'about',
1104
+ feature: 'branding',
1105
+ },
1120
1106
  {
1121
1107
  kind: 'item',
1122
1108
  id: 'github',
1123
1109
  label: 'View on GitHub',
1124
1110
  icon: 'open_in_new',
1125
1111
  run: () => openExternal('https://github.com/CasualOffice/sheets'),
1112
+ feature: 'branding',
1126
1113
  },
1127
1114
  ],
1128
1115
  },
@@ -1219,93 +1206,6 @@ const SUBMENU_PANEL_STYLE: CSSProperties = {
1219
1206
  zIndex: 1001,
1220
1207
  };
1221
1208
 
1222
- /* ───────────────────────────── filtering ──────────────────────────────── */
1223
-
1224
- /** True when the feature gate (if any) is enabled (default: enabled). */
1225
- function featureOn(feature: string | undefined, features: Record<string, boolean>): boolean {
1226
- if (!feature) return true;
1227
- return features[feature] !== false;
1228
- }
1229
-
1230
- /**
1231
- * Keep an item if its feature is on AND — for a dialog item — the chrome can
1232
- * open it (built-in dialog, host override, or `onDialogRequest`). Dialog items
1233
- * with no way to open are dropped (the SDK never fakes a dialog). Submenus are
1234
- * filtered recursively and dropped when empty.
1235
- */
1236
- function keepItem(
1237
- item: MenuItemDef,
1238
- features: Record<string, boolean>,
1239
- canOpen: (kind: DialogKind) => boolean,
1240
- ): MenuItemDef | null {
1241
- if (!featureOn(item.feature, features)) return null;
1242
- if (item.kind === 'separator') return item;
1243
- if (item.kind === 'submenu') {
1244
- const items = filterItems(item.items, features, canOpen);
1245
- if (items.length === 0) return null;
1246
- return { ...item, items };
1247
- }
1248
- if (item.dialog && !canOpen(item.dialog)) return null;
1249
- return item;
1250
- }
1251
-
1252
- /** Filter a list and collapse leading/trailing/double separators. */
1253
- function filterItems(
1254
- items: MenuItemDef[],
1255
- features: Record<string, boolean>,
1256
- canOpen: (kind: DialogKind) => boolean,
1257
- ): MenuItemDef[] {
1258
- const kept = items
1259
- .map((i) => keepItem(i, features, canOpen))
1260
- .filter((i): i is MenuItemDef => i !== null);
1261
- // Collapse separators: drop leading, trailing, and runs.
1262
- const out: MenuItemDef[] = [];
1263
- for (const item of kept) {
1264
- if (item.kind === 'separator') {
1265
- if (out.length === 0) continue;
1266
- if (out[out.length - 1].kind === 'separator') continue;
1267
- }
1268
- out.push(item);
1269
- }
1270
- while (out.length > 0 && out[out.length - 1].kind === 'separator') out.pop();
1271
- return out;
1272
- }
1273
-
1274
- /* ─────────────────────────── host extensions ──────────────────────────── */
1275
-
1276
- /**
1277
- * Append host menu extensions to their target top-level menu. Each extension
1278
- * becomes a normal `item` (with a leading separator before the first host item
1279
- * in that menu so it's visually grouped). Host items dispatch via `onClick` or
1280
- * route a `dialog` kind through the dialog host, exactly like built-ins.
1281
- */
1282
- function withMenuExtensions(menus: MenuDef[], ext?: MenuExtension[]): MenuDef[] {
1283
- if (!ext || ext.length === 0) return menus;
1284
- const byMenu = new Map<MenuId, MenuExtension[]>();
1285
- for (const e of ext) {
1286
- const list = byMenu.get(e.menu) ?? [];
1287
- list.push(e);
1288
- byMenu.set(e.menu, list);
1289
- }
1290
- return menus.map((menu) => {
1291
- const extras = byMenu.get(menu.id);
1292
- if (!extras || extras.length === 0) return menu;
1293
- const items: MenuItemDef[] = [...menu.items, { kind: 'separator', id: `ext-sep-${menu.id}` }];
1294
- for (const e of extras) {
1295
- items.push({
1296
- kind: 'item',
1297
- id: `ext-${e.id}`,
1298
- label: e.label,
1299
- icon: e.icon,
1300
- shortcut: e.shortcut,
1301
- dialog: e.dialog,
1302
- run: e.onClick ? (api) => e.onClick?.(api) : undefined,
1303
- });
1304
- }
1305
- return { ...menu, items };
1306
- });
1307
- }
1308
-
1309
1209
  /* ───────────────────────────── component ──────────────────────────────── */
1310
1210
 
1311
1211
  export interface MenuBarProps {
@@ -1381,15 +1281,10 @@ export function MenuBar({ api, features = {}, extensions }: MenuBarProps) {
1381
1281
  item.run?.(api);
1382
1282
  };
1383
1283
 
1384
- // Compute the visible menus once per render (feature + dialog-open gating),
1385
- // then append host menu extensions to their target menu.
1386
- const baseMenus = withMenuExtensions(MENUS, extensions?.menu);
1387
- const visibleMenus = baseMenus
1388
- .map((menu) => ({
1389
- ...menu,
1390
- items: filterItems(menu.items, features, dialogs.canOpen),
1391
- }))
1392
- .filter((menu) => featureOn(menu.feature, features) && menu.items.length > 0);
1284
+ // Compute the visible menus once per render (feature + dialog-open gating +
1285
+ // host menu extensions). Extracted to `computeVisibleMenus` so the gating
1286
+ // contract is unit-testable without a DOM.
1287
+ const visibleMenus = computeVisibleMenus(MENUS, features, dialogs.canOpen, extensions?.menu);
1393
1288
 
1394
1289
  const renderItems = (items: MenuItemDef[]): ReactNode =>
1395
1290
  items.map((item) => {
@@ -0,0 +1,378 @@
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
+ * NameManagerDialog — the SDK chrome's built-in Name Manager (defined names).
19
+ *
20
+ * Mirrors the Data Validation / Format Cells exemplars: it reaches the workbook
21
+ * off the FUniver facade and drives the workbook's defined-name API to list,
22
+ * add, edit, and delete named ranges. Grounded in
23
+ * `@univerjs/sheets/lib/types/facade/f-workbook.d.ts`:
24
+ * - `getDefinedNames(): FDefinedName[]` (L571)
25
+ * - `getDefinedName(name): FDefinedName | null` (L559)
26
+ * - `insertDefinedName(name, formulaOrRefString): FWorkbook` (L639)
27
+ * - `deleteDefinedName(name): boolean` (L651)
28
+ * - `updateDefinedNameBuilder(param): void` (L626)
29
+ * and `f-defined-name.d.ts`: `FDefinedName.getName()` (L221) /
30
+ * `getFormulaOrRefString()` (L265) / `toBuilder()` (L385); the builder's
31
+ * `setName` (L44) / `setRef` (L74) / `build()` (L175). The "add from selection"
32
+ * ref comes from `FRange.getA1Notation(true)` (sheet-qualified, e.g.
33
+ * `Sheet1!A1:B2` — `f-range.d.ts` L1220), which is exactly the shape
34
+ * `insertDefinedName`'s `formulaOrRefString` accepts.
35
+ *
36
+ * Univer's defined-name API is unscoped by name (no id needed here): to "edit" a
37
+ * name we rebuild it from `getDefinedName(oldName).toBuilder()`, preserving its
38
+ * identity while updating name + ref, then `updateDefinedNameBuilder`.
39
+ *
40
+ * Mounted by `<DialogHost>` when `openDialog('name-manager')` is called and no
41
+ * host override is registered.
42
+ */
43
+
44
+ import { useCallback, useMemo, useState, type CSSProperties } from 'react';
45
+ import type { DialogComponentProps } from './extensions';
46
+ import type { CasualSheetsAPI } from '../sheets/api';
47
+ import { Dialog } from './Dialog';
48
+ import {
49
+ DIALOG_BTN_PRIMARY_STYLE,
50
+ DIALOG_BTN_SECONDARY_STYLE,
51
+ DIALOG_FIELD_STYLE,
52
+ DIALOG_INPUT_STYLE,
53
+ DIALOG_LABEL_STYLE,
54
+ } from './dialog-styles';
55
+
56
+ /** Minimal shape of the FDefinedNameBuilder chain we use (grounded in f-defined-name.ts). */
57
+ interface FDefinedNameBuilderLike {
58
+ setName(name: string): FDefinedNameBuilderLike;
59
+ setRef(a1Notation: string): FDefinedNameBuilderLike;
60
+ build(): unknown;
61
+ }
62
+
63
+ /** Minimal shape of an FDefinedName we read (grounded in f-defined-name.ts). */
64
+ interface FDefinedNameLike {
65
+ getName(): string;
66
+ getFormulaOrRefString(): string;
67
+ toBuilder(): FDefinedNameBuilderLike;
68
+ }
69
+
70
+ /** Minimal shape of the FWorkbook defined-name surface (grounded in f-workbook.d.ts). */
71
+ interface FWorkbookLike {
72
+ getDefinedNames(): FDefinedNameLike[];
73
+ getDefinedName(name: string): FDefinedNameLike | null;
74
+ insertDefinedName(name: string, formulaOrRefString: string): unknown;
75
+ deleteDefinedName(name: string): boolean;
76
+ updateDefinedNameBuilder(param: unknown): void;
77
+ getActiveSheet(): {
78
+ getActiveRange(): { getA1Notation(withSheet?: boolean): string } | null;
79
+ } | null;
80
+ }
81
+
82
+ interface NameRow {
83
+ name: string;
84
+ refersTo: string;
85
+ }
86
+
87
+ /** Names must start with a letter/underscore and avoid A1-like tokens; keep it
88
+ * loose but block the common mistakes (spaces, leading digit, empty). */
89
+ const NAME_RE = /^[A-Za-z_][A-Za-z0-9_.]*$/;
90
+
91
+ function workbook(api: CasualSheetsAPI): FWorkbookLike | null {
92
+ return (api.univer.getActiveWorkbook() as unknown as FWorkbookLike | null) ?? null;
93
+ }
94
+
95
+ function readRows(api: CasualSheetsAPI): NameRow[] {
96
+ const wb = workbook(api);
97
+ if (!wb) return [];
98
+ return wb.getDefinedNames().map((dn) => ({
99
+ name: dn.getName(),
100
+ refersTo: dn.getFormulaOrRefString(),
101
+ }));
102
+ }
103
+
104
+ /** Sheet-qualified A1 of the current selection, e.g. `Sheet1!A1:B2`, or ''. */
105
+ function selectionRef(api: CasualSheetsAPI): string {
106
+ const range = workbook(api)?.getActiveSheet()?.getActiveRange();
107
+ return range?.getA1Notation(true) ?? '';
108
+ }
109
+
110
+ const ROW_STYLE: CSSProperties = {
111
+ display: 'grid',
112
+ gridTemplateColumns: '1fr 1.3fr auto',
113
+ alignItems: 'center',
114
+ gap: 8,
115
+ padding: '6px 8px',
116
+ borderBottom: '1px solid var(--cs-chrome-border, #edeff3)',
117
+ };
118
+
119
+ const LIST_STYLE: CSSProperties = {
120
+ border: '1px solid var(--cs-chrome-border, #cdd3db)',
121
+ borderRadius: 6,
122
+ maxHeight: 200,
123
+ overflow: 'auto',
124
+ marginBottom: 16,
125
+ };
126
+
127
+ const EMPTY_STYLE: CSSProperties = {
128
+ padding: '16px 8px',
129
+ fontSize: 13,
130
+ color: 'var(--cs-chrome-muted, #605e5c)',
131
+ textAlign: 'center',
132
+ };
133
+
134
+ const REF_CELL_STYLE: CSSProperties = {
135
+ fontSize: 12,
136
+ color: 'var(--cs-chrome-muted, #605e5c)',
137
+ overflow: 'hidden',
138
+ textOverflow: 'ellipsis',
139
+ whiteSpace: 'nowrap',
140
+ };
141
+
142
+ const ROW_ACTIONS_STYLE: CSSProperties = {
143
+ display: 'flex',
144
+ gap: 4,
145
+ };
146
+
147
+ const SMALL_BTN_STYLE: CSSProperties = {
148
+ ...DIALOG_BTN_SECONDARY_STYLE,
149
+ height: 24,
150
+ padding: '0 8px',
151
+ fontSize: 12,
152
+ };
153
+
154
+ const SECTION_TITLE_STYLE: CSSProperties = {
155
+ fontSize: 13,
156
+ fontWeight: 600,
157
+ color: 'var(--cs-chrome-fg, #201f1e)',
158
+ margin: '0 0 8px',
159
+ };
160
+
161
+ const ERROR_STYLE: CSSProperties = {
162
+ fontSize: 12,
163
+ color: 'var(--cs-chrome-danger, #b91c1c)',
164
+ marginBottom: 8,
165
+ };
166
+
167
+ const TWO_COL_STYLE: CSSProperties = {
168
+ display: 'grid',
169
+ gridTemplateColumns: '1fr 1.3fr',
170
+ gap: 8,
171
+ };
172
+
173
+ export function NameManagerDialog({ api, onClose }: DialogComponentProps) {
174
+ const [rows, setRows] = useState<NameRow[]>(() => readRows(api));
175
+ // When editing, this holds the ORIGINAL name of the row being edited (used to
176
+ // locate + update it). null means we're adding a new name.
177
+ const [editingOriginal, setEditingOriginal] = useState<string | null>(null);
178
+ const [nameInput, setNameInput] = useState('');
179
+ const [refInput, setRefInput] = useState('');
180
+ const [error, setError] = useState<string | null>(null);
181
+
182
+ const initialSelRef = useMemo(() => selectionRef(api), [api]);
183
+
184
+ const refresh = useCallback(() => setRows(readRows(api)), [api]);
185
+
186
+ const resetForm = useCallback(() => {
187
+ setEditingOriginal(null);
188
+ setNameInput('');
189
+ setRefInput('');
190
+ setError(null);
191
+ }, []);
192
+
193
+ const startEdit = (row: NameRow) => {
194
+ setEditingOriginal(row.name);
195
+ setNameInput(row.name);
196
+ setRefInput(row.refersTo);
197
+ setError(null);
198
+ };
199
+
200
+ const useSelection = () => {
201
+ const ref = selectionRef(api);
202
+ if (ref) setRefInput(ref);
203
+ };
204
+
205
+ const remove = (row: NameRow) => {
206
+ const wb = workbook(api);
207
+ if (!wb) return;
208
+ wb.deleteDefinedName(row.name);
209
+ if (editingOriginal === row.name) resetForm();
210
+ refresh();
211
+ };
212
+
213
+ const submit = () => {
214
+ const wb = workbook(api);
215
+ if (!wb) {
216
+ setError('No active workbook.');
217
+ return;
218
+ }
219
+ const name = nameInput.trim();
220
+ const ref = refInput.trim();
221
+ if (!name) {
222
+ setError('Enter a name.');
223
+ return;
224
+ }
225
+ if (!NAME_RE.test(name)) {
226
+ setError('Names must start with a letter or underscore and contain no spaces.');
227
+ return;
228
+ }
229
+ if (!ref) {
230
+ setError('Enter a range or formula (e.g. Sheet1!A1:B2).');
231
+ return;
232
+ }
233
+
234
+ // Duplicate-name guard: allow keeping the same name while editing, but block
235
+ // colliding with a different existing name.
236
+ const collides = rows.some(
237
+ (r) => r.name.toLowerCase() === name.toLowerCase() && r.name !== editingOriginal,
238
+ );
239
+ if (collides) {
240
+ setError(`A defined name "${name}" already exists.`);
241
+ return;
242
+ }
243
+
244
+ if (editingOriginal === null) {
245
+ // Add.
246
+ wb.insertDefinedName(name, ref);
247
+ } else {
248
+ // Edit — rebuild from the existing name to preserve identity/scope, then
249
+ // update. If the row vanished (e.g. deleted elsewhere) fall back to insert.
250
+ const existing = wb.getDefinedName(editingOriginal);
251
+ if (existing) {
252
+ const param = existing.toBuilder().setName(name).setRef(ref).build();
253
+ wb.updateDefinedNameBuilder(param);
254
+ } else {
255
+ wb.insertDefinedName(name, ref);
256
+ }
257
+ }
258
+
259
+ refresh();
260
+ resetForm();
261
+ };
262
+
263
+ const isEditing = editingOriginal !== null;
264
+
265
+ return (
266
+ <Dialog
267
+ title="Name manager"
268
+ onClose={onClose}
269
+ width={480}
270
+ data-testid="cs-name-manager-dialog"
271
+ footer={
272
+ <>
273
+ <span style={{ flex: 1 }} />
274
+ <button type="button" style={DIALOG_BTN_SECONDARY_STYLE} onClick={onClose}>
275
+ Close
276
+ </button>
277
+ </>
278
+ }
279
+ >
280
+ <h3 style={SECTION_TITLE_STYLE}>Defined names</h3>
281
+ <div style={LIST_STYLE} data-testid="cs-name-manager-list">
282
+ {rows.length === 0 ? (
283
+ <div style={EMPTY_STYLE} data-testid="cs-name-manager-empty">
284
+ No defined names yet. Add one below.
285
+ </div>
286
+ ) : (
287
+ rows.map((row) => (
288
+ <div key={row.name} style={ROW_STYLE} data-testid="cs-name-manager-row">
289
+ <span style={{ fontWeight: 500 }}>{row.name}</span>
290
+ <span style={REF_CELL_STYLE} title={row.refersTo}>
291
+ {row.refersTo}
292
+ </span>
293
+ <span style={ROW_ACTIONS_STYLE}>
294
+ <button
295
+ type="button"
296
+ style={SMALL_BTN_STYLE}
297
+ data-testid="cs-name-manager-edit"
298
+ onClick={() => startEdit(row)}
299
+ >
300
+ Edit
301
+ </button>
302
+ <button
303
+ type="button"
304
+ style={SMALL_BTN_STYLE}
305
+ data-testid="cs-name-manager-delete"
306
+ onClick={() => remove(row)}
307
+ >
308
+ Delete
309
+ </button>
310
+ </span>
311
+ </div>
312
+ ))
313
+ )}
314
+ </div>
315
+
316
+ <h3 style={SECTION_TITLE_STYLE}>{isEditing ? 'Edit name' : 'Add a name'}</h3>
317
+
318
+ {error && (
319
+ <div style={ERROR_STYLE} data-testid="cs-name-manager-error">
320
+ {error}
321
+ </div>
322
+ )}
323
+
324
+ <div style={TWO_COL_STYLE}>
325
+ <label style={DIALOG_FIELD_STYLE}>
326
+ <span style={DIALOG_LABEL_STYLE}>Name</span>
327
+ <input
328
+ style={DIALOG_INPUT_STYLE}
329
+ data-testid="cs-name-manager-name"
330
+ value={nameInput}
331
+ placeholder="MyRange"
332
+ onChange={(e) => setNameInput(e.target.value)}
333
+ />
334
+ </label>
335
+ <label style={DIALOG_FIELD_STYLE}>
336
+ <span style={DIALOG_LABEL_STYLE}>Refers to</span>
337
+ <input
338
+ style={DIALOG_INPUT_STYLE}
339
+ data-testid="cs-name-manager-ref"
340
+ value={refInput}
341
+ placeholder={initialSelRef || 'Sheet1!A1:B2'}
342
+ onChange={(e) => setRefInput(e.target.value)}
343
+ />
344
+ </label>
345
+ </div>
346
+
347
+ <div style={{ display: 'flex', gap: 8, marginTop: 4 }}>
348
+ <button
349
+ type="button"
350
+ style={DIALOG_BTN_SECONDARY_STYLE}
351
+ data-testid="cs-name-manager-use-selection"
352
+ onClick={useSelection}
353
+ >
354
+ Use selection
355
+ </button>
356
+ <span style={{ flex: 1 }} />
357
+ {isEditing && (
358
+ <button
359
+ type="button"
360
+ style={DIALOG_BTN_SECONDARY_STYLE}
361
+ data-testid="cs-name-manager-cancel-edit"
362
+ onClick={resetForm}
363
+ >
364
+ Cancel
365
+ </button>
366
+ )}
367
+ <button
368
+ type="button"
369
+ style={DIALOG_BTN_PRIMARY_STYLE}
370
+ data-testid="cs-name-manager-submit"
371
+ onClick={submit}
372
+ >
373
+ {isEditing ? 'Update' : 'Add'}
374
+ </button>
375
+ </div>
376
+ </Dialog>
377
+ );
378
+ }