@keenaioc/data-view 0.2.0-alpha.11 → 0.2.0-alpha.14

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.
Files changed (51) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/INTEGRATION.md +85 -0
  3. package/KNOWN-LIMITATIONS.md +19 -0
  4. package/README.md +4 -2
  5. package/dist/action-menu.d.ts +37 -0
  6. package/dist/action-menu.d.ts.map +1 -0
  7. package/dist/action-menu.js +64 -0
  8. package/dist/action-menu.js.map +1 -0
  9. package/dist/column-menu.d.ts +2 -16
  10. package/dist/column-menu.d.ts.map +1 -1
  11. package/dist/column-menu.js +5 -62
  12. package/dist/column-menu.js.map +1 -1
  13. package/dist/data-view.d.ts.map +1 -1
  14. package/dist/data-view.js +140 -24
  15. package/dist/data-view.js.map +1 -1
  16. package/dist/dialog.d.ts +8 -0
  17. package/dist/dialog.d.ts.map +1 -0
  18. package/dist/dialog.js +10 -0
  19. package/dist/dialog.js.map +1 -0
  20. package/dist/gantt-grid.d.ts +2 -1
  21. package/dist/gantt-grid.d.ts.map +1 -1
  22. package/dist/gantt-grid.js +3 -3
  23. package/dist/gantt-grid.js.map +1 -1
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.js +1 -1
  26. package/dist/registry.d.ts +1 -4
  27. package/dist/registry.d.ts.map +1 -1
  28. package/dist/registry.js +1 -17
  29. package/dist/registry.js.map +1 -1
  30. package/dist/relation-picker.d.ts +21 -0
  31. package/dist/relation-picker.d.ts.map +1 -0
  32. package/dist/relation-picker.js +84 -0
  33. package/dist/relation-picker.js.map +1 -0
  34. package/dist/row-actions.d.ts +11 -0
  35. package/dist/row-actions.d.ts.map +1 -0
  36. package/dist/row-actions.js +15 -0
  37. package/dist/row-actions.js.map +1 -0
  38. package/dist/schedule-view.d.ts +2 -1
  39. package/dist/schedule-view.d.ts.map +1 -1
  40. package/dist/schedule-view.js +2 -2
  41. package/dist/schedule-view.js.map +1 -1
  42. package/dist/style.css +29 -3
  43. package/dist/types.d.ts +26 -2
  44. package/dist/types.d.ts.map +1 -1
  45. package/dist/v2-panels.d.ts +1 -8
  46. package/dist/v2-panels.d.ts.map +1 -1
  47. package/dist/v2-panels.js +2 -7
  48. package/dist/v2-panels.js.map +1 -1
  49. package/dist/view-workspace.js +2 -2
  50. package/dist/view-workspace.js.map +1 -1
  51. 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 controlColumn = selectable || !!props.rowMove || features.rowOpen === true;
106
- const controlWidth = (selectable ? 32 : 0) + (props.rowMove ? 24 : 0) + (features.rowOpen === true ? 28 : 0);
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");
@@ -149,13 +158,23 @@ function DataViewInner(props) {
149
158
  const query = useMemo(() => createDataViewQuery(activeView), [activeView.filters, activeView.sorts, activeView.groups, search]);
150
159
  const queryKey = dataViewQueryKey(query);
151
160
  const remoteStale = !!props.remote && props.remote.queryKey !== queryKey;
161
+ const rowContext = useRef({ props, query, queryKey, selected });
162
+ rowContext.current = { props, query, queryKey, selected };
163
+ useEffect(() => {
164
+ const unavailable = (target) => target && (target.queryKey !== queryKey || !rowMenuEnabled || access !== "granted" || loading || error || !rows.some(row => getRowId(row) === target.rowId));
165
+ if (unavailable(rowMenu))
166
+ setRowMenu(null);
167
+ if (unavailable(rowDelete))
168
+ setRowDelete(null);
169
+ }, [rows, queryKey, rowMenuEnabled, access, loading, error, rowMenu, rowDelete, getRowId]);
170
+ useEffect(() => { setRowMenu(null); setRowDelete(null); }, [props.presentation, activeView.viewType]);
152
171
  const queryCallback = useRef(props.onQueryChange);
153
172
  queryCallback.current = props.onQueryChange;
154
173
  const previousQuery = useRef(queryKey);
155
174
  useEffect(() => { if (access === "granted" && !loading && !error)
156
175
  queryCallback.current?.(query); }, [queryKey, props.onQueryChange, access, loading, error]);
157
176
  useEffect(() => { if (previousQuery.current === queryKey)
158
- return; previousQuery.current = queryKey; setLocalSelected([]); onSelectionChange?.([]); setFocus(null); setAnchor(null); setEditing(null); setUndoStack([]); setRedoStack([]); }, [queryKey]);
177
+ return; previousQuery.current = queryKey; setLocalSelected([]); onSelectionChange?.([]); setFocus(null); setAnchor(null); setEditing(null); setUndoStack([]); setRedoStack([]); setAttachmentTarget(null); }, [queryKey]);
159
178
  const canFilter = permissions?.canFilter ?? configurable;
