@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/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");
@@ -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, selectable]);
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
- setView({
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], () => onRequestAttachment({ row, rowId, field, files }));
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 ? `${rowId} ${field.label}` : undefined, "aria-busy": status?.state === "saving", tabIndex: surface === "desktop" ? -1 : undefined, onClick: (event) => {
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(["*"], () => onFieldChange(change));
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 ? "重新查詢" : "繼續載入" })] }), 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 {
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 })] }), headerMenu && features.columnMenu !== false && (() => {
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": `${rowId} ${field.label}`, onFocus: () => setFocus({ rowId, fieldId: field.id }), onClick: (event) => { if (event.shiftKey) {
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
- 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") {
784
- event.preventDefault();
785
- event.stopPropagation();
786
- setHeaderMenu({ fieldId: field.id, anchor: event.currentTarget });
787
- } }, children: "\u2304" })] }), configurable && _jsx("span", { className: "hy-dv-resize", onPointerDown: (event) => resizeColumn(field.id, column.width, event), "aria-hidden": true })] }, field.id);
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 }) {