@mshuja288/aksuicore 0.3.0 → 0.4.1
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/CHANGELOG.md +24 -0
- package/dist/aksui.css +508 -17
- package/dist/index.cjs +727 -219
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +49 -9
- package/dist/index.d.ts +49 -9
- package/dist/index.js +727 -219
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/patterns/AppShell.tsx +140 -17
- package/src/patterns/RecordWorkspace.tsx +656 -189
- package/src/patterns/index.ts +2 -1
- package/src/primitives/SideNav.tsx +3 -1
- package/src/primitives/Topbar.tsx +38 -8
- package/src/theme/patterns.css +343 -11
- package/src/theme/primitives.css +11 -1
- package/src/theme/shell.css +154 -5
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
useEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
useSyncExternalStore,
|
|
9
|
+
type MouseEvent,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
} from "react";
|
|
4
12
|
import { createPortal } from "react-dom";
|
|
5
13
|
import { Pagination } from "../primitives/Pagination";
|
|
6
14
|
import { Select, type SelectOption } from "../primitives/Select";
|
|
@@ -13,6 +21,25 @@ function useIsClient() {
|
|
|
13
21
|
);
|
|
14
22
|
}
|
|
15
23
|
|
|
24
|
+
/** Hover tooltips only on fine pointers (desktop). Touch / narrow layouts never get them. */
|
|
25
|
+
function useHoverTooltipsEnabled() {
|
|
26
|
+
return useSyncExternalStore(
|
|
27
|
+
(onStoreChange) => {
|
|
28
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
29
|
+
return () => {};
|
|
30
|
+
}
|
|
31
|
+
const media = window.matchMedia("(hover: hover) and (pointer: fine)");
|
|
32
|
+
media.addEventListener("change", onStoreChange);
|
|
33
|
+
return () => media.removeEventListener("change", onStoreChange);
|
|
34
|
+
},
|
|
35
|
+
() =>
|
|
36
|
+
typeof window !== "undefined" &&
|
|
37
|
+
typeof window.matchMedia === "function" &&
|
|
38
|
+
window.matchMedia("(hover: hover) and (pointer: fine)").matches,
|
|
39
|
+
() => false,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
16
43
|
export type TableColumn<T> = {
|
|
17
44
|
key: string;
|
|
18
45
|
header: string;
|
|
@@ -23,11 +50,21 @@ export type TableColumn<T> = {
|
|
|
23
50
|
/** When set, filter uses an exact-match Select instead of text contains. */
|
|
24
51
|
filterOptions?: SelectOption[];
|
|
25
52
|
filterPlaceholder?: string;
|
|
53
|
+
/** When false, column starts hidden. Default true. */
|
|
54
|
+
defaultVisible?: boolean;
|
|
55
|
+
/** Key sent to export APIs when different from `key` (e.g. PascalCase property name). */
|
|
56
|
+
exportKey?: string;
|
|
26
57
|
};
|
|
27
58
|
|
|
28
59
|
/** Formats supported by the shared table download control (matches akscore TableExport). */
|
|
29
60
|
export type ExportDownloadFormat = "csv" | "xlsx" | "json";
|
|
30
61
|
|
|
62
|
+
/** Column descriptors passed to `onDownload` (visible columns only). */
|
|
63
|
+
export type ExportColumnRef = {
|
|
64
|
+
key: string;
|
|
65
|
+
header: string;
|
|
66
|
+
};
|
|
67
|
+
|
|
31
68
|
const EXPORT_FORMAT_OPTIONS: { format: ExportDownloadFormat; label: string }[] = [
|
|
32
69
|
{ format: "csv", label: "CSV" },
|
|
33
70
|
{ format: "xlsx", label: "Excel" },
|
|
@@ -39,6 +76,41 @@ type SortState = { key: string; direction: SortDirection };
|
|
|
39
76
|
type DetailLine = { label: string; value: string };
|
|
40
77
|
type TooltipState = { x: number; y: number; lines: DetailLine[] };
|
|
41
78
|
|
|
79
|
+
function defaultVisibleKeys<T>(columns: TableColumn<T>[]) {
|
|
80
|
+
return columns.filter((column) => column.defaultVisible !== false).map((column) => column.key);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readStoredColumnKeys(prefsKey: string | undefined, fallback: string[], allowed: Set<string>) {
|
|
84
|
+
if (!prefsKey || typeof window === "undefined") {
|
|
85
|
+
return fallback;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const raw = window.localStorage.getItem(prefsKey);
|
|
89
|
+
if (!raw) {
|
|
90
|
+
return fallback;
|
|
91
|
+
}
|
|
92
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
93
|
+
if (!Array.isArray(parsed)) {
|
|
94
|
+
return fallback;
|
|
95
|
+
}
|
|
96
|
+
const keys = parsed.filter((entry): entry is string => typeof entry === "string" && allowed.has(entry));
|
|
97
|
+
return keys.length > 0 ? keys : fallback;
|
|
98
|
+
} catch {
|
|
99
|
+
return fallback;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function writeStoredColumnKeys(prefsKey: string | undefined, keys: string[]) {
|
|
104
|
+
if (!prefsKey || typeof window === "undefined") {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
window.localStorage.setItem(prefsKey, JSON.stringify(keys));
|
|
109
|
+
} catch {
|
|
110
|
+
/* ignore quota / private mode */
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
42
114
|
function compareColumnValues<T>(column: TableColumn<T>, left: T, right: T) {
|
|
43
115
|
const leftRaw = column.display ? column.display(left) : column.value(left);
|
|
44
116
|
const rightRaw = column.display ? column.display(right) : column.value(right);
|
|
@@ -97,6 +169,14 @@ function ResetIcon() {
|
|
|
97
169
|
);
|
|
98
170
|
}
|
|
99
171
|
|
|
172
|
+
function ColumnsIcon() {
|
|
173
|
+
return (
|
|
174
|
+
<svg viewBox="0 0 20 20" aria-hidden="true">
|
|
175
|
+
<path d="M4 4.5h3.5v11H4zM8.25 4.5h3.5v11h-3.5zM12.5 4.5H16v11h-3.5z" fill="none" stroke="currentColor" strokeWidth="1.5" />
|
|
176
|
+
</svg>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
100
180
|
function ChevronIcon({ open }: { open: boolean }) {
|
|
101
181
|
return (
|
|
102
182
|
<svg className={open ? "side-rail-chevron open" : "side-rail-chevron"} viewBox="0 0 20 20" aria-hidden="true">
|
|
@@ -124,6 +204,31 @@ function DownloadIcon() {
|
|
|
124
204
|
);
|
|
125
205
|
}
|
|
126
206
|
|
|
207
|
+
function MoreIcon() {
|
|
208
|
+
return (
|
|
209
|
+
<svg viewBox="0 0 20 20" aria-hidden="true">
|
|
210
|
+
<circle cx="4.5" cy="10" r="1.5" fill="currentColor" />
|
|
211
|
+
<circle cx="10" cy="10" r="1.5" fill="currentColor" />
|
|
212
|
+
<circle cx="15.5" cy="10" r="1.5" fill="currentColor" />
|
|
213
|
+
</svg>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function ToolsIcon() {
|
|
218
|
+
return (
|
|
219
|
+
<svg viewBox="0 0 20 20" aria-hidden="true">
|
|
220
|
+
<path
|
|
221
|
+
d="M11.5 3.5h5v5M16.2 3.8 10 10M8.2 5.2H3.8v11h11V12"
|
|
222
|
+
fill="none"
|
|
223
|
+
stroke="currentColor"
|
|
224
|
+
strokeWidth="1.6"
|
|
225
|
+
strokeLinecap="round"
|
|
226
|
+
strokeLinejoin="round"
|
|
227
|
+
/>
|
|
228
|
+
</svg>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
127
232
|
function DetailList({ lines }: { lines: DetailLine[] }) {
|
|
128
233
|
return (
|
|
129
234
|
<dl className="record-detail-list">
|
|
@@ -154,6 +259,11 @@ export function RecordWorkspace<T>({
|
|
|
154
259
|
onDownload,
|
|
155
260
|
canDownload = true,
|
|
156
261
|
downloadBusy = false,
|
|
262
|
+
showColumnPicker = true,
|
|
263
|
+
columnPrefsKey,
|
|
264
|
+
visibleColumnKeys,
|
|
265
|
+
defaultVisibleColumnKeys,
|
|
266
|
+
onVisibleColumnKeysChange,
|
|
157
267
|
toolbar,
|
|
158
268
|
form,
|
|
159
269
|
}: {
|
|
@@ -170,30 +280,111 @@ export function RecordWorkspace<T>({
|
|
|
170
280
|
onDelete?: (item: T) => void;
|
|
171
281
|
canUpdate?: (item: T) => boolean;
|
|
172
282
|
canDelete?: (item: T) => boolean;
|
|
173
|
-
/**
|
|
174
|
-
|
|
283
|
+
/**
|
|
284
|
+
* When set, shows CSV / Excel / JSON download controls.
|
|
285
|
+
* Second argument lists currently visible columns (exportKey ?? key).
|
|
286
|
+
*/
|
|
287
|
+
onDownload?: (format: ExportDownloadFormat, columns: ExportColumnRef[]) => void;
|
|
175
288
|
canDownload?: boolean;
|
|
176
289
|
downloadBusy?: boolean;
|
|
290
|
+
/** Column checklist in the side rail. Default true. */
|
|
291
|
+
showColumnPicker?: boolean;
|
|
292
|
+
/** Persist visible column keys in localStorage under this key. */
|
|
293
|
+
columnPrefsKey?: string;
|
|
294
|
+
/** Controlled visible column keys. */
|
|
295
|
+
visibleColumnKeys?: string[];
|
|
296
|
+
/** Uncontrolled initial visible keys (defaults to columns with defaultVisible !== false). */
|
|
297
|
+
defaultVisibleColumnKeys?: string[];
|
|
298
|
+
onVisibleColumnKeysChange?: (keys: string[]) => void;
|
|
177
299
|
toolbar?: ReactNode;
|
|
178
300
|
/** Slot for Add/Update/View dialogs owned by the host app. */
|
|
179
301
|
form?: ReactNode;
|
|
180
302
|
}) {
|
|
181
|
-
const
|
|
303
|
+
const allowedKeys = useMemo(() => new Set(columns.map((column) => column.key)), [columns]);
|
|
304
|
+
const fallbackKeys = useMemo(
|
|
305
|
+
() => defaultVisibleColumnKeys ?? defaultVisibleKeys(columns),
|
|
306
|
+
[columns, defaultVisibleColumnKeys],
|
|
307
|
+
);
|
|
308
|
+
const isControlled = visibleColumnKeys !== undefined;
|
|
309
|
+
const [internalVisibleKeys, setInternalVisibleKeys] = useState<string[]>(() =>
|
|
310
|
+
readStoredColumnKeys(columnPrefsKey, fallbackKeys, allowedKeys),
|
|
311
|
+
);
|
|
182
312
|
const [filters, setFilters] = useState<Record<string, string>>({});
|
|
183
313
|
const [sort, setSort] = useState<SortState | null>(null);
|
|
184
314
|
const [sideOpen, setSideOpen] = useState(false);
|
|
315
|
+
const [toolsDrawerOpen, setToolsDrawerOpen] = useState(false);
|
|
185
316
|
const [downloadOpen, setDownloadOpen] = useState(false);
|
|
317
|
+
const [columnsOpen, setColumnsOpen] = useState(false);
|
|
186
318
|
const [previewEnabled, setPreviewEnabled] = useState(false);
|
|
187
319
|
const [previewKey, setPreviewKey] = useState<string | number | null>(null);
|
|
188
320
|
const [tooltip, setTooltip] = useState<TooltipState | null>(null);
|
|
189
321
|
const [pageSize, setPageSize] = useState("15");
|
|
190
322
|
const [page, setPage] = useState(1);
|
|
323
|
+
const [actionsMenu, setActionsMenu] = useState<{
|
|
324
|
+
key: string | number;
|
|
325
|
+
item: T;
|
|
326
|
+
x: number;
|
|
327
|
+
y: number;
|
|
328
|
+
} | null>(null);
|
|
329
|
+
const actionsMenuRef = useRef<HTMLDivElement | null>(null);
|
|
191
330
|
const isClient = useIsClient();
|
|
331
|
+
const hoverTooltipsEnabled = useHoverTooltipsEnabled();
|
|
332
|
+
|
|
333
|
+
useEffect(() => {
|
|
334
|
+
if (isControlled) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
setInternalVisibleKeys(readStoredColumnKeys(columnPrefsKey, fallbackKeys, allowedKeys));
|
|
338
|
+
}, [allowedKeys, columnPrefsKey, fallbackKeys, isControlled]);
|
|
339
|
+
|
|
340
|
+
const resolvedVisibleKeys = useMemo(() => {
|
|
341
|
+
const source = isControlled ? visibleColumnKeys! : internalVisibleKeys;
|
|
342
|
+
const filtered = source.filter((key) => allowedKeys.has(key));
|
|
343
|
+
return filtered.length > 0 ? filtered : fallbackKeys.filter((key) => allowedKeys.has(key));
|
|
344
|
+
}, [allowedKeys, fallbackKeys, internalVisibleKeys, isControlled, visibleColumnKeys]);
|
|
345
|
+
|
|
346
|
+
const visibleColumns = useMemo(() => {
|
|
347
|
+
const byKey = new Map(columns.map((column) => [column.key, column]));
|
|
348
|
+
return resolvedVisibleKeys.map((key) => byKey.get(key)).filter((column): column is TableColumn<T> => Boolean(column));
|
|
349
|
+
}, [columns, resolvedVisibleKeys]);
|
|
350
|
+
|
|
351
|
+
function setVisibleKeys(next: string[]) {
|
|
352
|
+
const unique = Array.from(new Set(next.filter((key) => allowedKeys.has(key))));
|
|
353
|
+
const safe = unique.length > 0 ? unique : fallbackKeys.filter((key) => allowedKeys.has(key));
|
|
354
|
+
if (!isControlled) {
|
|
355
|
+
setInternalVisibleKeys(safe);
|
|
356
|
+
writeStoredColumnKeys(columnPrefsKey, safe);
|
|
357
|
+
}
|
|
358
|
+
onVisibleColumnKeysChange?.(safe);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function toggleColumnVisible(key: string, checked: boolean) {
|
|
362
|
+
if (checked) {
|
|
363
|
+
if (resolvedVisibleKeys.includes(key)) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
setVisibleKeys([...resolvedVisibleKeys, key]);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (resolvedVisibleKeys.length <= 1) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
setVisibleKeys(resolvedVisibleKeys.filter((entry) => entry !== key));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function selectAllColumns() {
|
|
376
|
+
setVisibleKeys(columns.map((column) => column.key));
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function resetColumns() {
|
|
380
|
+
setVisibleKeys(fallbackKeys);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const filterableColumns = visibleColumns.filter((column) => column.filterable !== false);
|
|
192
384
|
|
|
193
385
|
const filtered = useMemo(() => {
|
|
194
|
-
const activeFilters = columns.filter((column) => column.filterable !== false);
|
|
195
386
|
return items.filter((item) =>
|
|
196
|
-
|
|
387
|
+
filterableColumns.every((column) => {
|
|
197
388
|
const query = (filters[column.key] ?? "").trim();
|
|
198
389
|
if (!query) {
|
|
199
390
|
return true;
|
|
@@ -206,25 +397,27 @@ export function RecordWorkspace<T>({
|
|
|
206
397
|
return text.toLowerCase().includes(query.toLowerCase());
|
|
207
398
|
}),
|
|
208
399
|
);
|
|
209
|
-
}, [items, filters,
|
|
400
|
+
}, [items, filters, filterableColumns]);
|
|
210
401
|
|
|
211
402
|
const sorted = useMemo(() => {
|
|
212
403
|
if (!sort) {
|
|
213
404
|
return filtered;
|
|
214
405
|
}
|
|
215
|
-
const column =
|
|
406
|
+
const column = visibleColumns.find((entry) => entry.key === sort.key);
|
|
216
407
|
if (!column || column.sortable === false) {
|
|
217
408
|
return filtered;
|
|
218
409
|
}
|
|
219
410
|
const direction = sort.direction === "asc" ? 1 : -1;
|
|
220
411
|
return [...filtered].sort((left, right) => compareColumnValues(column, left, right) * direction);
|
|
221
|
-
}, [filtered, sort,
|
|
412
|
+
}, [filtered, sort, visibleColumns]);
|
|
222
413
|
|
|
223
414
|
const activeFilterCount = useMemo(
|
|
224
415
|
() => filterableColumns.reduce((count, column) => count + ((filters[column.key] ?? "").trim() ? 1 : 0), 0),
|
|
225
416
|
[filterableColumns, filters],
|
|
226
417
|
);
|
|
227
418
|
|
|
419
|
+
const hiddenColumnCount = columns.length - visibleColumns.length;
|
|
420
|
+
const toolsBadgeCount = activeFilterCount + (hiddenColumnCount > 0 ? 1 : 0) + (previewEnabled ? 1 : 0);
|
|
228
421
|
const showAll = pageSize === "all";
|
|
229
422
|
const numericPageSize = showAll ? Math.max(sorted.length, 1) : Number(pageSize);
|
|
230
423
|
const pageCount = showAll ? 1 : Math.max(1, Math.ceil(sorted.length / numericPageSize));
|
|
@@ -234,13 +427,54 @@ export function RecordWorkspace<T>({
|
|
|
234
427
|
const showAdd = canAdd && Boolean(onAdd);
|
|
235
428
|
const showDownload = Boolean(onDownload) && canDownload;
|
|
236
429
|
const showFilters = filterableColumns.length > 0;
|
|
430
|
+
const showColumns = showColumnPicker && columns.length > 0;
|
|
237
431
|
const previewItem = previewKey === null ? null : sorted.find((item) => rowKey(item) === previewKey) ?? null;
|
|
432
|
+
const exportColumns: ExportColumnRef[] = visibleColumns.map((column) => ({
|
|
433
|
+
key: column.exportKey ?? column.key,
|
|
434
|
+
header: column.header,
|
|
435
|
+
}));
|
|
436
|
+
const canShowTooltip = hoverTooltipsEnabled && !previewEnabled && actionsMenu === null;
|
|
437
|
+
|
|
438
|
+
useEffect(() => {
|
|
439
|
+
if (actionsMenu === null) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function onPointerDown(event: globalThis.MouseEvent) {
|
|
444
|
+
if (actionsMenuRef.current && !actionsMenuRef.current.contains(event.target as Node)) {
|
|
445
|
+
setActionsMenu(null);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
document.addEventListener("mousedown", onPointerDown);
|
|
450
|
+
return () => document.removeEventListener("mousedown", onPointerDown);
|
|
451
|
+
}, [actionsMenu]);
|
|
452
|
+
|
|
453
|
+
useEffect(() => {
|
|
454
|
+
if (!toolsDrawerOpen) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function onKeyDown(event: KeyboardEvent) {
|
|
459
|
+
if (event.key === "Escape") {
|
|
460
|
+
setToolsDrawerOpen(false);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
document.addEventListener("keydown", onKeyDown);
|
|
465
|
+
const previousOverflow = document.body.style.overflow;
|
|
466
|
+
document.body.style.overflow = "hidden";
|
|
467
|
+
return () => {
|
|
468
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
469
|
+
document.body.style.overflow = previousOverflow;
|
|
470
|
+
};
|
|
471
|
+
}, [toolsDrawerOpen]);
|
|
238
472
|
|
|
239
473
|
function requestDownload(format: ExportDownloadFormat) {
|
|
240
474
|
if (!onDownload || downloadBusy) {
|
|
241
475
|
return;
|
|
242
476
|
}
|
|
243
|
-
onDownload(format);
|
|
477
|
+
onDownload(format, exportColumns);
|
|
244
478
|
setDownloadOpen(false);
|
|
245
479
|
}
|
|
246
480
|
|
|
@@ -250,13 +484,13 @@ export function RecordWorkspace<T>({
|
|
|
250
484
|
}
|
|
251
485
|
|
|
252
486
|
function detailLines(item: T): DetailLine[] {
|
|
253
|
-
return
|
|
487
|
+
return visibleColumns.map((column) => ({
|
|
254
488
|
label: column.header,
|
|
255
489
|
value: cellText(column, item),
|
|
256
490
|
}));
|
|
257
491
|
}
|
|
258
492
|
|
|
259
|
-
const previewTitle = previewItem &&
|
|
493
|
+
const previewTitle = previewItem && visibleColumns[0] ? cellText(visibleColumns[0], previewItem) : null;
|
|
260
494
|
|
|
261
495
|
function changeFilter(key: string, value: string) {
|
|
262
496
|
setFilters((current) => ({ ...current, [key]: value }));
|
|
@@ -299,6 +533,9 @@ export function RecordWorkspace<T>({
|
|
|
299
533
|
}
|
|
300
534
|
|
|
301
535
|
function showTooltip(item: T, event: MouseEvent) {
|
|
536
|
+
if (!canShowTooltip) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
302
539
|
const padding = 16;
|
|
303
540
|
const width = 280;
|
|
304
541
|
const x = Math.min(event.clientX + 14, window.innerWidth - width - padding);
|
|
@@ -306,6 +543,22 @@ export function RecordWorkspace<T>({
|
|
|
306
543
|
setTooltip({ x, y, lines: detailLines(item) });
|
|
307
544
|
}
|
|
308
545
|
|
|
546
|
+
function openRowActions(item: T, event: MouseEvent<HTMLButtonElement>) {
|
|
547
|
+
event.stopPropagation();
|
|
548
|
+
setTooltip(null);
|
|
549
|
+
const key = rowKey(item);
|
|
550
|
+
if (actionsMenu?.key === key) {
|
|
551
|
+
setActionsMenu(null);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
555
|
+
const menuWidth = 168;
|
|
556
|
+
const menuHeight = 148;
|
|
557
|
+
const x = Math.min(Math.max(8, rect.right - menuWidth), window.innerWidth - menuWidth - 8);
|
|
558
|
+
const y = Math.min(rect.bottom + 6, window.innerHeight - menuHeight - 8);
|
|
559
|
+
setActionsMenu({ key, item, x, y });
|
|
560
|
+
}
|
|
561
|
+
|
|
309
562
|
const summary =
|
|
310
563
|
sorted.length === 0
|
|
311
564
|
? "0 records"
|
|
@@ -313,8 +566,320 @@ export function RecordWorkspace<T>({
|
|
|
313
566
|
? `1–${sorted.length} of ${sorted.length}`
|
|
314
567
|
: `${start + 1}–${Math.min(start + numericPageSize, sorted.length)} of ${sorted.length}`;
|
|
315
568
|
|
|
569
|
+
function renderToolsBody({ compactIcons }: { compactIcons: boolean }) {
|
|
570
|
+
if (compactIcons) {
|
|
571
|
+
return (
|
|
572
|
+
<div className="side-rail-icons">
|
|
573
|
+
<label className="side-rail-icon preview-check-icon" title="Preview">
|
|
574
|
+
<input
|
|
575
|
+
type="checkbox"
|
|
576
|
+
checked={previewEnabled}
|
|
577
|
+
onChange={(event) => setPreviewEnabledValue(event.target.checked)}
|
|
578
|
+
aria-label="Preview"
|
|
579
|
+
/>
|
|
580
|
+
<PreviewIcon />
|
|
581
|
+
</label>
|
|
582
|
+
{showAdd ? (
|
|
583
|
+
<button type="button" className="ghost side-rail-icon" title={addLabel} aria-label={addLabel} onClick={onAdd}>
|
|
584
|
+
<PlusIcon />
|
|
585
|
+
</button>
|
|
586
|
+
) : null}
|
|
587
|
+
{showColumns ? (
|
|
588
|
+
<button
|
|
589
|
+
type="button"
|
|
590
|
+
className="ghost side-rail-icon"
|
|
591
|
+
title="Columns"
|
|
592
|
+
aria-label="Columns"
|
|
593
|
+
onClick={() => {
|
|
594
|
+
setSideOpen(true);
|
|
595
|
+
setColumnsOpen(true);
|
|
596
|
+
}}
|
|
597
|
+
>
|
|
598
|
+
<ColumnsIcon />
|
|
599
|
+
{hiddenColumnCount > 0 ? <span className="filter-badge">{hiddenColumnCount}</span> : null}
|
|
600
|
+
</button>
|
|
601
|
+
) : null}
|
|
602
|
+
{showDownload ? (
|
|
603
|
+
<button
|
|
604
|
+
type="button"
|
|
605
|
+
className="ghost side-rail-icon"
|
|
606
|
+
title="Download"
|
|
607
|
+
aria-label="Download"
|
|
608
|
+
disabled={downloadBusy}
|
|
609
|
+
onClick={() => {
|
|
610
|
+
setSideOpen(true);
|
|
611
|
+
setDownloadOpen(true);
|
|
612
|
+
}}
|
|
613
|
+
>
|
|
614
|
+
<DownloadIcon />
|
|
615
|
+
</button>
|
|
616
|
+
) : null}
|
|
617
|
+
{showFilters ? (
|
|
618
|
+
<button
|
|
619
|
+
type="button"
|
|
620
|
+
className="ghost side-rail-icon"
|
|
621
|
+
title="Show filters"
|
|
622
|
+
aria-label="Show filters"
|
|
623
|
+
onClick={() => setSideOpen(true)}
|
|
624
|
+
>
|
|
625
|
+
<FilterIcon />
|
|
626
|
+
{activeFilterCount > 0 ? <span className="filter-badge">{activeFilterCount}</span> : null}
|
|
627
|
+
</button>
|
|
628
|
+
) : null}
|
|
629
|
+
{showFilters ? (
|
|
630
|
+
<button
|
|
631
|
+
type="button"
|
|
632
|
+
className="ghost side-rail-icon"
|
|
633
|
+
title="Reset filters"
|
|
634
|
+
aria-label="Reset filters"
|
|
635
|
+
disabled={activeFilterCount === 0}
|
|
636
|
+
onClick={clearFilters}
|
|
637
|
+
>
|
|
638
|
+
<ResetIcon />
|
|
639
|
+
</button>
|
|
640
|
+
) : null}
|
|
641
|
+
</div>
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return (
|
|
646
|
+
<div className="side-rail-body">
|
|
647
|
+
<div className="preview-check">
|
|
648
|
+
<input
|
|
649
|
+
type="checkbox"
|
|
650
|
+
checked={previewEnabled}
|
|
651
|
+
onChange={(event) => setPreviewEnabledValue(event.target.checked)}
|
|
652
|
+
aria-label="Preview"
|
|
653
|
+
/>
|
|
654
|
+
<PreviewIcon />
|
|
655
|
+
<span aria-hidden="true">Preview</span>
|
|
656
|
+
</div>
|
|
657
|
+
|
|
658
|
+
{showAdd ? (
|
|
659
|
+
<button
|
|
660
|
+
type="button"
|
|
661
|
+
className="side-rail-add"
|
|
662
|
+
onClick={() => {
|
|
663
|
+
setToolsDrawerOpen(false);
|
|
664
|
+
onAdd?.();
|
|
665
|
+
}}
|
|
666
|
+
>
|
|
667
|
+
<PlusIcon />
|
|
668
|
+
<span>{addLabel}</span>
|
|
669
|
+
</button>
|
|
670
|
+
) : null}
|
|
671
|
+
|
|
672
|
+
{showColumns ? (
|
|
673
|
+
<div className="side-rail-columns">
|
|
674
|
+
<button
|
|
675
|
+
type="button"
|
|
676
|
+
className="ghost side-rail-columns-toggle"
|
|
677
|
+
aria-expanded={columnsOpen}
|
|
678
|
+
onClick={() => setColumnsOpen((open) => !open)}
|
|
679
|
+
>
|
|
680
|
+
<ColumnsIcon />
|
|
681
|
+
<span>Columns{hiddenColumnCount > 0 ? ` (${visibleColumns.length}/${columns.length})` : ""}</span>
|
|
682
|
+
</button>
|
|
683
|
+
{columnsOpen ? (
|
|
684
|
+
<div className="side-rail-columns-panel" role="group" aria-label="Visible columns">
|
|
685
|
+
<div className="side-rail-columns-actions">
|
|
686
|
+
<button type="button" className="ghost" onClick={selectAllColumns}>
|
|
687
|
+
Select all
|
|
688
|
+
</button>
|
|
689
|
+
<button type="button" className="ghost" onClick={resetColumns}>
|
|
690
|
+
Reset
|
|
691
|
+
</button>
|
|
692
|
+
</div>
|
|
693
|
+
<div className="side-rail-columns-list">
|
|
694
|
+
{columns.map((column) => {
|
|
695
|
+
const checked = resolvedVisibleKeys.includes(column.key);
|
|
696
|
+
const disableUncheck = checked && resolvedVisibleKeys.length <= 1;
|
|
697
|
+
return (
|
|
698
|
+
<label key={column.key} className="side-rail-column-option">
|
|
699
|
+
<input
|
|
700
|
+
type="checkbox"
|
|
701
|
+
checked={checked}
|
|
702
|
+
disabled={disableUncheck}
|
|
703
|
+
onChange={(event) => toggleColumnVisible(column.key, event.target.checked)}
|
|
704
|
+
/>
|
|
705
|
+
<span>{column.header}</span>
|
|
706
|
+
</label>
|
|
707
|
+
);
|
|
708
|
+
})}
|
|
709
|
+
</div>
|
|
710
|
+
</div>
|
|
711
|
+
) : null}
|
|
712
|
+
</div>
|
|
713
|
+
) : null}
|
|
714
|
+
|
|
715
|
+
{showDownload ? (
|
|
716
|
+
<div className="side-rail-download">
|
|
717
|
+
<button
|
|
718
|
+
type="button"
|
|
719
|
+
className="ghost side-rail-download-toggle"
|
|
720
|
+
aria-expanded={downloadOpen}
|
|
721
|
+
disabled={downloadBusy}
|
|
722
|
+
onClick={() => setDownloadOpen((open) => !open)}
|
|
723
|
+
>
|
|
724
|
+
<DownloadIcon />
|
|
725
|
+
<span>{downloadBusy ? "Downloading…" : "Download"}</span>
|
|
726
|
+
</button>
|
|
727
|
+
{downloadOpen ? (
|
|
728
|
+
<div className="side-rail-download-formats" role="group" aria-label="Download format">
|
|
729
|
+
{EXPORT_FORMAT_OPTIONS.map((option) => (
|
|
730
|
+
<button
|
|
731
|
+
key={option.format}
|
|
732
|
+
type="button"
|
|
733
|
+
className="ghost side-rail-download-format"
|
|
734
|
+
disabled={downloadBusy}
|
|
735
|
+
onClick={() => requestDownload(option.format)}
|
|
736
|
+
>
|
|
737
|
+
{option.label}
|
|
738
|
+
</button>
|
|
739
|
+
))}
|
|
740
|
+
</div>
|
|
741
|
+
) : null}
|
|
742
|
+
</div>
|
|
743
|
+
) : null}
|
|
744
|
+
|
|
745
|
+
{showFilters ? (
|
|
746
|
+
<div className="side-rail-filters">
|
|
747
|
+
<div className="filter-panel-head">
|
|
748
|
+
<h3>Filters</h3>
|
|
749
|
+
<button
|
|
750
|
+
type="button"
|
|
751
|
+
className="ghost filter-clear"
|
|
752
|
+
title="Reset filters"
|
|
753
|
+
aria-label="Reset filters"
|
|
754
|
+
disabled={activeFilterCount === 0}
|
|
755
|
+
onClick={clearFilters}
|
|
756
|
+
>
|
|
757
|
+
<ResetIcon />
|
|
758
|
+
<span>Reset</span>
|
|
759
|
+
</button>
|
|
760
|
+
</div>
|
|
761
|
+
<div className="filter-panel-stack">
|
|
762
|
+
{filterableColumns.map((column) => (
|
|
763
|
+
<label key={column.key}>
|
|
764
|
+
{column.header}
|
|
765
|
+
{column.filterOptions ? (
|
|
766
|
+
<Select
|
|
767
|
+
value={filters[column.key] ?? ""}
|
|
768
|
+
options={[{ value: "", label: column.filterPlaceholder ?? "All" }, ...column.filterOptions]}
|
|
769
|
+
onChange={(value) => changeFilter(column.key, value)}
|
|
770
|
+
placeholder={column.filterPlaceholder ?? "All"}
|
|
771
|
+
/>
|
|
772
|
+
) : (
|
|
773
|
+
<input
|
|
774
|
+
value={filters[column.key] ?? ""}
|
|
775
|
+
onChange={(event) => changeFilter(column.key, event.target.value)}
|
|
776
|
+
placeholder={`Filter ${column.header.toLowerCase()}`}
|
|
777
|
+
/>
|
|
778
|
+
)}
|
|
779
|
+
</label>
|
|
780
|
+
))}
|
|
781
|
+
</div>
|
|
782
|
+
</div>
|
|
783
|
+
) : null}
|
|
784
|
+
</div>
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function renderRowActions(item: T) {
|
|
789
|
+
const key = rowKey(item);
|
|
790
|
+
const updateAllowed = canUpdate ? canUpdate(item) : Boolean(onUpdate);
|
|
791
|
+
const deleteAllowed = canDelete ? canDelete(item) : Boolean(onDelete);
|
|
792
|
+
const hasActions = Boolean(onView || onUpdate || onDelete);
|
|
793
|
+
if (!hasActions) {
|
|
794
|
+
return null;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return (
|
|
798
|
+
<>
|
|
799
|
+
<div className="row-actions row-actions-desktop">
|
|
800
|
+
{onView ? (
|
|
801
|
+
<button type="button" className="ghost row-action" onClick={() => onView(item)}>
|
|
802
|
+
View
|
|
803
|
+
</button>
|
|
804
|
+
) : null}
|
|
805
|
+
{onUpdate ? (
|
|
806
|
+
<button
|
|
807
|
+
type="button"
|
|
808
|
+
className="ghost row-action"
|
|
809
|
+
disabled={!updateAllowed}
|
|
810
|
+
title={updateAllowed ? "Update" : "Update is not available"}
|
|
811
|
+
onClick={() => onUpdate(item)}
|
|
812
|
+
>
|
|
813
|
+
Update
|
|
814
|
+
</button>
|
|
815
|
+
) : null}
|
|
816
|
+
{onDelete ? (
|
|
817
|
+
<button
|
|
818
|
+
type="button"
|
|
819
|
+
className="ghost row-action danger"
|
|
820
|
+
disabled={!deleteAllowed}
|
|
821
|
+
title={deleteAllowed ? "Delete" : "Delete is not available"}
|
|
822
|
+
onClick={() => onDelete(item)}
|
|
823
|
+
>
|
|
824
|
+
Delete
|
|
825
|
+
</button>
|
|
826
|
+
) : null}
|
|
827
|
+
</div>
|
|
828
|
+
|
|
829
|
+
<div className="row-actions row-actions-mobile">
|
|
830
|
+
<button
|
|
831
|
+
type="button"
|
|
832
|
+
className={actionsMenu?.key === key ? "ghost row-action-menu-trigger open" : "ghost row-action-menu-trigger"}
|
|
833
|
+
aria-haspopup="menu"
|
|
834
|
+
aria-expanded={actionsMenu?.key === key}
|
|
835
|
+
aria-label="Row actions"
|
|
836
|
+
onClick={(event) => openRowActions(item, event)}
|
|
837
|
+
>
|
|
838
|
+
<MoreIcon />
|
|
839
|
+
</button>
|
|
840
|
+
</div>
|
|
841
|
+
</>
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const actionsMenuItem = actionsMenu?.item;
|
|
846
|
+
const actionsUpdateAllowed = actionsMenuItem
|
|
847
|
+
? canUpdate
|
|
848
|
+
? canUpdate(actionsMenuItem)
|
|
849
|
+
: Boolean(onUpdate)
|
|
850
|
+
: false;
|
|
851
|
+
const actionsDeleteAllowed = actionsMenuItem
|
|
852
|
+
? canDelete
|
|
853
|
+
? canDelete(actionsMenuItem)
|
|
854
|
+
: Boolean(onDelete)
|
|
855
|
+
: false;
|
|
856
|
+
|
|
316
857
|
return (
|
|
317
|
-
<div className="list-workspace" aria-label={title}>
|
|
858
|
+
<div className={toolsDrawerOpen ? "list-workspace tools-drawer-open" : "list-workspace"} aria-label={title}>
|
|
859
|
+
<div className="tools-mobile-bar">
|
|
860
|
+
<div className="tools-mobile-bar-start">
|
|
861
|
+
{showAdd ? (
|
|
862
|
+
<button type="button" className="tools-mobile-add" onClick={onAdd}>
|
|
863
|
+
<PlusIcon />
|
|
864
|
+
<span>{addLabel}</span>
|
|
865
|
+
</button>
|
|
866
|
+
) : (
|
|
867
|
+
<p className="tools-mobile-title">{title}</p>
|
|
868
|
+
)}
|
|
869
|
+
</div>
|
|
870
|
+
<button
|
|
871
|
+
type="button"
|
|
872
|
+
className="ghost tools-mobile-toggle"
|
|
873
|
+
aria-expanded={toolsDrawerOpen}
|
|
874
|
+
aria-controls="aksui-tools-drawer"
|
|
875
|
+
onClick={() => setToolsDrawerOpen(true)}
|
|
876
|
+
>
|
|
877
|
+
<ToolsIcon />
|
|
878
|
+
<span>Tools</span>
|
|
879
|
+
{toolsBadgeCount > 0 ? <span className="filter-badge">{toolsBadgeCount}</span> : null}
|
|
880
|
+
</button>
|
|
881
|
+
</div>
|
|
882
|
+
|
|
318
883
|
<div
|
|
319
884
|
className={["list-body", sideOpen ? "side-open" : "", previewEnabled ? "preview-open" : ""]
|
|
320
885
|
.filter(Boolean)
|
|
@@ -332,7 +897,7 @@ export function RecordWorkspace<T>({
|
|
|
332
897
|
<table className="data-table">
|
|
333
898
|
<thead>
|
|
334
899
|
<tr>
|
|
335
|
-
{
|
|
900
|
+
{visibleColumns.map((column) => {
|
|
336
901
|
const sortable = column.sortable !== false;
|
|
337
902
|
const active = sort?.key === column.key ? sort.direction : null;
|
|
338
903
|
return (
|
|
@@ -363,7 +928,7 @@ export function RecordWorkspace<T>({
|
|
|
363
928
|
<tbody>
|
|
364
929
|
{pageItems.length === 0 ? (
|
|
365
930
|
<tr>
|
|
366
|
-
<td colSpan={
|
|
931
|
+
<td colSpan={visibleColumns.length + 1}>
|
|
367
932
|
<p className="hint table-empty">
|
|
368
933
|
{items.length === 0 ? empty : "No records match the current filters."}
|
|
369
934
|
</p>
|
|
@@ -372,60 +937,24 @@ export function RecordWorkspace<T>({
|
|
|
372
937
|
) : (
|
|
373
938
|
pageItems.map((item) => {
|
|
374
939
|
const key = rowKey(item);
|
|
375
|
-
const updateAllowed = canUpdate ? canUpdate(item) : Boolean(onUpdate);
|
|
376
|
-
const deleteAllowed = canDelete ? canDelete(item) : Boolean(onDelete);
|
|
377
940
|
const selected = previewEnabled && previewKey === key;
|
|
378
941
|
return (
|
|
379
942
|
<tr
|
|
380
943
|
key={key}
|
|
381
944
|
className={selected ? "data-row selected" : "data-row"}
|
|
382
945
|
onMouseEnter={(event) => {
|
|
383
|
-
|
|
384
|
-
showTooltip(item, event);
|
|
385
|
-
}
|
|
946
|
+
showTooltip(item, event);
|
|
386
947
|
focusRecord(item);
|
|
387
948
|
}}
|
|
388
|
-
onMouseMove={(event) =>
|
|
389
|
-
if (!previewEnabled) {
|
|
390
|
-
showTooltip(item, event);
|
|
391
|
-
}
|
|
392
|
-
}}
|
|
949
|
+
onMouseMove={(event) => showTooltip(item, event)}
|
|
393
950
|
onMouseLeave={() => setTooltip(null)}
|
|
394
951
|
onClick={() => focusRecord(item)}
|
|
395
952
|
>
|
|
396
|
-
{
|
|
953
|
+
{visibleColumns.map((column) => (
|
|
397
954
|
<td key={column.key}>{cellText(column, item)}</td>
|
|
398
955
|
))}
|
|
399
956
|
<td className="actions-col" onClick={(event) => event.stopPropagation()}>
|
|
400
|
-
|
|
401
|
-
{onView ? (
|
|
402
|
-
<button type="button" className="ghost row-action" onClick={() => onView(item)}>
|
|
403
|
-
View
|
|
404
|
-
</button>
|
|
405
|
-
) : null}
|
|
406
|
-
{onUpdate ? (
|
|
407
|
-
<button
|
|
408
|
-
type="button"
|
|
409
|
-
className="ghost row-action"
|
|
410
|
-
disabled={!updateAllowed}
|
|
411
|
-
title={updateAllowed ? "Update" : "Update is not available"}
|
|
412
|
-
onClick={() => onUpdate(item)}
|
|
413
|
-
>
|
|
414
|
-
Update
|
|
415
|
-
</button>
|
|
416
|
-
) : null}
|
|
417
|
-
{onDelete ? (
|
|
418
|
-
<button
|
|
419
|
-
type="button"
|
|
420
|
-
className="ghost row-action danger"
|
|
421
|
-
disabled={!deleteAllowed}
|
|
422
|
-
title={deleteAllowed ? "Delete" : "Delete is not available"}
|
|
423
|
-
onClick={() => onDelete(item)}
|
|
424
|
-
>
|
|
425
|
-
Delete
|
|
426
|
-
</button>
|
|
427
|
-
) : null}
|
|
428
|
-
</div>
|
|
957
|
+
{renderRowActions(item)}
|
|
429
958
|
</td>
|
|
430
959
|
</tr>
|
|
431
960
|
);
|
|
@@ -461,7 +990,7 @@ export function RecordWorkspace<T>({
|
|
|
461
990
|
{previewItem ? (
|
|
462
991
|
<DetailList lines={detailLines(previewItem)} />
|
|
463
992
|
) : (
|
|
464
|
-
<p className="preview-empty">
|
|
993
|
+
<p className="preview-empty">Select a row to preview its details here.</p>
|
|
465
994
|
)}
|
|
466
995
|
</div>
|
|
467
996
|
</aside>
|
|
@@ -478,161 +1007,99 @@ export function RecordWorkspace<T>({
|
|
|
478
1007
|
<ChevronIcon open={sideOpen} />
|
|
479
1008
|
{sideOpen ? <span>Collapse</span> : null}
|
|
480
1009
|
</button>
|
|
1010
|
+
{sideOpen ? renderToolsBody({ compactIcons: false }) : renderToolsBody({ compactIcons: true })}
|
|
1011
|
+
</aside>
|
|
1012
|
+
</div>
|
|
481
1013
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
type="button"
|
|
506
|
-
className="ghost side-rail-download-toggle"
|
|
507
|
-
aria-expanded={downloadOpen}
|
|
508
|
-
disabled={downloadBusy}
|
|
509
|
-
onClick={() => setDownloadOpen((open) => !open)}
|
|
510
|
-
>
|
|
511
|
-
<DownloadIcon />
|
|
512
|
-
<span>{downloadBusy ? "Downloading…" : "Download"}</span>
|
|
1014
|
+
{isClient
|
|
1015
|
+
? createPortal(
|
|
1016
|
+
<>
|
|
1017
|
+
<button
|
|
1018
|
+
type="button"
|
|
1019
|
+
className={toolsDrawerOpen ? "tools-drawer-backdrop open" : "tools-drawer-backdrop"}
|
|
1020
|
+
aria-label="Close tools"
|
|
1021
|
+
tabIndex={toolsDrawerOpen ? 0 : -1}
|
|
1022
|
+
onClick={() => setToolsDrawerOpen(false)}
|
|
1023
|
+
/>
|
|
1024
|
+
<aside
|
|
1025
|
+
id="aksui-tools-drawer"
|
|
1026
|
+
className={toolsDrawerOpen ? "tools-drawer open" : "tools-drawer"}
|
|
1027
|
+
aria-label="Workspace tools"
|
|
1028
|
+
aria-hidden={!toolsDrawerOpen}
|
|
1029
|
+
>
|
|
1030
|
+
<div className="tools-drawer-head">
|
|
1031
|
+
<div>
|
|
1032
|
+
<p className="tools-drawer-kicker">Workspace</p>
|
|
1033
|
+
<h3>Tools</h3>
|
|
1034
|
+
</div>
|
|
1035
|
+
<button type="button" className="ghost tools-drawer-close" onClick={() => setToolsDrawerOpen(false)}>
|
|
1036
|
+
Close
|
|
513
1037
|
</button>
|
|
514
|
-
{downloadOpen ? (
|
|
515
|
-
<div className="side-rail-download-formats" role="group" aria-label="Download format">
|
|
516
|
-
{EXPORT_FORMAT_OPTIONS.map((option) => (
|
|
517
|
-
<button
|
|
518
|
-
key={option.format}
|
|
519
|
-
type="button"
|
|
520
|
-
className="ghost side-rail-download-format"
|
|
521
|
-
disabled={downloadBusy}
|
|
522
|
-
onClick={() => requestDownload(option.format)}
|
|
523
|
-
>
|
|
524
|
-
{option.label}
|
|
525
|
-
</button>
|
|
526
|
-
))}
|
|
527
|
-
</div>
|
|
528
|
-
) : null}
|
|
529
1038
|
</div>
|
|
530
|
-
|
|
1039
|
+
<div className="tools-drawer-body">{renderToolsBody({ compactIcons: false })}</div>
|
|
1040
|
+
</aside>
|
|
1041
|
+
</>,
|
|
1042
|
+
document.body,
|
|
1043
|
+
)
|
|
1044
|
+
: null}
|
|
531
1045
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
>
|
|
544
|
-
<ResetIcon />
|
|
545
|
-
<span>Reset</span>
|
|
546
|
-
</button>
|
|
547
|
-
</div>
|
|
548
|
-
<div className="filter-panel-stack">
|
|
549
|
-
{filterableColumns.map((column) => (
|
|
550
|
-
<label key={column.key}>
|
|
551
|
-
{column.header}
|
|
552
|
-
{column.filterOptions ? (
|
|
553
|
-
<Select
|
|
554
|
-
value={filters[column.key] ?? ""}
|
|
555
|
-
options={[{ value: "", label: column.filterPlaceholder ?? "All" }, ...column.filterOptions]}
|
|
556
|
-
onChange={(value) => changeFilter(column.key, value)}
|
|
557
|
-
placeholder={column.filterPlaceholder ?? "All"}
|
|
558
|
-
/>
|
|
559
|
-
) : (
|
|
560
|
-
<input
|
|
561
|
-
value={filters[column.key] ?? ""}
|
|
562
|
-
onChange={(event) => changeFilter(column.key, event.target.value)}
|
|
563
|
-
placeholder={`Filter ${column.header.toLowerCase()}`}
|
|
564
|
-
/>
|
|
565
|
-
)}
|
|
566
|
-
</label>
|
|
567
|
-
))}
|
|
568
|
-
</div>
|
|
569
|
-
</div>
|
|
570
|
-
) : null}
|
|
571
|
-
</div>
|
|
572
|
-
) : (
|
|
573
|
-
<div className="side-rail-icons">
|
|
574
|
-
<label className="side-rail-icon preview-check-icon" title="Preview">
|
|
575
|
-
<input
|
|
576
|
-
type="checkbox"
|
|
577
|
-
checked={previewEnabled}
|
|
578
|
-
onChange={(event) => setPreviewEnabledValue(event.target.checked)}
|
|
579
|
-
aria-label="Preview"
|
|
580
|
-
/>
|
|
581
|
-
<PreviewIcon />
|
|
582
|
-
</label>
|
|
583
|
-
{showAdd ? (
|
|
584
|
-
<button type="button" className="ghost side-rail-icon" title={addLabel} aria-label={addLabel} onClick={onAdd}>
|
|
585
|
-
<PlusIcon />
|
|
586
|
-
</button>
|
|
587
|
-
) : null}
|
|
588
|
-
{showDownload ? (
|
|
1046
|
+
{form}
|
|
1047
|
+
|
|
1048
|
+
{isClient && actionsMenu && actionsMenuItem
|
|
1049
|
+
? createPortal(
|
|
1050
|
+
<div
|
|
1051
|
+
ref={actionsMenuRef}
|
|
1052
|
+
className="row-action-menu row-action-menu-portal"
|
|
1053
|
+
role="menu"
|
|
1054
|
+
style={{ left: actionsMenu.x, top: actionsMenu.y }}
|
|
1055
|
+
>
|
|
1056
|
+
{onView ? (
|
|
589
1057
|
<button
|
|
590
1058
|
type="button"
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
aria-label="Download"
|
|
594
|
-
aria-expanded={downloadOpen}
|
|
595
|
-
disabled={downloadBusy}
|
|
1059
|
+
role="menuitem"
|
|
1060
|
+
className="ghost"
|
|
596
1061
|
onClick={() => {
|
|
597
|
-
|
|
598
|
-
|
|
1062
|
+
setActionsMenu(null);
|
|
1063
|
+
onView(actionsMenuItem);
|
|
599
1064
|
}}
|
|
600
1065
|
>
|
|
601
|
-
|
|
1066
|
+
View
|
|
602
1067
|
</button>
|
|
603
1068
|
) : null}
|
|
604
|
-
{
|
|
1069
|
+
{onUpdate ? (
|
|
605
1070
|
<button
|
|
606
1071
|
type="button"
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
onClick={() =>
|
|
1072
|
+
role="menuitem"
|
|
1073
|
+
className="ghost"
|
|
1074
|
+
disabled={!actionsUpdateAllowed}
|
|
1075
|
+
onClick={() => {
|
|
1076
|
+
setActionsMenu(null);
|
|
1077
|
+
onUpdate(actionsMenuItem);
|
|
1078
|
+
}}
|
|
611
1079
|
>
|
|
612
|
-
|
|
613
|
-
{activeFilterCount > 0 ? <span className="filter-badge">{activeFilterCount}</span> : null}
|
|
1080
|
+
Update
|
|
614
1081
|
</button>
|
|
615
1082
|
) : null}
|
|
616
|
-
{
|
|
1083
|
+
{onDelete ? (
|
|
617
1084
|
<button
|
|
618
1085
|
type="button"
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
1086
|
+
role="menuitem"
|
|
1087
|
+
className="ghost danger"
|
|
1088
|
+
disabled={!actionsDeleteAllowed}
|
|
1089
|
+
onClick={() => {
|
|
1090
|
+
setActionsMenu(null);
|
|
1091
|
+
onDelete(actionsMenuItem);
|
|
1092
|
+
}}
|
|
624
1093
|
>
|
|
625
|
-
|
|
1094
|
+
Delete
|
|
626
1095
|
</button>
|
|
627
1096
|
) : null}
|
|
628
|
-
</div
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
{form}
|
|
1097
|
+
</div>,
|
|
1098
|
+
document.body,
|
|
1099
|
+
)
|
|
1100
|
+
: null}
|
|
634
1101
|
|
|
635
|
-
{isClient && tooltip &&
|
|
1102
|
+
{isClient && tooltip && canShowTooltip
|
|
636
1103
|
? createPortal(
|
|
637
1104
|
<div className="record-tooltip" style={{ left: tooltip.x, top: tooltip.y }} role="tooltip">
|
|
638
1105
|
<p className="record-tooltip-title">{title}</p>
|