160
179
  const canSort = permissions?.canSort ?? configurable;
161
180
  const processedRows = useMemo(() => {
@@ -169,8 +188,8 @@ function DataViewInner(props) {
169
188
  const groups = fullGroups;
170
189
  const visible = useMemo(() => visibleFields(fields, activeView), [activeView, fields]);
171
190
  const tableColumnCount = visible.length + (controlColumn ? 1 : 0) + (canManageFields ? 1 : 0);
172
- const pinnedOffsets = useMemo(() => getPinnedOffsets(activeView.columns, controlWidth), [activeView.columns, selectable]);
173
- const ganttLeftOffsets = useMemo(() => getPinnedOffsets(activeView.columns, 44), [activeView.columns]);
191
+ const pinnedOffsets = useMemo(() => getPinnedOffsets(activeView.columns, controlWidth), [activeView.columns, controlWidth]);
192
+ const ganttLeftOffsets = useMemo(() => getPinnedOffsets(activeView.columns, 44 + (rowMenuEnabled ? 28 : 0)), [activeView.columns, rowMenuEnabled]);
174
193
  const ganttRightOffsets = useMemo(() => getRightPinnedOffsets(activeView.columns), [activeView.columns]);
175
194
  const tableWidth = visible.reduce((total, item) => total + item.column.width, (controlWidth) + addColumnWidth);
176
195
  const collapsed = useMemo(() => new Set(activeView.collapsedGroups), [activeView.collapsedGroups]);
@@ -198,7 +217,7 @@ function DataViewInner(props) {
198
217
  const field = fields.find((f) => f.id === id) ?? availableFields.find((f) => !f.renderAction && canRead(row, f));
199
218
  if (field && (field.internal || !canRead(row, field)))
200
219
  return "無權限";
201
- return props.getRowLabel?.(row)?.trim() || (field ? formatReadableValue(getFieldValue(row, field), { ...field, internal: false }, row) : "") || "未命名工作";
220
+ return props.getRowLabel?.(row)?.trim() || (field ? formatReadableValue(getFieldValue(row, field), { ...field, internal: false }, row).trim() : "") || "未命名工作";
202
221
  }
203
222
  function openRow(row) { if (features.rowOpen === false)
204
223
  return; if (props.onRowOpen)
@@ -393,9 +412,13 @@ function DataViewInner(props) {
393
412
  }
394
413
  if (field.type === "relation" && !field.Editor && !field.renderQuickView && !field.render && canWriteField(row, field, permissions) && (onBatchCommit || onCellCommit))
395
414
  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 })] });
415
+ if (field.type === "attachment" && surface !== "detail" && !field.render && !field.Editor && !field.renderQuickView) {
416
+ const count = spec.isEmpty(value) ? 0 : Array.isArray(value) ? value.length : 1;
417
+ 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 })] });
418
+ }
396
419
  if (field.type === "attachment")
