@keenaioc/data-view 0.2.0-alpha.12 → 0.2.0-alpha.15
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 +30 -0
- package/INTEGRATION.md +83 -0
- package/KNOWN-LIMITATIONS.md +19 -0
- package/README.md +2 -2
- package/dist/action-menu.d.ts +37 -0
- package/dist/action-menu.d.ts.map +1 -0
- package/dist/action-menu.js +64 -0
- package/dist/action-menu.js.map +1 -0
- package/dist/column-menu.d.ts +2 -16
- package/dist/column-menu.d.ts.map +1 -1
- package/dist/column-menu.js +5 -62
- package/dist/column-menu.js.map +1 -1
- package/dist/data-view.d.ts.map +1 -1
- package/dist/data-view.js +182 -23
- package/dist/data-view.js.map +1 -1
- package/dist/gantt-grid.d.ts +2 -1
- package/dist/gantt-grid.d.ts.map +1 -1
- package/dist/gantt-grid.js +3 -3
- package/dist/gantt-grid.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/row-actions.d.ts +11 -0
- package/dist/row-actions.d.ts.map +1 -0
- package/dist/row-actions.js +15 -0
- package/dist/row-actions.js.map +1 -0
- package/dist/schedule-view.d.ts +2 -1
- package/dist/schedule-view.d.ts.map +1 -1
- package/dist/schedule-view.js +2 -2
- package/dist/schedule-view.js.map +1 -1
- package/dist/style.css +13 -3
- package/dist/types.d.ts +26 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/view-workspace.js +2 -2
- package/dist/view-workspace.js.map +1 -1
- package/package.json +5 -1
package/dist/data-view.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { Fragment, Children, useEffect, useId, useMemo, useRef, useState, } from "react";
|
|
4
4
|
import { columnMovePeers, moveColumnSetting } from "./field-order.js";
|
|
5
|
+
import { ActionMenu } from "./action-menu.js";
|
|
6
|
+
import { rowActionItems, retainHistoryAfterRowDelete } from "./row-actions.js";
|
|
7
|
+
import { rowGroupPaths } from "./row-move.js";
|
|
5
8
|
import { ColumnMenu } from "./column-menu.js";
|
|
6
9
|
import { headerItems, changeHeaderView, FieldActionForm, ColumnFilterForm, FilterValueInput } from "./column-actions.js";
|
|
7
10
|
import { GroupSettings } from "./group-settings.js";
|
|
@@ -44,9 +47,14 @@ function DataViewInner(props) {
|
|
|
44
47
|
const [scrollTop, setScrollTop] = useState(0);
|
|
45
48
|
const [focus, setFocus] = useState(null);
|
|
46
49
|
const [anchor, setAnchor] = useState(null);
|
|
50
|
+
const headerDragUntil = useRef(0);
|
|
51
|
+
const [attachmentTarget, setAttachmentTarget] = useState(null);
|
|
47
52
|
const [detailId, setDetailId] = useState(null);
|
|
48
53
|
const [dialog, setDialog] = useState(null);
|
|
49
54
|
const menuId = useId();
|
|
55
|
+
const rowMenuId = useId();
|
|
56
|
+
const [rowMenu, setRowMenu] = useState(null);
|
|
57
|
+
const [rowDelete, setRowDelete] = useState(null);
|
|
50
58
|
const [headerMenu, setHeaderMenu] = useState(null);
|
|
51
59
|
const [fieldAction, setFieldAction] = useState(null);
|
|
52
60
|
const [pending, setPending] = useState(null);
|
|
@@ -102,8 +110,9 @@ function DataViewInner(props) {
|
|
|
102
110
|
} }; window.addEventListener("beforeunload", leave); return () => window.removeEventListener("beforeunload", leave); }, []);
|
|
103
111
|
const selected = selectedRowIds ?? localSelected;
|
|
104
112
|
const selectable = permissions?.canSelectRows !== false;
|
|
105
|
-
const
|
|
106
|
-
const
|
|
113
|
+
const rowMenuEnabled = !!props.onRowChange && features.rowMenu !== false;
|
|
114
|
+
const controlColumn = selectable || !!props.rowMove || features.rowOpen === true || rowMenuEnabled;
|
|
115
|
+
const controlWidth = (selectable ? 32 : 0) + (props.rowMove ? 24 : 0) + (features.rowOpen === true ? 28 : 0) + (rowMenuEnabled ? 28 : 0);
|
|
107
116
|
const move = useRowMove(props, activeView, rootRef, (id, action) => writes.current.run([id], action));
|
|
108
117
|
const configurable = permissions?.canConfigureView !== false;
|
|
109
118
|
const canManageFields = Boolean(onFieldChange && permissions?.canManageFields === true && access === "granted");
|
|
@@ -145,17 +154,31 @@ function DataViewInner(props) {
|
|
|
145
154
|
setViewSaveState(e instanceof DataViewConflictError ? "conflict" : "failed"); });
|
|
146
155
|
}
|
|
147
156
|
function patchView(patch, scope = "layout") { setView({ ...viewRef.current, ...patch }, scope); }
|
|
157
|
+
const updateView = useRef(setView);
|
|
158
|
+
updateView.current = setView;
|
|
159
|
+
const stopResize = useRef(null);
|
|
160
|
+
useEffect(() => () => stopResize.current?.(), []);
|
|
148
161
|
function patchLocation(patch) { patchView({ location: { ...viewRef.current.location, ...patch } }, "navigation"); }
|
|
149
162
|
const query = useMemo(() => createDataViewQuery(activeView), [activeView.filters, activeView.sorts, activeView.groups, search]);
|
|
150
163
|
const queryKey = dataViewQueryKey(query);
|
|
151
164
|
const remoteStale = !!props.remote && props.remote.queryKey !== queryKey;
|
|
165
|
+
const rowContext = useRef({ props, query, queryKey, selected });
|
|
166
|
+
rowContext.current = { props, query, queryKey, selected };
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
const unavailable = (target) => target && (target.queryKey !== queryKey || !rowMenuEnabled || access !== "granted" || loading || error || !rows.some(row => getRowId(row) === target.rowId));
|
|
169
|
+
if (unavailable(rowMenu))
|
|
170
|
+
setRowMenu(null);
|
|
171
|
+
if (unavailable(rowDelete))
|
|
172
|
+
setRowDelete(null);
|
|
173
|
+
}, [rows, queryKey, rowMenuEnabled, access, loading, error, rowMenu, rowDelete, getRowId]);
|
|
174
|
+
useEffect(() => { setRowMenu(null); setRowDelete(null); }, [props.presentation, activeView.viewType]);
|
|
152
175
|
const queryCallback = useRef(props.onQueryChange);
|
|
153
176
|
queryCallback.current = props.onQueryChange;
|
|
154
177
|
const previousQuery = useRef(queryKey);
|
|
155
178
|
useEffect(() => { if (access === "granted" && !loading && !error)
|
|
156
179
|
queryCallback.current?.(query); }, [queryKey, props.onQueryChange, access, loading, error]);
|
|
157
180
|
useEffect(() => { if (previousQuery.current === queryKey)
|
|
158
|
-
return; previousQuery.current = queryKey; setLocalSelected([]); onSelectionChange?.([]); setFocus(null); setAnchor(null); setEditing(null); setUndoStack([]); setRedoStack([]); }, [queryKey]);
|
|
181
|
+
return; previousQuery.current = queryKey; setLocalSelected([]); onSelectionChange?.([]); setFocus(null); setAnchor(null); setEditing(null); setUndoStack([]); setRedoStack([]); setAttachmentTarget(null); }, [queryKey]);
|
|
159
182
|
const canFilter = permissions?.canFilter ?? configurable;
|
|
160
183
|
const canSort = permissions?.canSort ?? configurable;
|
|
161
184
|
const processedRows = useMemo(() => {
|
|
@@ -169,8 +192,8 @@ function DataViewInner(props) {
|
|
|
169
192
|
const groups = fullGroups;
|
|
170
193
|
const visible = useMemo(() => visibleFields(fields, activeView), [activeView, fields]);
|
|
171
194
|
const tableColumnCount = visible.length + (controlColumn ? 1 : 0) + (canManageFields ? 1 : 0);
|
|
172
|
-
const pinnedOffsets = useMemo(() => getPinnedOffsets(activeView.columns, controlWidth), [activeView.columns,
|
|
173
|
-
const ganttLeftOffsets = useMemo(() => getPinnedOffsets(activeView.columns, 44), [activeView.columns]);
|
|
195
|
+
const pinnedOffsets = useMemo(() => getPinnedOffsets(activeView.columns, controlWidth), [activeView.columns, controlWidth]);
|
|
196
|
+
const ganttLeftOffsets = useMemo(() => getPinnedOffsets(activeView.columns, 44 + (rowMenuEnabled ? 28 : 0)), [activeView.columns, rowMenuEnabled]);
|
|
174
197
|
const ganttRightOffsets = useMemo(() => getRightPinnedOffsets(activeView.columns), [activeView.columns]);
|
|
175
198
|
const tableWidth = visible.reduce((total, item) => total + item.column.width, (controlWidth) + addColumnWidth);
|
|
176
199
|
const collapsed = useMemo(() => new Set(activeView.collapsedGroups), [activeView.collapsedGroups]);
|
|
@@ -198,7 +221,7 @@ function DataViewInner(props) {
|
|
|
198
221
|
const field = fields.find((f) => f.id === id) ?? availableFields.find((f) => !f.renderAction && canRead(row, f));
|
|
199
222
|
if (field && (field.internal || !canRead(row, field)))
|
|
200
223
|
return "無權限";
|
|
201
|
-
return props.getRowLabel?.(row)?.trim() || (field ? formatReadableValue(getFieldValue(row, field), { ...field, internal: false }, row) : "") || "未命名工作";
|
|
224
|
+
return props.getRowLabel?.(row)?.trim() || (field ? formatReadableValue(getFieldValue(row, field), { ...field, internal: false }, row).trim() : "") || "未命名工作";
|
|
202
225
|
}
|
|
203
226
|
function openRow(row) { if (features.rowOpen === false)
|
|
204
227
|
return; if (props.onRowOpen)
|
|
@@ -261,10 +284,16 @@ function DataViewInner(props) {
|
|
|
261
284
|
}
|
|
262
285
|
function resizeColumn(fieldId, startWidth, event) {
|
|
263
286
|
event.preventDefault();
|
|
287
|
+
stopResize.current?.();
|
|
264
288
|
const startX = event.clientX;
|
|
265
289
|
function move(pointer) {
|
|
290
|
+
const current = rowContext.current.props;
|
|
291
|
+
if (current.access === "denied" || current.loading || current.error || current.permissions?.canConfigureView === false || !current.fields.some(field => field.id === fieldId && !field.internal) || !viewRef.current.columns.some(column => column.fieldId === fieldId && column.visible)) {
|
|
292
|
+
up();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
266
295
|
const width = normalizeColumnWidth(startWidth + pointer.clientX - startX);
|
|
267
|
-
|
|
296
|
+
updateView.current({
|
|
268
297
|
...viewRef.current,
|
|
269
298
|
columns: viewRef.current.columns.map((column) => column.fieldId === fieldId ? { ...column, width } : column),
|
|
270
299
|
});
|
|
@@ -273,7 +302,10 @@ function DataViewInner(props) {
|
|
|
273
302
|
window.removeEventListener("pointermove", move);
|
|
274
303
|
window.removeEventListener("pointerup", up);
|
|
275
304
|
window.removeEventListener("pointercancel", up);
|
|
305
|
+
if (stopResize.current === up)
|
|
306
|
+
stopResize.current = null;
|
|
276
307
|
}
|
|
308
|
+
stopResize.current = up;
|
|
277
309
|
window.addEventListener("pointermove", move);
|
|
278
310
|
window.addEventListener("pointerup", up);
|
|
279
311
|
window.addEventListener("pointercancel", up);
|
|
@@ -380,7 +412,18 @@ function DataViewInner(props) {
|
|
|
380
412
|
setEditing(null);
|
|
381
413
|
setCellStatuses((current) => ({ ...current, [key]: { state: "saving" } }));
|
|
382
414
|
try {
|
|
383
|
-
await writes.current.run([rowId], () =>
|
|
415
|
+
await writes.current.run([rowId], () => {
|
|
416
|
+
const current = rowContext.current;
|
|
417
|
+
const fresh = current.props.rows.find(row => current.props.getRowId(row) === rowId);
|
|
418
|
+
const definition = current.props.fields.find(item => item.id === field.id);
|
|
419
|
+
if (!fresh || !definition || definition.type !== field.type)
|
|
420
|
+
throw new DataViewConflictError("資料列或欄位已變更,請重新選擇附件");
|
|
421
|
+
if (current.queryKey !== queryKey || current.props.remote && current.props.remote.queryKey !== current.queryKey)
|
|
422
|
+
throw new DataViewConflictError("查詢已變更,請重新選擇附件");
|
|
423
|
+
if (!current.props.onRequestAttachment || current.props.access === "denied" || current.props.loading || current.props.error || current.props.remote?.loading || !canWriteField(fresh, definition, current.props.permissions))
|
|
424
|
+
throw new Error("沒有附件上傳權限或資料仍在載入");
|
|
425
|
+
return current.props.onRequestAttachment({ row: fresh, rowId, field: definition, files });
|
|
426
|
+
});
|
|
384
427
|
setCellStatuses((current) => ({ ...current, [key]: { state: "saved" } }));
|
|
385
428
|
}
|
|
386
429
|
catch (uploadError) {
|
|
@@ -393,9 +436,13 @@ function DataViewInner(props) {
|
|
|
393
436
|
}
|
|
394
437
|
if (field.type === "relation" && !field.Editor && !field.renderQuickView && !field.render && canWriteField(row, field, permissions) && (onBatchCommit || onCellCommit))
|
|
395
438
|
return _jsxs("div", { className: "hy-dv-relation-cell", children: [_jsx(RelationSelect, { value: value, row: row, field: field, disabled: !canEdit(row, field), onCommit: (next) => void commitCell(row, field, next), onCancel: () => setEditing(null) }), status && _jsx(SaveBadge, { state: status.state, message: status.message })] });
|
|
439
|
+
if (field.type === "attachment" && surface !== "detail" && !field.render && !field.Editor && !field.renderQuickView) {
|
|
440
|
+
const count = spec.isEmpty(value) ? 0 : Array.isArray(value) ? value.length : 1;
|
|
441
|
+
return _jsxs("div", { className: "hy-dv-attachment-cell", children: [count > 0 || canEdit(row, field) ? _jsx("button", { type: "button", className: "hy-dv-attachment-count", "aria-haspopup": "dialog", "aria-label": `${readableLabel(row)} ${field.label}:${count ? `${count} 個附件,查看附件` : "新增附件"}`, title: count ? `${count} 個附件` : "新增附件", disabled: status?.state === "saving", onClick: () => { setAttachmentTarget({ rowId, fieldId: field.id }); setEditing(null); }, children: count > 0 ? `+${count}` : null }) : null, status && _jsx(SaveBadge, { state: status.state, message: status.message })] });
|
|
442
|
+
}
|
|
396
443
|
if (field.type === "attachment")
|
|
397
444
|
return _jsxs("div", { className: "hy-dv-attachment-cell", children: [(!spec.isEmpty(value) || !canEdit(row, field) || field.render) && renderFieldValue(value, field, row), canEdit(row, field) && _jsx("button", { className: "hy-dv-attachment-add", type: "button", "aria-label": "\u65B0\u589E\u9644\u4EF6", title: "\u65B0\u589E\u9644\u4EF6", onClick: () => setEditing({ rowId, fieldId: field.id, surface }), children: _jsx("svg", { "aria-hidden": "true", focusable: "false", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "m16 6-8.414 8.586a2 2 0 0 0 2.829 2.829l8.414-8.586a4 4 0 1 0-5.657-5.657l-8.379 8.551a6 6 0 1 0 8.485 8.485l8.379-8.551" }) }) }), status && _jsx(SaveBadge, { state: status.state, message: status.message })] });
|
|
398
|
-
return _jsxs("button", { type: "button", className: `hy-dv-cell-button ${canEdit(row, field) ? "is-editable" : ""} ${spec.toggleValue ? "is-toggle" : ""}`, role: spec.toggleValue ? "checkbox" : undefined, "aria-checked": spec.toggleValue ? Boolean(value) : undefined, "aria-label": spec.toggleValue ? `${
|
|
445
|
+
return _jsxs("button", { type: "button", className: `hy-dv-cell-button ${canEdit(row, field) ? "is-editable" : ""} ${spec.toggleValue ? "is-toggle" : ""}`, role: spec.toggleValue ? "checkbox" : undefined, "aria-checked": spec.toggleValue ? Boolean(value) : undefined, "aria-label": spec.toggleValue ? `${readableLabel(row)} ${field.label}` : undefined, "aria-busy": status?.state === "saving", tabIndex: surface === "desktop" ? -1 : undefined, onClick: (event) => {
|
|
399
446
|
if (event.shiftKey && surface === "desktop")
|
|
400
447
|
return;
|
|
401
448
|
setFocus({ rowId, fieldId: field.id });
|
|
@@ -458,8 +505,8 @@ function DataViewInner(props) {
|
|
|
458
505
|
if (validation)
|
|
459
506
|
throw new Error(validation);
|
|
460
507
|
setOperation({ state: "saving", message: `正在儲存 ${changes.length} 格` });
|
|
461
|
-
await onBatchCommit({ ...request, changes });
|
|
462
|
-
if (features.undo !== false && request.reason !== "undo" && request.reason !== "redo") {
|
|
508
|
+
const result = await onBatchCommit({ ...request, changes });
|
|
509
|
+
if (features.undo !== false && request.reason !== "undo" && request.reason !== "redo" && result?.history !== "skip") {
|
|
463
510
|
setUndoStack((stack) => [...stack.slice(-19), { changes: changes.map((c) => ({ ...c, previousValue: structuredClone(c.previousValue), nextValue: structuredClone(c.nextValue) })), scheduleGuard: request.scheduleGuard ? structuredClone(request.scheduleGuard) : undefined }]);
|
|
464
511
|
setRedoStack([]);
|
|
465
512
|
}
|
|
@@ -487,6 +534,76 @@ function DataViewInner(props) {
|
|
|
487
534
|
setDialog("review");
|
|
488
535
|
setEditing(null);
|
|
489
536
|
}
|
|
537
|
+
function rowOperationBlocked() {
|
|
538
|
+
const current = rowContext.current;
|
|
539
|
+
return busyRef.current || busy || !current.props.onRowChange || current.props.features?.rowMenu === false || current.props.access === "denied" || !!current.props.loading || !!current.props.error || !!current.props.remote?.loading || (!!current.props.remote && current.props.remote.queryKey !== current.queryKey);
|
|
540
|
+
}
|
|
541
|
+
function showRowMenu(row, anchor, point) {
|
|
542
|
+
if (rowOperationBlocked())
|
|
543
|
+
return;
|
|
544
|
+
setHeaderMenu(null);
|
|
545
|
+
setRowDelete(null);
|
|
546
|
+
setRowMenu({ rowId: getRowId(row), anchor, point, queryKey });
|
|
547
|
+
}
|
|
548
|
+
function rowContextEvent(row, event) {
|
|
549
|
+
if (!rowMenuEnabled || rowOperationBlocked() || event.defaultPrevented)
|
|
550
|
+
return;
|
|
551
|
+
if (event.type !== "contextmenu" && !("key" in event && (event.key === "ContextMenu" || event.key === "F10" && event.shiftKey)))
|
|
552
|
+
return;
|
|
553
|
+
const target = event.target;
|
|
554
|
+
if (target.closest?.("input,textarea,select,a,[contenteditable]:not([contenteditable='false']),.hy-dv-action-cell,.hy-dv-editor-wrap"))
|
|
555
|
+
return;
|
|
556
|
+
event.preventDefault();
|
|
557
|
+
event.stopPropagation();
|
|
558
|
+
const anchor = target.closest?.("[data-hy-cell]") ?? event.currentTarget;
|
|
559
|
+
showRowMenu(row, anchor, event.type === "contextmenu" && "clientX" in event ? { x: event.clientX, y: event.clientY } : undefined);
|
|
560
|
+
}
|
|
561
|
+
function rowMenuTrigger(row) {
|
|
562
|
+
return rowMenuEnabled && _jsx("button", { type: "button", className: "hy-dv-row-menu-trigger", "aria-label": `${readableLabel(row)}資料列操作`, title: "\u8CC7\u6599\u5217\u64CD\u4F5C", "aria-haspopup": "menu", "aria-expanded": rowMenu?.rowId === getRowId(row), "aria-controls": rowMenu?.rowId === getRowId(row) ? rowMenuId : undefined, disabled: rowOperationBlocked(), onPointerDown: event => event.stopPropagation(), onClick: event => { event.stopPropagation(); showRowMenu(row, event.currentTarget); }, children: "\u22EF" });
|
|
563
|
+
}
|
|
564
|
+
async function applyRowChange(action, target) {
|
|
565
|
+
if (rowOperationBlocked())
|
|
566
|
+
return false;
|
|
567
|
+
busyRef.current = true;
|
|
568
|
+
setOperation({ state: "saving", message: action === "delete" ? "正在刪除此筆…" : "正在新增一筆…" });
|
|
569
|
+
try {
|
|
570
|
+
await writes.current.run(["*"], async () => {
|
|
571
|
+
const current = rowContext.current;
|
|
572
|
+
const row = current.props.rows.find(row => current.props.getRowId(row) === target.rowId);
|
|
573
|
+
if (target.queryKey !== current.queryKey || current.props.remote && current.props.remote.queryKey !== current.queryKey)
|
|
574
|
+
throw new DataViewConflictError("查詢已變更,請重新選擇資料列");
|
|
575
|
+
if (!row)
|
|
576
|
+
throw new DataViewConflictError("資料列已不存在,請重新選擇");
|
|
577
|
+
if (!current.props.onRowChange || current.props.features?.rowMenu === false || current.props.access === "denied" || current.props.loading || current.props.error || current.props.remote?.loading || current.props.permissions?.canChangeRow?.(row, action) !== true)
|
|
578
|
+
throw new Error("沒有此列操作權限或資料仍在載入");
|
|
579
|
+
const path = rowGroupPaths([row], current.props.fields, current.query.groups, current.props.getRowId).rowPaths.get(target.rowId) ?? [];
|
|
580
|
+
if (action !== "delete" && (path.length !== current.query.groups.length || path.some(p => p.valueKey === "__restricted__")))
|
|
581
|
+
throw new Error("無法在不可讀的分組新增資料");
|
|
582
|
+
await current.props.onRowChange({ id: newOperationId(), action, row, rowId: target.rowId, groupPath: path, query: structuredClone(current.query) });
|
|
583
|
+
});
|
|
584
|
+
if (action === "delete") {
|
|
585
|
+
setUndoStack(stack => retainHistoryAfterRowDelete(stack, target.rowId));
|
|
586
|
+
setRedoStack(stack => retainHistoryAfterRowDelete(stack, target.rowId));
|
|
587
|
+
changeSelection(rowContext.current.selected.filter(id => id !== target.rowId));
|
|
588
|
+
setFocus(current => current?.rowId === target.rowId ? null : current);
|
|
589
|
+
setAnchor(current => current?.rowId === target.rowId ? null : current);
|
|
590
|
+
setEditing(current => current?.rowId === target.rowId ? null : current);
|
|
591
|
+
setDetailId(current => current === target.rowId ? null : current);
|
|
592
|
+
setAttachmentTarget(current => current?.rowId === target.rowId ? null : current);
|
|
593
|
+
setQuick(current => current?.rowId === target.rowId ? null : current);
|
|
594
|
+
setRowDelete(null);
|
|
595
|
+
}
|
|
596
|
+
setOperation({ state: "saved", message: action === "delete" ? "已刪除此筆" : "已新增一筆;顯示位置依目前排序、分組與篩選" });
|
|
597
|
+
return true;
|
|
598
|
+
}
|
|
599
|
+
catch (error) {
|
|
600
|
+
reportFailure(error);
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
finally {
|
|
604
|
+
busyRef.current = false;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
490
607
|
async function applyFieldChange(change) {
|
|
491
608
|
if (!onFieldChange || busyRef.current || latest.current.access !== "granted")
|
|
492
609
|
return false;
|
|
@@ -498,7 +615,15 @@ function DataViewInner(props) {
|
|
|
498
615
|
busyRef.current = true;
|
|
499
616
|
setOperation({ state: "saving", message: "正在儲存欄位結構" });
|
|
500
617
|
try {
|
|
501
|
-
await writes.current.run(["*"], () =>
|
|
618
|
+
await writes.current.run(["*"], () => {
|
|
619
|
+
const current = rowContext.current.props;
|
|
620
|
+
if (!current.onFieldChange || current.access === "denied" || current.loading || current.error)
|
|
621
|
+
throw new Error("沒有管理欄位的權限或資料仍在載入");
|
|
622
|
+
const validation = validateFieldChange(current.fields, change, current.permissions);
|
|
623
|
+
if (validation)
|
|
624
|
+
throw new Error(validation);
|
|
625
|
+
return current.onFieldChange(change);
|
|
626
|
+
});
|
|
502
627
|
setUndoStack([]);
|
|
503
628
|
setRedoStack([]);
|
|
504
629
|
setFocus(null);
|
|
@@ -628,6 +753,14 @@ function DataViewInner(props) {
|
|
|
628
753
|
event.preventDefault();
|
|
629
754
|
const row = desktopRows[range.row];
|
|
630
755
|
const field = visible[range.col].field;
|
|
756
|
+
const attachmentCount = event.target.closest("td")?.querySelector(".hy-dv-attachment-count");
|
|
757
|
+
if (attachmentCount) {
|
|
758
|
+
if (!attachmentCount.disabled) {
|
|
759
|
+
attachmentCount.focus();
|
|
760
|
+
attachmentCount.click();
|
|
761
|
+
}
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
631
764
|
const relationSelect = event.target.closest("td")?.querySelector(".hy-dv-relation-select");
|
|
632
765
|
if (relationSelect && !relationSelect.disabled) {
|
|
633
766
|
relationSelect.focus();
|
|
@@ -711,15 +844,35 @@ function DataViewInner(props) {
|
|
|
711
844
|
}
|
|
712
845
|
const range = gridRange();
|
|
713
846
|
const desktopRowIndices = new Map(desktopRows.map((row, index) => [getRowId(row), index]));
|
|
714
|
-
return _jsxs("section", { ref: rootRef, className: `hy-dv density-${activeView.density ?? "standard"} presentation-${props.presentation ?? "auto"} chrome-${props.chrome ?? "standard"} ${props.height !== undefined ? "has-total-height" : ""}`, style: { height: props.height, "--hy-row-height": `${rowHeight}px`, "--hy-header-height": `${metrics.headerHeight}px`, "--hy-group-height": `${metrics.groupHeight}px`, "--hy-cell-font": `${metrics.cellFontSize}px`, "--hy-cell-line": `${metrics.cellLineHeight}px`, "--hy-header-font": `${metrics.headerFontSize}px`, "--hy-cell-padding": `${metrics.cellPadding}px`, "--hy-header-left": `${metrics.headerPaddingLeft}px`, "--hy-header-right": `${metrics.headerPaddingRight}px`, "--hy-control-height": `${metrics.controlHeight}px`, "--hy-action-height": `${metrics.actionHeight}px`, "--hy-badge-height": `${metrics.badgeHeight}px`, "--hy-badge-font": `${metrics.badgeFontSize}px`, "--hy-badge-padding": `${metrics.badgePadding}px`, "--hy-viewport-height": typeof props.viewportHeight === "number" ? `${props.viewportHeight}px` : props.viewportHeight ?? "560px", "--hy-scrollbar-height": `${metrics.scrollbarHeight}px`, "--hy-scrollbar-thumb": `${metrics.scrollbarThumb}px` }, "aria-label": title, onKeyDown: handleKey, onCopy: handleCopy, onPaste: handlePaste, children: [_jsxs("div", { className: "hy-dv-heading", children: [_jsxs("header", { className: "hy-dv-titlebar", children: [props.headerStart, _jsxs("div", { children: [_jsx("h2", { children: title }), _jsx("span", { children: props.remote ? `符合 ${remoteStale ? "…" : props.remote.total ?? "…"} 筆,已載入 ${processedRows.length} 筆${props.remote.complete && !remoteStale ? "(完整)" : "(尚未完整)"}` : `${processedRows.length} / ${rows.length} 筆` })] }), _jsxs("div", { className: "hy-dv-title-status", children: [selected.length > 0 && _jsxs("span", { children: ["\u5DF2\u9078 ", selected.length, " \u7B46"] }), !props.hideViewSaveStatus && _jsx(SaveBadge, { state: viewSaveState, prefix: "\u6AA2\u8996" }), !props.hideViewSaveStatus && (viewSaveState === "failed" || viewSaveState === "conflict") && _jsx("button", { onClick: () => setView(viewRef.current), children: "\u91CD\u8A66\u5132\u5B58" })] }), props.headerEnd] }), features.viewTabs !== false && _jsxs("nav", { className: "hy-dv-view-tabs", "aria-label": "\u8CC7\u6599\u8996\u5716", children: [[["table", "資料表格"], ["gantt", "甘特圖"], ["calendar", "行事曆"]].map(([type, label]) => _jsx("button", { "aria-pressed": activeView.viewType === type, disabled: permissions?.canSwitchView === false, onClick: () => { patchView({ viewType: type }, configurable ? "layout" : "navigation"); setEditing(null); setPanel(null); }, children: label }, type)), configurable && features.viewSettings !== false && _jsx("button", { onClick: () => setPanel(panel === "settings" ? null : "settings"), children: activeView.viewType === "gantt" ? "甘特圖設定" : activeView.viewType === "calendar" ? "行事曆設定" : "視圖設定" })] })] }), _jsxs(DataViewToolbar, { compact: props.chrome === "compact", start: props.toolbarStart, end: props.toolbarEnd, secondary: _jsxs(_Fragment, { children: [configurable && features.columns !== false && _jsx(ToolbarButton, { active: panel === "columns", onClick: () => setPanel(panel === "columns" ? null : "columns"), children: "\u96FB\u8166\u8868\u683C\u6B04\u4F4D" }), configurable && features.colors !== false && _jsx(ToolbarButton, { active: panel === "colors", onClick: () => setPanel(panel === "colors" ? null : "colors"), children: "\u586B\u8272" }), canManageFields && _jsxs(_Fragment, { children: [_jsx(ToolbarButton, { onClick: () => { setPanel(null); setDialog("add-field"); }, children: "\uFF0B \u65B0\u589E\u6B04\u4F4D" }), _jsx(ToolbarButton, { onClick: () => { setPanel(null); setDialog("fields"); }, children: "\u7BA1\u7406\u6B04\u4F4D" })] }), onBatchCommit && _jsxs(_Fragment, { children: [features.batch !== false && _jsxs("button", { disabled: !selectedRows.length || busy, onClick: () => { setPanel(null); setDialog("batch"); }, children: ["\u6279\u6B21\u4FEE\u6539 ", selectedRows.length || ""] }), features.undo !== false && _jsx("button", { disabled: !undoStack.length || busy, onClick: () => void historyAction("undo"), children: "\u5FA9\u539F" }), features.redo !== false && _jsx("button", { disabled: !redoStack.length || busy, onClick: () => void historyAction("redo"), children: "\u91CD\u505A" })] }), selected.length > 0 && _jsx(ToolbarButton, { onClick: () => changeSelection([]), children: "\u6E05\u9664\u9078\u53D6" }), activeView.groups.length > 0 && permissions?.canCollapseGroups !== false && _jsxs(_Fragment, { children: [_jsx(ToolbarButton, { onClick: () => setAllGroups(true), children: "\u5168\u90E8\u6536\u5408" }), _jsx(ToolbarButton, { onClick: () => setAllGroups(false), children: "\u5168\u90E8\u5C55\u958B" })] })] }), children: [features.search !== false && permissions?.canSearch !== false && _jsxs("label", { className: "hy-dv-search", children: [_jsx("span", { "aria-hidden": true, children: "\u2315" }), _jsx("input", { "aria-label": searchPlaceholder, value: search, onChange: (event) => patchLocation({ search: event.target.value, tableTop: 0, ganttTop: 0 }), placeholder: searchPlaceholder })] }), canFilter && features.filter !== false && _jsxs(ToolbarButton, { active: panel === "filter" || activeView.filters.rules.length > 0, onClick: () => setPanel(panel === "filter" ? null : "filter"), children: ["\u7BE9\u9078 ", activeView.filters.rules.length || ""] }), canSort && features.sort !== false && _jsxs(ToolbarButton, { active: panel === "sort" || activeView.sorts.length > 0, onClick: () => setPanel(panel === "sort" ? null : "sort"), children: ["\u6392\u5E8F ", activeView.sorts.length || ""] }), configurable && features.group !== false && _jsxs(ToolbarButton, { active: panel === "group" || activeView.groups.length > 0, onClick: () => setPanel(panel === "group" ? null : "group"), children: ["\u5206\u7D44 ", activeView.groups.length || ""] }), props.onPresentationChange && _jsxs("select", { "aria-label": "\u5448\u73FE\u65B9\u5F0F", value: props.presentation ?? "auto", onChange: (e) => props.onPresentationChange?.(e.target.value), children: [_jsx("option", { value: "auto", children: "\u81EA\u52D5" }), _jsx("option", { value: "table", children: "\u8868\u683C" }), _jsx("option", { value: "cards", children: "\u5361\u7247" })] })] }), move.message && _jsx("p", { role: "status", children: move.message }), operation.state !== "idle" && _jsxs("div", { className: `hy-dv-operation is-${operation.state}`, role: operation.state === "failed" || operation.state === "conflict" ? "alert" : "status", children: [_jsx(SaveBadge, { state: operation.state }), _jsx("span", { children: operation.message })] }), panel && (configurable || panel === "filter" && canFilter || panel === "sort" && canSort) && _jsxs("div", { className: "hy-dv-panel-wrap", children: [panel === "filter" && _jsx(FilterPanel, { fields: availableFields, view: activeView, onChange: (v) => setView(v, "query"), onClose: () => setPanel(null) }), panel === "sort" && _jsx(SortPanel, { fields: availableFields, view: activeView, onChange: (v) => setView(v, "query"), onClose: () => setPanel(null) }), panel === "group" && _jsx(PanelShell, { title: "\u5206\u7D44\u8A2D\u5B9A", onClose: () => setPanel(null), children: _jsx(GroupSettings, { fields: fields, rows: remoteStale ? [] : rows, view: activeView, onChange: setView }) }), panel === "columns" && _jsx(ColumnPanel, { fields: availableFields, view: { ...activeView, columns: activeView.columns.filter((c) => availableFields.some((f) => f.id === c.fieldId)) }, onChange: setView, onClose: () => setPanel(null), dragged: draggedColumn, setDragged: setDraggedColumn, drop: dropColumn }), panel === "settings" && _jsx(PanelShell, { title: "\u8996\u5716\u8A2D\u5B9A", onClose: () => setPanel(null), children: _jsx(ViewSettings, { fields: availableFields, view: activeView, onChange: setView }) }), panel === "colors" && _jsx(PanelShell, { title: "\u586B\u8272\u898F\u5247", onClose: () => setPanel(null), children: _jsx(ColorSettings, { fields: availableFields, view: activeView, onChange: setView }) })] }), _jsx("div", { className: "hy-dv-body", children: remoteStale || props.remote?.loading && !processedRows.length ? _jsx(LoadingState, {}) : processedRows.length === 0 && props.remote?.error ? _jsx(StateCard, { title: "\u67E5\u8A62\u5931\u6557", message: props.remote.error, tone: "error" }) : processedRows.length === 0 ? _jsx(StateCard, { title: "\u6C92\u6709\u7B26\u5408\u689D\u4EF6\u7684\u8CC7\u6599", message: rows.length === 0 ? emptyMessage : "請調整搜尋或篩選條件。" }) : activeView.viewType !== "table" ? _jsx(ScheduleView, { metrics: metrics, getRowLabel: readableLabel, groupLabel: groupLabel, onLoadNearEnd: loadNearEnd, allRows: rows, dependencies: ganttDependencies, onDependencyChange: onGanttDependencyChange && permissions?.canManageDependencies === true ? changeDependency : undefined, onScheduleCommit: onBatchCommit ? execute : undefined, rows: processedRows, renderMobileRow: (row) => _jsx("dl", { className: "hy-dv-schedule-fields", children: mobileFields.filter((field) => canRead(row, field)).map((field) => _jsxs(Fragment, { children: [_jsx("dt", { children: field.label }), _jsx("dd", { children: renderCell(row, field, "mobile") })] }, field.id)) }), onLocationChange: patchLocation, onToggleGroup: toggleGroup, renderLeftHeader: () => _jsxs("tr", { children: [_jsx("th", { className: "hy-dv-open-header is-pinned", style: { left: 0 }, children: "\u8A73\u60C5" }), visible.map((v) => renderColumnHeader(v.field, v.column, true))] }), renderLeftRow: (row) => _jsxs("tr", { children: [_jsx("td", { className: "is-pinned", style: { left: 0 }, children: _jsx("button", { "aria-label": `開啟 ${getRowId(row)} 詳情`, onClick: () => openRow(row), children: "\u2197" }) }), visible.map(({ field }) => _jsx("td", { className: ganttLeftOffsets.has(field.id) || ganttRightOffsets.has(field.id) ? "is-pinned" : "", style: { left: ganttLeftOffsets.get(field.id), right: ganttRightOffsets.get(field.id), backgroundColor: colorMap.get(getRowId(row))?.get(field.id) ?? colorMap.get(getRowId(row))?.get("row") }, children: _jsx("div", { className: "hy-dv-gantt-cell", children: renderCell(row, field, "gantt") }) }, field.id))] }, getRowId(row)), fields: fields, getRowId: getRowId, view: activeView, permissions: permissions, busy: busy, onCommit: onBatchCommit ? (changes) => execute({ id: newOperationId(), reason: "edit", changes }) : undefined, onOpen: openRow }, activeView.viewType) : _jsxs(_Fragment, { children: [_jsx("div", { className: "hy-dv-desktop", children: _jsx("div", { ref: scrollRef, className: `hy-dv-table-scroll ${virtual ? "is-virtual" : ""}`, onScroll: (event) => { setScrollTop(event.currentTarget.scrollTop); loadNearEnd(event.currentTarget); if (bottomRef.current)
|
|
847
|
+
return _jsxs("section", { ref: rootRef, className: `hy-dv density-${activeView.density ?? "standard"} presentation-${props.presentation ?? "auto"} chrome-${props.chrome ?? "standard"} ${props.height !== undefined ? "has-total-height" : ""}`, style: { height: props.height, "--hy-row-height": `${rowHeight}px`, "--hy-header-height": `${metrics.headerHeight}px`, "--hy-group-height": `${metrics.groupHeight}px`, "--hy-cell-font": `${metrics.cellFontSize}px`, "--hy-cell-line": `${metrics.cellLineHeight}px`, "--hy-header-font": `${metrics.headerFontSize}px`, "--hy-cell-padding": `${metrics.cellPadding}px`, "--hy-header-left": `${metrics.headerPaddingLeft}px`, "--hy-header-right": `${metrics.headerPaddingRight}px`, "--hy-control-height": `${metrics.controlHeight}px`, "--hy-action-height": `${metrics.actionHeight}px`, "--hy-badge-height": `${metrics.badgeHeight}px`, "--hy-badge-font": `${metrics.badgeFontSize}px`, "--hy-badge-padding": `${metrics.badgePadding}px`, "--hy-viewport-height": typeof props.viewportHeight === "number" ? `${props.viewportHeight}px` : props.viewportHeight ?? "560px", "--hy-scrollbar-height": `${metrics.scrollbarHeight}px`, "--hy-scrollbar-thumb": `${metrics.scrollbarThumb}px` }, "aria-label": title, onKeyDown: handleKey, onCopy: handleCopy, onPaste: handlePaste, children: [_jsxs("div", { className: "hy-dv-heading", children: [_jsxs("header", { className: "hy-dv-titlebar", children: [props.headerStart, _jsxs("div", { children: [_jsx("h2", { children: title }), _jsx("span", { children: props.remote ? `符合 ${remoteStale ? "…" : props.remote.total ?? "…"} 筆,已載入 ${processedRows.length} 筆${props.remote.complete && !remoteStale ? "(完整)" : "(尚未完整)"}` : `${processedRows.length} / ${rows.length} 筆` })] }), _jsxs("div", { className: "hy-dv-title-status", children: [selected.length > 0 && _jsxs("span", { children: ["\u5DF2\u9078 ", selected.length, " \u7B46"] }), !props.hideViewSaveStatus && _jsx(SaveBadge, { state: viewSaveState, prefix: "\u6AA2\u8996" }), !props.hideViewSaveStatus && (viewSaveState === "failed" || viewSaveState === "conflict") && _jsx("button", { onClick: () => setView(viewRef.current), children: "\u91CD\u8A66\u5132\u5B58" })] }), props.headerEnd] }), features.viewTabs !== false && _jsxs("nav", { className: "hy-dv-view-tabs", "aria-label": "\u8CC7\u6599\u8996\u5716", children: [[["table", "資料表格"], ["gantt", "甘特圖"], ["calendar", "行事曆"]].map(([type, label]) => _jsx("button", { "aria-pressed": activeView.viewType === type, disabled: permissions?.canSwitchView === false, onClick: () => { patchView({ viewType: type }, configurable ? "layout" : "navigation"); setEditing(null); setPanel(null); }, children: label }, type)), configurable && features.viewSettings !== false && _jsx("button", { onClick: () => setPanel(panel === "settings" ? null : "settings"), children: activeView.viewType === "gantt" ? "甘特圖設定" : activeView.viewType === "calendar" ? "行事曆設定" : "視圖設定" })] })] }), _jsxs(DataViewToolbar, { compact: props.chrome === "compact", start: props.toolbarStart, end: props.toolbarEnd, secondary: _jsxs(_Fragment, { children: [configurable && features.columns !== false && _jsx(ToolbarButton, { active: panel === "columns", onClick: () => setPanel(panel === "columns" ? null : "columns"), children: "\u96FB\u8166\u8868\u683C\u6B04\u4F4D" }), configurable && features.colors !== false && _jsx(ToolbarButton, { active: panel === "colors", onClick: () => setPanel(panel === "colors" ? null : "colors"), children: "\u586B\u8272" }), canManageFields && _jsxs(_Fragment, { children: [_jsx(ToolbarButton, { onClick: () => { setPanel(null); setDialog("add-field"); }, children: "\uFF0B \u65B0\u589E\u6B04\u4F4D" }), _jsx(ToolbarButton, { onClick: () => { setPanel(null); setDialog("fields"); }, children: "\u7BA1\u7406\u6B04\u4F4D" })] }), onBatchCommit && _jsxs(_Fragment, { children: [features.batch !== false && _jsxs("button", { disabled: !selectedRows.length || busy, onClick: () => { setPanel(null); setDialog("batch"); }, children: ["\u6279\u6B21\u4FEE\u6539 ", selectedRows.length || ""] }), features.undo !== false && _jsx("button", { disabled: !undoStack.length || busy, onClick: () => void historyAction("undo"), children: "\u5FA9\u539F" }), features.redo !== false && _jsx("button", { disabled: !redoStack.length || busy, onClick: () => void historyAction("redo"), children: "\u91CD\u505A" })] }), selected.length > 0 && _jsx(ToolbarButton, { onClick: () => changeSelection([]), children: "\u6E05\u9664\u9078\u53D6" }), activeView.groups.length > 0 && permissions?.canCollapseGroups !== false && _jsxs(_Fragment, { children: [_jsx(ToolbarButton, { onClick: () => setAllGroups(true), children: "\u5168\u90E8\u6536\u5408" }), _jsx(ToolbarButton, { onClick: () => setAllGroups(false), children: "\u5168\u90E8\u5C55\u958B" })] })] }), children: [features.search !== false && permissions?.canSearch !== false && _jsxs("label", { className: "hy-dv-search", children: [_jsx("span", { "aria-hidden": true, children: "\u2315" }), _jsx("input", { "aria-label": searchPlaceholder, value: search, onChange: (event) => patchLocation({ search: event.target.value, tableTop: 0, ganttTop: 0 }), placeholder: searchPlaceholder })] }), canFilter && features.filter !== false && _jsxs(ToolbarButton, { active: panel === "filter" || activeView.filters.rules.length > 0, onClick: () => setPanel(panel === "filter" ? null : "filter"), children: ["\u7BE9\u9078 ", activeView.filters.rules.length || ""] }), canSort && features.sort !== false && _jsxs(ToolbarButton, { active: panel === "sort" || activeView.sorts.length > 0, onClick: () => setPanel(panel === "sort" ? null : "sort"), children: ["\u6392\u5E8F ", activeView.sorts.length || ""] }), configurable && features.group !== false && _jsxs(ToolbarButton, { active: panel === "group" || activeView.groups.length > 0, onClick: () => setPanel(panel === "group" ? null : "group"), children: ["\u5206\u7D44 ", activeView.groups.length || ""] }), props.onPresentationChange && _jsxs("select", { "aria-label": "\u5448\u73FE\u65B9\u5F0F", value: props.presentation ?? "auto", onChange: (e) => props.onPresentationChange?.(e.target.value), children: [_jsx("option", { value: "auto", children: "\u81EA\u52D5" }), _jsx("option", { value: "table", children: "\u8868\u683C" }), _jsx("option", { value: "cards", children: "\u5361\u7247" })] })] }), move.message && _jsx("p", { role: "status", children: move.message }), operation.state !== "idle" && _jsxs("div", { className: `hy-dv-operation is-${operation.state}`, role: operation.state === "failed" || operation.state === "conflict" ? "alert" : "status", children: [_jsx(SaveBadge, { state: operation.state }), _jsx("span", { children: operation.message })] }), panel && (configurable || panel === "filter" && canFilter || panel === "sort" && canSort) && _jsxs("div", { className: "hy-dv-panel-wrap", children: [panel === "filter" && _jsx(FilterPanel, { fields: availableFields, view: activeView, onChange: (v) => setView(v, "query"), onClose: () => setPanel(null) }), panel === "sort" && _jsx(SortPanel, { fields: availableFields, view: activeView, onChange: (v) => setView(v, "query"), onClose: () => setPanel(null) }), panel === "group" && _jsx(PanelShell, { title: "\u5206\u7D44\u8A2D\u5B9A", onClose: () => setPanel(null), children: _jsx(GroupSettings, { fields: fields, rows: remoteStale ? [] : rows, view: activeView, onChange: setView }) }), panel === "columns" && _jsx(ColumnPanel, { fields: availableFields, view: { ...activeView, columns: activeView.columns.filter((c) => availableFields.some((f) => f.id === c.fieldId)) }, onChange: setView, onClose: () => setPanel(null), dragged: draggedColumn, setDragged: setDraggedColumn, drop: dropColumn }), panel === "settings" && _jsx(PanelShell, { title: "\u8996\u5716\u8A2D\u5B9A", onClose: () => setPanel(null), children: _jsx(ViewSettings, { fields: availableFields, view: activeView, onChange: setView }) }), panel === "colors" && _jsx(PanelShell, { title: "\u586B\u8272\u898F\u5247", onClose: () => setPanel(null), children: _jsx(ColorSettings, { fields: availableFields, view: activeView, onChange: setView }) })] }), _jsx("div", { className: "hy-dv-body", children: remoteStale || props.remote?.loading && !processedRows.length ? _jsx(LoadingState, {}) : processedRows.length === 0 && props.remote?.error ? _jsx(StateCard, { title: "\u67E5\u8A62\u5931\u6557", message: props.remote.error, tone: "error" }) : processedRows.length === 0 ? _jsx(StateCard, { title: "\u6C92\u6709\u7B26\u5408\u689D\u4EF6\u7684\u8CC7\u6599", message: rows.length === 0 ? emptyMessage : "請調整搜尋或篩選條件。" }) : activeView.viewType !== "table" ? _jsx(ScheduleView, { controlWidth: 44 + (rowMenuEnabled ? 28 : 0), metrics: metrics, getRowLabel: readableLabel, groupLabel: groupLabel, onLoadNearEnd: loadNearEnd, allRows: rows, dependencies: ganttDependencies, onDependencyChange: onGanttDependencyChange && permissions?.canManageDependencies === true ? changeDependency : undefined, onScheduleCommit: onBatchCommit ? execute : undefined, rows: processedRows, renderMobileRow: (row) => _jsx("dl", { className: "hy-dv-schedule-fields", children: mobileFields.filter((field) => canRead(row, field)).map((field) => _jsxs(Fragment, { children: [_jsx("dt", { children: field.label }), _jsx("dd", { children: renderCell(row, field, "mobile") })] }, field.id)) }), onLocationChange: patchLocation, onToggleGroup: toggleGroup, renderLeftHeader: () => _jsxs("tr", { children: [_jsx("th", { className: "hy-dv-open-header is-pinned", style: { left: 0 }, children: "\u8A73\u60C5" }), visible.map((v) => renderColumnHeader(v.field, v.column, true))] }), renderLeftRow: (row) => _jsxs("tr", { tabIndex: rowMenuEnabled ? -1 : undefined, onContextMenu: event => rowContextEvent(row, event), onKeyDown: event => rowContextEvent(row, event), children: [_jsx("td", { className: "is-pinned", style: { left: 0 }, children: _jsxs("div", { className: "hy-dv-row-controls", children: [rowMenuTrigger(row), _jsx("button", { "aria-label": `開啟 ${readableLabel(row)} 詳情`, onClick: () => openRow(row), children: "\u2197" })] }) }), visible.map(({ field }) => _jsx("td", { className: ganttLeftOffsets.has(field.id) || ganttRightOffsets.has(field.id) ? "is-pinned" : "", style: { left: ganttLeftOffsets.get(field.id), right: ganttRightOffsets.get(field.id), backgroundColor: colorMap.get(getRowId(row))?.get(field.id) ?? colorMap.get(getRowId(row))?.get("row") }, children: _jsx("div", { className: "hy-dv-gantt-cell", children: renderCell(row, field, "gantt") }) }, field.id))] }, getRowId(row)), fields: fields, getRowId: getRowId, view: activeView, permissions: permissions, busy: busy, onCommit: onBatchCommit ? (changes) => execute({ id: newOperationId(), reason: "edit", changes }) : undefined, onOpen: openRow }, activeView.viewType) : _jsxs(_Fragment, { children: [_jsx("div", { className: "hy-dv-desktop", children: _jsx("div", { ref: scrollRef, className: `hy-dv-table-scroll ${virtual ? "is-virtual" : ""}`, onScroll: (event) => { setScrollTop(event.currentTarget.scrollTop); loadNearEnd(event.currentTarget); if (bottomRef.current)
|
|
715
848
|
bottomRef.current.scrollLeft = event.currentTarget.scrollLeft; patchLocation({ tableTop: event.currentTarget.scrollTop, tableLeft: event.currentTarget.scrollLeft }); }, children: _jsxs("table", { className: "hy-dv-table", style: { width: tableWidth }, "aria-label": "\u8CC7\u6599\u5132\u5B58\u683C", "aria-rowcount": desktopRows.length + 1, children: [_jsxs("colgroup", { children: [controlColumn && _jsx("col", { style: { width: controlWidth } }), visible.map(({ field, column }) => _jsx("col", { style: { width: column.width } }, field.id)), canManageFields && _jsx("col", { style: { width: addColumnWidth } })] }), _jsx("thead", { children: _jsxs("tr", { children: [controlColumn && _jsx("th", { className: "hy-dv-selection is-pinned", style: { left: 0 }, children: selectable && _jsx("input", { type: "checkbox", checked: allPageSelected, onChange: togglePageSelection, "aria-label": props.remote && !props.remote.complete ? "選取目前已載入資料" : "選取全部符合條件資料" }) }), visible.map(({ field, column }) => renderColumnHeader(field, column)), canManageFields && _jsx("th", { className: "hy-dv-add-column is-pinned", style: { right: 0 }, children: _jsx("button", { type: "button", disabled: busy, "aria-label": "\u65B0\u589E\u6B04\u4F4D", title: "\u65B0\u589E\u6B04\u4F4D", onClick: () => { setPanel(null); setDialog("add-field"); }, children: "\uFF0B" }) })] }) }), _jsxs("tbody", { children: [virtual && virtualStart > 0 && _jsx("tr", { "aria-hidden": true, children: _jsx("td", { colSpan: tableColumnCount, style: { height: windowed.before, padding: 0, border: 0 } }) }), flatEntries.slice(virtualStart, virtualEnd).map((entry) => entry.kind === "row" ? renderDesktopRow(entry.row) : _jsx("tr", { "data-group-key": entry.node.key, className: `hy-dv-group depth-${entry.node.depth} ${move.target?.position === "groupEnd" && move.target.targetGroupPath.at(-1)?.key === entry.node.key ? "is-drop-after" : ""}`, style: { height: entryHeights[flatEntries.indexOf(entry)] }, children: _jsx("td", { colSpan: tableColumnCount, children: _jsxs("button", { type: "button", onClick: () => toggleGroup(entry.node.key), "aria-expanded": !collapsed.has(entry.node.key), children: [collapsed.has(entry.node.key) ? "›" : "⌄", groupLabel(entry.node)] }) }) }, `group:${entry.node.key}`)), virtual && virtualEnd < flatEntries.length && _jsx("tr", { "aria-hidden": true, children: _jsx("td", { colSpan: tableColumnCount, style: { height: windowed.after, padding: 0, border: 0 } }) })] })] }) }) }), _jsx(VirtualList, { items: flatEntries, itemKey: (e) => e.kind === "group" ? `g:${e.node.key}` : getRowId(e.row), estimate: (e) => e.kind === "group" ? 40 : activeView.mobile?.presentation === "compact" ? 80 + mobileFields.length * 28 : 100 + mobileFields.length * 36, className: `hy-dv-mobile mobile-${activeView.mobile?.presentation ?? "cards"}`, initialTop: activeView.location?.mobileTop, onScroll: (el) => { loadNearEnd(el); patchLocation({ mobileTop: el.scrollTop }); }, renderItem: (entry) => entry.kind === "group" ? _jsxs("button", { "data-group-key": entry.node.key, className: "hy-dv-mobile-group-title", "aria-expanded": !collapsed.has(entry.node.key), onClick: () => toggleGroup(entry.node.key), children: [collapsed.has(entry.node.key) ? "›" : "⌄", groupLabel(entry.node)] }) : renderMobileCard(entry.row) }), _jsxs("p", { className: "hy-dv-keyboard-help", children: ["\u65B9\u5411\u9375\u79FB\u52D5 \u00B7 Shift\uFF0B\u65B9\u5411\u9375\u9078\u7BC4\u570D \u00B7 Enter \u7DE8\u8F2F \u00B7 Ctrl/Cmd\uFF0BC\uFF0FV \u8907\u88FD\u8CBC\u4E0A \u00B7 Delete \u6E05\u7A7A\u9810\u89BD", virtual ? ` · 連續捲動,共 ${desktopRows.length} 筆` : ""] }), props.bottomScrollbar !== false && tableWidth > viewportWidth && _jsx("div", { ref: bottomRef, className: "hy-dv-bottom-scroll", "aria-label": "\u8868\u683C\u6C34\u5E73\u6372\u52D5", onScroll: (e) => { if (scrollRef.current)
|
|
716
|
-
scrollRef.current.scrollLeft = e.currentTarget.scrollLeft; }, children: _jsx("div", { style: { width: tableWidth, height: 1 } }) })] }) }), props.remote && _jsxs("div", { className: "hy-dv-load-status", role: props.remote.error ? "alert" : "status", children: [props.remote.error || (props.remote.loading || remoteStale ? "正在載入符合條件的資料…" : props.remote.complete ? `已完整載入 ${processedRows.length} 筆` : `尚未完整:已載入 ${processedRows.length}/${props.remote.total ?? "…"} 筆`), !props.remote.complete && !props.remote.loading && !remoteStale && _jsx("button", { onClick: props.remote.error ? props.remote.onRetry : props.remote.onLoadMore, children: props.remote.error ? "重新查詢" : "繼續載入" })] }),
|
|
849
|
+
scrollRef.current.scrollLeft = e.currentTarget.scrollLeft; }, children: _jsx("div", { style: { width: tableWidth, height: 1 } }) })] }) }), props.remote && _jsxs("div", { className: "hy-dv-load-status", role: props.remote.error ? "alert" : "status", children: [props.remote.error || (props.remote.loading || remoteStale ? "正在載入符合條件的資料…" : props.remote.complete ? `已完整載入 ${processedRows.length} 筆` : `尚未完整:已載入 ${processedRows.length}/${props.remote.total ?? "…"} 筆`), !props.remote.complete && !props.remote.loading && !remoteStale && _jsx("button", { onClick: props.remote.error ? props.remote.onRetry : props.remote.onLoadMore, children: props.remote.error ? "重新查詢" : "繼續載入" })] }), attachmentTarget && (() => {
|
|
850
|
+
const row = rows.find(row => getRowId(row) === attachmentTarget.rowId);
|
|
851
|
+
const field = fields.find(field => field.id === attachmentTarget.fieldId);
|
|
852
|
+
return row && field && field.type === "attachment" && !field.internal && canRead(row, field) && _jsx(DataViewDialog, { title: `${readableLabel(row)} · ${field.label}`, busy: busy, onClose: () => { setAttachmentTarget(null); setEditing(null); }, children: renderCell(row, field, "detail") });
|
|
853
|
+
})(), quick && (() => { const row = rows.find((r) => getRowId(r) === quick.rowId); const field = fields.find((f) => f.id === quick.fieldId); return row && field && !field.internal && canRead(row, field) && _jsxs(DataViewDialog, { title: field.label, onClose: () => setQuick(null), children: [field.renderQuickView?.({ row, field, value: getFieldValue(row, field), close: () => setQuick(null), openRow: () => { setQuick(null); openRow(row); } }), canEdit(row, field) && _jsx("button", { onClick: () => { setQuick(null); setDetailId(getRowId(row)); setEditing({ rowId: getRowId(row), fieldId: field.id, surface: "detail" }); }, children: "\u7DE8\u8F2F" })] }); })(), detailRow && _jsxs(DataViewDialog, { title: readableLabel(detailRow), busy: busy, onClose: () => { setDetailId(null); setEditing(null); }, children: [props.renderDetail ? props.renderDetail(detailContext(detailRow)) : _jsx("div", { className: "hy-dv-detail", children: availableFields.filter((field) => canRead(detailRow, field)).map((field) => _jsxs("label", { children: [_jsx("span", { children: field.label }), renderCell(detailRow, field, "detail")] }, field.id)) }), features.copy !== false && _jsx("button", { onClick: async () => { try {
|
|
717
854
|
await navigator.clipboard.writeText(formatReadableRow(detailRow, availableFields));
|
|
718
855
|
setOperation({ state: "saved", message: "已複製可讀內容" });
|
|
719
856
|
}
|
|
720
857
|
catch {
|
|
721
858
|
reportFailure(new Error("無法使用剪貼簿,請確認瀏覽器權限"));
|
|
722
|
-
} }, children: "\u8907\u88FD\u6587\u5B57" }), props.renderDetailFooter?.(detailContext(detailRow)), operation.state !== "idle" && _jsx("p", { role: "status", children: operation.message })] }),
|
|
859
|
+
} }, children: "\u8907\u88FD\u6587\u5B57" }), props.renderDetailFooter?.(detailContext(detailRow)), operation.state !== "idle" && _jsx("p", { role: "status", children: operation.message })] }), rowMenu && rowMenuEnabled && (() => {
|
|
860
|
+
const row = rows.find(row => getRowId(row) === rowMenu.rowId);
|
|
861
|
+
return row && _jsx(ActionMenu, { id: rowMenuId, label: `${readableLabel(row)}資料列操作`, anchor: rowMenu.anchor, point: rowMenu.point, items: rowActionItems(row, permissions, rowOperationBlocked()), onClose: () => setRowMenu(null), onChoose: action => {
|
|
862
|
+
if (rowOperationBlocked() || permissions?.canChangeRow?.(row, action) !== true)
|
|
863
|
+
return;
|
|
864
|
+
setRowMenu(null);
|
|
865
|
+
if (action === "delete") {
|
|
866
|
+
setOperation({ state: "idle", message: "" });
|
|
867
|
+
setRowDelete(rowMenu);
|
|
868
|
+
}
|
|
869
|
+
else
|
|
870
|
+
void applyRowChange(action, rowMenu);
|
|
871
|
+
} });
|
|
872
|
+
})(), rowDelete && (() => {
|
|
873
|
+
const row = rows.find(row => getRowId(row) === rowDelete.rowId);
|
|
874
|
+
return row && _jsxs(DataViewDialog, { title: `刪除此筆:${readableLabel(row)}`, busy: busy, onClose: () => { rowDelete.anchor.focus(); setRowDelete(null); }, children: [_jsxs("p", { children: ["\u78BA\u8A8D\u522A\u9664\u300C", readableLabel(row), "\u300D\uFF1F\u53EA\u522A\u9664\u6B64\u7B46\uFF0C\u4E0D\u5305\u542B\u5176\u4ED6\u5DF2\u9078\u53D6\u8CC7\u6599\u3002\u522A\u9664\u4E0D\u7D0D\u5165\u5132\u5B58\u683C\u5FA9\u539F\u3002"] }), _jsx("button", { type: "button", autoFocus: true, disabled: busy, onClick: () => { rowDelete.anchor.focus(); setRowDelete(null); }, children: "\u53D6\u6D88" }), _jsx("button", { type: "button", className: "hy-dv-danger", disabled: rowOperationBlocked() || permissions?.canChangeRow?.(row, "delete") !== true, onClick: () => void applyRowChange("delete", rowDelete), children: "\u78BA\u8A8D\u522A\u9664\u6B64\u7B46" }), (operation.state === "failed" || operation.state === "conflict") && _jsx("p", { role: "alert", children: operation.message })] });
|
|
875
|
+
})(), headerMenu && features.columnMenu !== false && (() => {
|
|
723
876
|
const field = availableFields.find((item) => item.id === headerMenu.fieldId);
|
|
724
877
|
return field && _jsx(ColumnMenu, { id: menuId, label: field.label, anchor: headerMenu.anchor, items: headerItems({ field, fields, view: activeView, permissions, features, hasFieldCallback: !!onFieldChange, busy }), onChoose: (action) => chooseHeaderAction(field, action), onClose: () => setHeaderMenu(null) });
|
|
725
878
|
})(), fieldAction && (() => {
|
|
@@ -748,11 +901,11 @@ function DataViewInner(props) {
|
|
|
748
901
|
const rowId = getRowId(row);
|
|
749
902
|
const index = desktopRowIndices.get(rowId) ?? 0;
|
|
750
903
|
const colors = colorMap.get(rowId);
|
|
751
|
-
return _jsxs("tr", { "data-move-row": rowId, "aria-rowindex": index + 2, className: `${selectedSet.has(rowId) ? "is-selected" : ""} ${move.sourceId === rowId ? "is-moving" : ""} ${move.target?.targetRowId === rowId ? `is-drop-${move.target.position}` : ""}`, style: { height: rowHeight }, children: [controlColumn && _jsx("td", { className: "hy-dv-selection is-pinned", style: { left: 0 }, children: _jsxs("div", { className: "hy-dv-row-controls", children: [props.rowMove && _jsx("button", { className: "hy-dv-row-drag", "aria-label": `移動 ${readableLabel(row)}`, disabled: move.pending || props.rowMove.canMove?.(row) === false, onPointerDown: (e) => move.start(rowId, e), onKeyDown: (e) => move.key(rowId, e), children: "\u283F" }), selectable && _jsx("input", { type: "checkbox", checked: selected.includes(rowId), onChange: () => toggleRow(rowId), "aria-label": `選取資料 ${readableLabel(row)}` }), features.rowOpen === true && _jsx("button", { "aria-label": `開啟 ${readableLabel(row)}`, onClick: () => openRow(row), children: "\u2197" })] }) }), visible.map(({ field }, col) => {
|
|
904
|
+
return _jsxs("tr", { "data-move-row": rowId, tabIndex: rowMenuEnabled ? -1 : undefined, onContextMenu: event => rowContextEvent(row, event), onKeyDown: event => rowContextEvent(row, event), "aria-rowindex": index + 2, className: `${selectedSet.has(rowId) ? "is-selected" : ""} ${move.sourceId === rowId ? "is-moving" : ""} ${move.target?.targetRowId === rowId ? `is-drop-${move.target.position}` : ""}`, style: { height: rowHeight }, children: [controlColumn && _jsx("td", { className: "hy-dv-selection is-pinned", style: { left: 0 }, children: _jsxs("div", { className: "hy-dv-row-controls", children: [rowMenuTrigger(row), props.rowMove && _jsx("button", { className: "hy-dv-row-drag", "aria-label": `移動 ${readableLabel(row)}`, disabled: move.pending || props.rowMove.canMove?.(row) === false, onPointerDown: (e) => move.start(rowId, e), onKeyDown: (e) => move.key(rowId, e), children: "\u283F" }), selectable && _jsx("input", { type: "checkbox", checked: selected.includes(rowId), onChange: () => toggleRow(rowId), "aria-label": `選取資料 ${readableLabel(row)}` }), features.rowOpen === true && _jsx("button", { "aria-label": `開啟 ${readableLabel(row)}`, onClick: () => openRow(row), children: "\u2197" })] }) }), visible.map(({ field }, col) => {
|
|
752
905
|
const left = pinnedOffsets.get(field.id);
|
|
753
906
|
const right = rightOffsets.get(field.id);
|
|
754
907
|
const inRange = range && index >= range.fromRow && index <= range.toRow && col >= range.fromCol && col <= range.toCol;
|
|
755
|
-
return _jsx("td", { "data-hy-cell": true, "data-row-id": rowId, "data-field-id": field.id, tabIndex: range && focus ? focus.rowId === rowId && focus.fieldId === field.id ? 0 : -1 : index === 0 && col === 0 ? 0 : -1, "aria-label": `${
|
|
908
|
+
return _jsx("td", { "data-hy-cell": true, "data-row-id": rowId, "data-field-id": field.id, tabIndex: range && focus ? focus.rowId === rowId && focus.fieldId === field.id ? 0 : -1 : index === 0 && col === 0 ? 0 : -1, "aria-label": `${readableLabel(row)} ${field.label}`, onFocus: () => setFocus({ rowId, fieldId: field.id }), onClick: (event) => { if (event.shiftKey) {
|
|
756
909
|
setAnchor(anchor ?? focus);
|
|
757
910
|
setFocus({ rowId, fieldId: field.id });
|
|
758
911
|
} }, className: `${left === undefined && right === undefined ? "" : "is-pinned"} ${inRange ? "is-range" : ""}`, style: { left, right, backgroundColor: colors?.get(field.id) ?? colors?.get("row") }, children: _jsx("div", { className: "hy-dv-cell-content", children: renderCell(row, field, "desktop") }) }, field.id);
|
|
@@ -780,15 +933,21 @@ function DataViewInner(props) {
|
|
|
780
933
|
function renderColumnHeader(field, column, gantt = false) {
|
|
781
934
|
const left = (gantt ? ganttLeftOffsets : pinnedOffsets).get(field.id);
|
|
782
935
|
const right = (gantt ? ganttRightOffsets : rightOffsets).get(field.id);
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
936
|
+
const hasMenu = features.columnMenu !== false && (configurable || canManageFields || canFilter || canSort);
|
|
937
|
+
const sortIndex = activeView.sorts.findIndex(rule => rule.fieldId === field.id);
|
|
938
|
+
const heading = _jsxs(_Fragment, { children: [_jsx("span", { className: "hy-dv-column-title", children: field.label }), sortIndex >= 0 && _jsxs("small", { children: [sortIndex + 1, activeView.sorts[sortIndex].direction === "asc" ? "↑" : "↓"] })] });
|
|
939
|
+
return _jsxs("th", { draggable: configurable, onDragStart: () => { headerDragUntil.current = Infinity; setDraggedColumn(field.id); }, onDragEnd: () => { headerDragUntil.current = Date.now() + 100; setDraggedColumn(null); }, onDragOver: event => event.preventDefault(), onDrop: () => dropColumn(field.id), className: `${left === undefined && right === undefined ? "" : "is-pinned"} ${hasMenu ? "has-column-menu" : ""}`, style: { left, right }, children: [hasMenu ? _jsx("button", { type: "button", className: "hy-dv-column-heading hy-dv-header-menu-trigger", "aria-label": `${field.label}欄位操作`, title: field.label, "aria-haspopup": "menu", "aria-expanded": headerMenu?.fieldId === field.id, "aria-controls": headerMenu?.fieldId === field.id ? menuId : undefined, draggable: configurable, onClick: event => { event.stopPropagation(); if (Date.now() < headerDragUntil.current)
|
|
940
|
+
return; setRowMenu(null); setHeaderMenu(headerMenu?.fieldId === field.id ? null : { fieldId: field.id, anchor: event.currentTarget }); }, onKeyDown: event => { if (event.key === "ArrowDown") {
|
|
941
|
+
event.preventDefault();
|
|
942
|
+
event.stopPropagation();
|
|
943
|
+
setRowMenu(null);
|
|
944
|
+
setHeaderMenu({ fieldId: field.id, anchor: event.currentTarget });
|
|
945
|
+
} }, children: heading })
|
|
946
|
+
: _jsx("div", { className: "hy-dv-column-heading", title: field.label, children: heading }), configurable && _jsx("span", { className: "hy-dv-resize", onPointerDown: event => { event.stopPropagation(); resizeColumn(field.id, column.width, event); }, onClick: event => event.stopPropagation(), "aria-hidden": true })] }, field.id);
|
|
788
947
|
}
|
|
789
948
|
function renderMobileCard(row) {
|
|
790
949
|
const rowId = getRowId(row);
|
|
791
|
-
return _jsxs("article", { "data-move-row": rowId, className: `hy-dv-card ${selected.includes(rowId) ? "is-selected" : ""}`, style: { backgroundColor: colorMap.get(rowId)?.get("row") }, children: [selectable && _jsxs("label", { className: "hy-dv-card-select", children: [_jsx("input", { type: "checkbox", checked: selected.includes(rowId), onChange: () => toggleRow(rowId) }), " \u9078\u53D6"] }), activeView.mobile?.summaryFieldId && _jsx("button", { className: "hy-dv-mobile-summary", onClick: () => openRow(row), children: readableLabel(row) }), props.renderMobileRow ? props.renderMobileRow(detailContext(row)) : _jsx("dl", { children: mobileFields.map((field) => canRead(row, field) && _jsxs(Fragment, { children: [_jsx("dt", { children: field.label }), _jsx("dd", { style: { backgroundColor: colorMap.get(rowId)?.get(field.id) }, children: renderCell(row, field, "mobile") })] }, field.id)) }), props.rowMove && _jsx("button", { className: "hy-dv-row-drag", "aria-label": `移動 ${readableLabel(row)}`, disabled: move.pending || props.rowMove.canMove?.(row) === false, onPointerDown: (e) => move.start(rowId, e), onKeyDown: (e) => move.key(rowId, e), children: "\u283F" }), _jsx("button", { className: "hy-dv-detail-button", onClick: () => openRow(row), children: "\u67E5\u770B\u8A73\u60C5" })] }, rowId);
|
|
950
|
+
return _jsxs("article", { "data-move-row": rowId, tabIndex: rowMenuEnabled ? -1 : undefined, onContextMenu: event => rowContextEvent(row, event), onKeyDown: event => rowContextEvent(row, event), className: `hy-dv-card ${selected.includes(rowId) ? "is-selected" : ""}`, style: { backgroundColor: colorMap.get(rowId)?.get("row") }, children: [rowMenuTrigger(row), selectable && _jsxs("label", { className: "hy-dv-card-select", children: [_jsx("input", { type: "checkbox", checked: selected.includes(rowId), onChange: () => toggleRow(rowId) }), " \u9078\u53D6"] }), activeView.mobile?.summaryFieldId && _jsx("button", { className: "hy-dv-mobile-summary", onClick: () => openRow(row), children: readableLabel(row) }), props.renderMobileRow ? props.renderMobileRow(detailContext(row)) : _jsx("dl", { children: mobileFields.map((field) => canRead(row, field) && _jsxs(Fragment, { children: [_jsx("dt", { children: field.label }), _jsx("dd", { style: { backgroundColor: colorMap.get(rowId)?.get(field.id) }, children: renderCell(row, field, "mobile") })] }, field.id)) }), props.rowMove && _jsx("button", { className: "hy-dv-row-drag", "aria-label": `移動 ${readableLabel(row)}`, disabled: move.pending || props.rowMove.canMove?.(row) === false, onPointerDown: (e) => move.start(rowId, e), onKeyDown: (e) => move.key(rowId, e), children: "\u283F" }), _jsx("button", { className: "hy-dv-detail-button", onClick: () => openRow(row), children: "\u67E5\u770B\u8A73\u60C5" })] }, rowId);
|
|
792
951
|
}
|
|
793
952
|
}
|
|
794
953
|
function ToolbarButton({ children, active = false, onClick }) {
|