397
420
  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 ? `${rowId} ${field.label}` : undefined, "aria-busy": status?.state === "saving", tabIndex: surface === "desktop" ? -1 : undefined, onClick: (event) => {
421
+ 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
422
  if (event.shiftKey && surface === "desktop")
400
423
  return;
401
424
  setFocus({ rowId, fieldId: field.id });
@@ -458,8 +481,8 @@ function DataViewInner(props) {
458
481
  if (validation)
459
482
  throw new Error(validation);
460
483
  setOperation({ state: "saving", message: `正在儲存 ${changes.length} 格` });
461
- await onBatchCommit({ ...request, changes });
462
- if (features.undo !== false && request.reason !== "undo" && request.reason !== "redo") {
484
+ const result = await onBatchCommit({ ...request, changes });
485
+ if (features.undo !== false && request.reason !== "undo" && request.reason !== "redo" && result?.history !== "skip") {
463
486
  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
487
  setRedoStack([]);
465
488
  }
@@ -487,6 +510,76 @@ function DataViewInner(props) {
487
510
  setDialog("review");
488
511
  setEditing(null);
489
512
  }
513
+ function rowOperationBlocked() {
514
+ const current = rowContext.current;
515
+ 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);
516
+ }
517
+ function showRowMenu(row, anchor, point) {
518
+ if (rowOperationBlocked())
519
+ return;
520
+ setHeaderMenu(null);
521
+ setRowDelete(null);
522
+ setRowMenu({ rowId: getRowId(row), anchor, point, queryKey });
523
+ }
524
+ function rowContextEvent(row, event) {
525
+ if (!rowMenuEnabled || rowOperationBlocked() || event.defaultPrevented)
526
+ return;
527
+ if (event.type !== "contextmenu" && !("key" in event && (event.key === "ContextMenu" || event.key === "F10" && event.shiftKey)))
528
+ return;
529
+ const target = event.target;
530
+ if (target.closest?.("input,textarea,select,a,[contenteditable]:not([contenteditable='false']),.hy-dv-action-cell,.hy-dv-editor-wrap"))
531
+ return;
532
+ event.preventDefault();
533
+ event.stopPropagation();
534
+ const anchor = target.closest?.("[data-hy-cell]") ?? event.currentTarget;
535
+ showRowMenu(row, anchor, event.type === "contextmenu" && "clientX" in event ? { x: event.clientX, y: event.clientY } : undefined);
536
+ }
537
+ function rowMenuTrigger(row) {
538
+ 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" });
539
+ }
540
+ async function applyRowChange(action, target) {
541
+ if (rowOperationBlocked())
542
+ return false;
543
+ busyRef.current = true;
544
+ setOperation({ state: "saving", message: action === "delete" ? "正在刪除此筆…" : "正在新增一筆…" });
545
+ try {
546
+ await writes.current.run(["*"], async () => {
547
+ const current = rowContext.current;
548
+ const row = current.props.rows.find(row => current.props.getRowId(row) === target.rowId);
549
+ if (target.queryKey !== current.queryKey || current.props.remote && current.props.remote.queryKey !== current.queryKey)
550
+ throw new DataViewConflictError("查詢已變更,請重新選擇資料列");
551
+ if (!row)
552
+ throw new DataViewConflictError("資料列已不存在,請重新選擇");
553
+ 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)
554
+ throw new Error("沒有此列操作權限或資料仍在載入");
555
+ const path = rowGroupPaths([row], current.props.fields, current.query.groups, current.props.getRowId).rowPaths.get(target.rowId) ?? [];
556
+ if (action !== "delete" && (path.length !== current.query.groups.length || path.some(p => p.valueKey === "__restricted__")))
557
+ throw new Error("無法在不可讀的分組新增資料");
558
+ await current.props.onRowChange({ id: newOperationId(), action, row, rowId: target.rowId, groupPath: path, query: structuredClone(current.query) });
559
+ });
560
+ if (action === "delete") {
561
+ setUndoStack(stack => retainHistoryAfterRowDelete(stack, target.rowId));
562
+ setRedoStack(stack => retainHistoryAfterRowDelete(stack, target.rowId));
563
+ changeSelection(rowContext.current.selected.filter(id => id !== target.rowId));
564
+ setFocus(current => current?.rowId === target.rowId ? null : current);
565
+ setAnchor(current => current?.rowId === target.rowId ? null : current);
566
+ setEditing(current => current?.rowId === target.rowId ? null : current);
567
+ setDetailId(current => current === target.rowId ? null : current);
568
+ setAttachmentTarget(current => current?.rowId === target.rowId ? null : current);
569
+ setQuick(current => current?.rowId === target.rowId ? null : current);
570
+ setRowDelete(null);
571
+ }
572
+ setOperation({ state: "saved", message: action === "delete" ? "已刪除此筆" : "已新增一筆;顯示位置依目前排序、分組與篩選" });
573
+ return true;
574
+ }
575
+ catch (error) {
576
+ reportFailure(error);
577
+ return false;
578
+ }
579
+ finally {
580
+ busyRef.current = false;
581
+ }
582
+ }
490
583
  async function applyFieldChange(change) {
491
584
  if (!onFieldChange || busyRef.current || latest.current.access !== "granted")
492
585
  return false;
@@ -631,10 +724,7 @@ function DataViewInner(props) {
631
724
  const relationSelect = event.target.closest("td")?.querySelector(".hy-dv-relation-select");
632
725
  if (relationSelect && !relationSelect.disabled) {
633
726
  relationSelect.focus();
634
- try {
635
- relationSelect.showPicker?.();
636
- }
637
- catch { /* Focus still permits native keyboard selection. */ }
727
+ relationSelect.click();
638
728
  return;
639
729
  }
640
730
  const toggleValue = getFieldTypeDefinition(field.type).toggleValue;
@@ -714,15 +804,35 @@ function DataViewInner(props) {
714
804
  }
715
805
  const range = gridRange();
716
806
  const desktopRowIndices = new Map(desktopRows.map((row, index) => [getRowId(row), index]));
717
- 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)
807
+ 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)
718
808
  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)
719
- 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 ? "重新查詢" : "繼續載入" })] }), 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 {
809
+ 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 && (() => {
810
+ const row = rows.find(row => getRowId(row) === attachmentTarget.rowId);
811
+ const field = fields.find(field => field.id === attachmentTarget.fieldId);
812
+ 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") });
813
+ })(), 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 {
720
814
  await navigator.clipboard.writeText(formatReadableRow(detailRow, availableFields));
721
815
  setOperation({ state: "saved", message: "已複製可讀內容" });
722
816
  }
723
817
  catch {
724
818
  reportFailure(new Error("無法使用剪貼簿,請確認瀏覽器權限"));
725
- } }, children: "\u8907\u88FD\u6587\u5B57" }), props.renderDetailFooter?.(detailContext(detailRow)), operation.state !== "idle" && _jsx("p", { role: "status", children: operation.message })] }), headerMenu && features.columnMenu !== false && (() => {
819
+ } }, children: "\u8907\u88FD\u6587\u5B57" }), props.renderDetailFooter?.(detailContext(detailRow)), operation.state !== "idle" && _jsx("p", { role: "status", children: operation.message })] }), rowMenu && rowMenuEnabled && (() => {
820
+ const row = rows.find(row => getRowId(row) === rowMenu.rowId);
821
+ 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 => {
822
+ if (rowOperationBlocked() || permissions?.canChangeRow?.(row, action) !== true)
823
+ return;
824
+ setRowMenu(null);
825
+ if (action === "delete") {
826
+ setOperation({ state: "idle", message: "" });
827
+ setRowDelete(rowMenu);
828
+ }
829
+ else
830
+ void applyRowChange(action, rowMenu);
831
+ } });
832
+ })(), rowDelete && (() => {
833
+ const row = rows.find(row => getRowId(row) === rowDelete.rowId);
834
+ 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 })] });
835
+ })(), headerMenu && features.columnMenu !== false && (() => {
726
836
  const field = availableFields.find((item) => item.id === headerMenu.fieldId);
727
837
  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) });
728
838
  })(), fieldAction && (() => {
@@ -751,11 +861,11 @@ function DataViewInner(props) {
751
861
  const rowId = getRowId(row);
752
862
  const index = desktopRowIndices.get(rowId) ?? 0;
753
863
  const colors = colorMap.get(rowId);
754
- 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) => {
864
+ 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) => {
755
865
  const left = pinnedOffsets.get(field.id);
756
866
  const right = rightOffsets.get(field.id);
757
867
  const inRange = range && index >= range.fromRow && index <= range.toRow && col >= range.fromCol && col <= range.toCol;
758
- 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": `${rowId} ${field.label}`, onFocus: () => setFocus({ rowId, fieldId: field.id }), onClick: (event) => { if (event.shiftKey) {
868
+ 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) {
759
869
  setAnchor(anchor ?? focus);
760
870
  setFocus({ rowId, fieldId: field.id });
761
871
  } }, 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);
@@ -783,15 +893,21 @@ function DataViewInner(props) {
783
893
  function renderColumnHeader(field, column, gantt = false) {
784
894
  const left = (gantt ? ganttLeftOffsets : pinnedOffsets).get(field.id);
785
895
  const right = (gantt ? ganttRightOffsets : rightOffsets).get(field.id);
786
- return _jsxs("th", { draggable: configurable, onDragStart: () => setDraggedColumn(field.id), onDragOver: (event) => event.preventDefault(), onDrop: () => dropColumn(field.id), className: left === undefined && right === undefined ? "" : "is-pinned", style: { left, right }, children: [_jsxs("div", { className: "hy-dv-column-heading", children: [_jsx("span", { className: "hy-dv-column-title", children: field.label }), activeView.sorts.findIndex((rule) => rule.fieldId === field.id) >= 0 && _jsxs("small", { children: [activeView.sorts.findIndex((rule) => rule.fieldId === field.id) + 1, activeView.sorts.find((rule) => rule.fieldId === field.id)?.direction === "asc" ? "↑" : "↓"] }), features.columnMenu !== false && (configurable || canManageFields || canFilter || canSort) && _jsx("button", { type: "button", className: "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: false, onDragStart: (event) => { event.preventDefault(); event.stopPropagation(); }, onPointerDown: (event) => event.stopPropagation(), onClick: (event) => { event.stopPropagation(); setHeaderMenu(headerMenu?.fieldId === field.id ? null : { fieldId: field.id, anchor: event.currentTarget }); }, onKeyDown: (event) => { if (event.key === "ArrowDown") {
787
- event.preventDefault();
788
- event.stopPropagation();
789
- setHeaderMenu({ fieldId: field.id, anchor: event.currentTarget });
790
- } }, children: "\u2304" })] }), configurable && _jsx("span", { className: "hy-dv-resize", onPointerDown: (event) => resizeColumn(field.id, column.width, event), "aria-hidden": true })] }, field.id);
896
+ const hasMenu = features.columnMenu !== false && (configurable || canManageFields || canFilter || canSort);
897
+ const sortIndex = activeView.sorts.findIndex(rule => rule.fieldId === field.id);
898
+ 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" ? "↑" : "↓"] })] });
899
+ 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)
900
+ return; setRowMenu(null); setHeaderMenu(headerMenu?.fieldId === field.id ? null : { fieldId: field.id, anchor: event.currentTarget }); }, onKeyDown: event => { if (event.key === "ArrowDown") {
901
+ event.preventDefault();
902
+ event.stopPropagation();
903
+ setRowMenu(null);
904
+ setHeaderMenu({ fieldId: field.id, anchor: event.currentTarget });
905
+ } }, children: heading })
906
+ : _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);
791
907
  }
792
908
  function renderMobileCard(row) {
793
909
  const rowId = getRowId(row);
794
- 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);
910
+ 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);
795
911
  }
796
912
  }
797
913
  function ToolbarButton({ children, active = false, onClick }) {