@keenaioc/data-view 0.0.0-poc.1 → 0.2.0-alpha.11

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 (129) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/DATA-PRIVACY.md +20 -0
  3. package/INTEGRATION.md +258 -0
  4. package/KNOWN-LIMITATIONS.md +63 -0
  5. package/LICENSE +41 -0
  6. package/README.md +73 -5
  7. package/dist/column-actions.d.ts +47 -0
  8. package/dist/column-actions.d.ts.map +1 -0
  9. package/dist/column-actions.js +92 -0
  10. package/dist/column-actions.js.map +1 -0
  11. package/dist/column-menu.d.ts +25 -0
  12. package/dist/column-menu.d.ts.map +1 -0
  13. package/dist/column-menu.js +64 -0
  14. package/dist/column-menu.js.map +1 -0
  15. package/dist/contracts.d.ts +33 -0
  16. package/dist/contracts.d.ts.map +1 -0
  17. package/dist/contracts.js +84 -0
  18. package/dist/contracts.js.map +1 -0
  19. package/dist/data-view.d.ts +3 -0
  20. package/dist/data-view.d.ts.map +1 -0
  21. package/dist/data-view.js +883 -0
  22. package/dist/data-view.js.map +1 -0
  23. package/dist/engine.d.ts +35 -0
  24. package/dist/engine.d.ts.map +1 -0
  25. package/dist/engine.js +315 -0
  26. package/dist/engine.js.map +1 -0
  27. package/dist/field-order.d.ts +8 -0
  28. package/dist/field-order.d.ts.map +1 -0
  29. package/dist/field-order.js +28 -0
  30. package/dist/field-order.js.map +1 -0
  31. package/dist/gantt-bar.d.ts +17 -0
  32. package/dist/gantt-bar.d.ts.map +1 -0
  33. package/dist/gantt-bar.js +142 -0
  34. package/dist/gantt-bar.js.map +1 -0
  35. package/dist/gantt-dependencies.d.ts +27 -0
  36. package/dist/gantt-dependencies.d.ts.map +1 -0
  37. package/dist/gantt-dependencies.js +218 -0
  38. package/dist/gantt-dependencies.js.map +1 -0
  39. package/dist/gantt-dependency-ui.d.ts +37 -0
  40. package/dist/gantt-dependency-ui.d.ts.map +1 -0
  41. package/dist/gantt-dependency-ui.js +79 -0
  42. package/dist/gantt-dependency-ui.js.map +1 -0
  43. package/dist/gantt-grid.d.ts +27 -0
  44. package/dist/gantt-grid.d.ts.map +1 -0
  45. package/dist/gantt-grid.js +72 -0
  46. package/dist/gantt-grid.js.map +1 -0
  47. package/dist/gantt-operations.d.ts +14 -0
  48. package/dist/gantt-operations.d.ts.map +1 -0
  49. package/dist/gantt-operations.js +103 -0
  50. package/dist/gantt-operations.js.map +1 -0
  51. package/dist/group-order.d.ts +8 -0
  52. package/dist/group-order.d.ts.map +1 -0
  53. package/dist/group-order.js +24 -0
  54. package/dist/group-order.js.map +1 -0
  55. package/dist/group-settings.d.ts +17 -0
  56. package/dist/group-settings.d.ts.map +1 -0
  57. package/dist/group-settings.js +44 -0
  58. package/dist/group-settings.js.map +1 -0
  59. package/dist/index.d.ts +17 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +17 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/operations.d.ts +11 -0
  64. package/dist/operations.d.ts.map +1 -0
  65. package/dist/operations.js +117 -0
  66. package/dist/operations.js.map +1 -0
  67. package/dist/registry.d.ts +17 -0
  68. package/dist/registry.d.ts.map +1 -0
  69. package/dist/registry.js +482 -0
  70. package/dist/registry.js.map +1 -0
  71. package/dist/remote-data.d.ts +19 -0
  72. package/dist/remote-data.d.ts.map +1 -0
  73. package/dist/remote-data.js +67 -0
  74. package/dist/remote-data.js.map +1 -0
  75. package/dist/row-move.d.ts +7 -0
  76. package/dist/row-move.d.ts.map +1 -0
  77. package/dist/row-move.js +27 -0
  78. package/dist/row-move.js.map +1 -0
  79. package/dist/schedule-view.d.ts +27 -0
  80. package/dist/schedule-view.d.ts.map +1 -0
  81. package/dist/schedule-view.js +122 -0
  82. package/dist/schedule-view.js.map +1 -0
  83. package/dist/schedule.d.ts +28 -0
  84. package/dist/schedule.d.ts.map +1 -0
  85. package/dist/schedule.js +97 -0
  86. package/dist/schedule.js.map +1 -0
  87. package/dist/style.css +1028 -0
  88. package/dist/timeline.d.ts +25 -0
  89. package/dist/timeline.d.ts.map +1 -0
  90. package/dist/timeline.js +58 -0
  91. package/dist/timeline.js.map +1 -0
  92. package/dist/toolbar.d.ts +10 -0
  93. package/dist/toolbar.d.ts.map +1 -0
  94. package/dist/toolbar.js +31 -0
  95. package/dist/toolbar.js.map +1 -0
  96. package/dist/types.d.ts +474 -0
  97. package/dist/types.d.ts.map +1 -0
  98. package/dist/types.js +22 -0
  99. package/dist/types.js.map +1 -0
  100. package/dist/use-remote-data.d.ts +18 -0
  101. package/dist/use-remote-data.d.ts.map +1 -0
  102. package/dist/use-remote-data.js +12 -0
  103. package/dist/use-remote-data.js.map +1 -0
  104. package/dist/use-row-move.d.ts +12 -0
  105. package/dist/use-row-move.d.ts.map +1 -0
  106. package/dist/use-row-move.js +151 -0
  107. package/dist/use-row-move.js.map +1 -0
  108. package/dist/v2-panels.d.ts +37 -0
  109. package/dist/v2-panels.d.ts.map +1 -0
  110. package/dist/v2-panels.js +118 -0
  111. package/dist/v2-panels.js.map +1 -0
  112. package/dist/view-state.d.ts +30 -0
  113. package/dist/view-state.d.ts.map +1 -0
  114. package/dist/view-state.js +94 -0
  115. package/dist/view-state.js.map +1 -0
  116. package/dist/view-workspace.d.ts +3 -0
  117. package/dist/view-workspace.d.ts.map +1 -0
  118. package/dist/view-workspace.js +95 -0
  119. package/dist/view-workspace.js.map +1 -0
  120. package/dist/virtual-list.d.ts +12 -0
  121. package/dist/virtual-list.d.ts.map +1 -0
  122. package/dist/virtual-list.js +46 -0
  123. package/dist/virtual-list.js.map +1 -0
  124. package/package.json +19 -16
  125. package/index.d.ts +0 -2
  126. package/index.js +0 -5
  127. package/react.d.ts +0 -2
  128. package/react.js +0 -8
  129. package/style.css +0 -1
@@ -0,0 +1,883 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { Fragment, Children, useEffect, useId, useMemo, useRef, useState, } from "react";
4
+ import { columnMovePeers, moveColumnSetting } from "./field-order.js";
5
+ import { ColumnMenu } from "./column-menu.js";
6
+ import { headerItems, changeHeaderView, FieldActionForm, ColumnFilterForm, FilterValueInput } from "./column-actions.js";
7
+ import { GroupSettings } from "./group-settings.js";
8
+ import { DataViewToolbar } from "./toolbar.js";
9
+ import { buildGroupTree, DataViewConflictError, filterRows, getFieldValue, getPinnedOffsets, getRightPinnedOffsets, moveColumn, normalizeColumnWidth, normalizeViewConfig, retainViewReferences, searchRows, sortRows, validateValue, visibleFields, } from "./engine.js";
10
+ import { FILTER_OPERATOR_LABELS, RelationSelect, getFieldTypeDefinition, renderFieldValue, formatClipboardValue, formatReadableValue, formatReadableRow } from "./registry.js";
11
+ import { canWriteField, createPasteChanges, encodeTSV, newOperationId, validateChanges, validateFieldChange } from "./operations.js";
12
+ import { createDataViewQuery, dataViewQueryKey, resolveMetrics, mergeViewState, splitViewState, createWriteQueue, virtualWindow } from "./contracts.js";
13
+ import { VirtualList } from "./virtual-list.js";
14
+ import { useRowMove } from "./use-row-move.js";
15
+ import { createSnapshotWriter } from "./view-state.js";
16
+ import { ScheduleView } from "./schedule-view.js";
17
+ import { applyGanttDependencyChange, ganttHistoryGuard, validateGanttScheduleGuard } from "./gantt-dependencies.js";
18
+ import { BatchEditor, ColorSettings, DataViewDialog, FieldCreator, FieldManager, ViewSettings } from "./v2-panels.js";
19
+ export function DataView(props) {
20
+ return _jsx(DataViewInner, { ...props }, props.identityKey ?? "default");
21
+ }
22
+ function DataViewInner(props) {
23
+ const { fields, rows, getRowId, title = "資料表格", loading = false, error = null, access = "granted", emptyMessage = "目前沒有資料", onViewChange, onCellCommit, onBatchCommit, onFieldChange, ganttDependencies, onGanttDependencyChange, onRequestAttachment, permissions, selectedRowIds, onSelectionChange, searchPlaceholder = "搜尋目前資料", } = props;
24
+ const [internalView, setInternalView] = useState(() => mergeViewState(normalizeViewConfig(fields, props.view ?? props.initialView), props.runtimeState));
25
+ const activeView = internalView;
26
+ const search = activeView.location?.search ?? "";
27
+ const [panel, setPanel] = useState(null);
28
+ const [draggedColumn, setDraggedColumn] = useState(null);
29
+ const [editing, setEditing] = useState(null);
30
+ const [viewSaveState, setViewSaveState] = useState(onViewChange ? "idle" : "readonly");
31
+ const [cellStatuses, setCellStatuses] = useState({});
32
+ const [localSelected, setLocalSelected] = useState([]);
33
+ const viewRef = useRef(activeView);
34
+ const layoutRef = useRef(normalizeViewConfig(fields, props.view ?? props.initialView));
35
+ const saveCallback = useRef(onViewChange);
36
+ useEffect(() => { saveCallback.current = onViewChange; }, [onViewChange]);
37
+ const writer = useRef(null);
38
+ const latestSave = useRef(0);
39
+ const pendingViewWrite = useRef(false);
40
+ const rootRef = useRef(null);
41
+ const scrollRef = useRef(null);
42
+ const mobileRef = useRef(null);
43
+ useEffect(() => { setEditing(null); }, [props.presentation]);
44
+ const [scrollTop, setScrollTop] = useState(0);
45
+ const [focus, setFocus] = useState(null);
46
+ const [anchor, setAnchor] = useState(null);
47
+ const [detailId, setDetailId] = useState(null);
48
+ const [dialog, setDialog] = useState(null);
49
+ const menuId = useId();
50
+ const [headerMenu, setHeaderMenu] = useState(null);
51
+ const [fieldAction, setFieldAction] = useState(null);
52
+ const [pending, setPending] = useState(null);
53
+ const [operation, setOperation] = useState({ state: "idle" });
54
+ const [undoStack, setUndoStack] = useState([]);
55
+ const [redoStack, setRedoStack] = useState([]);
56
+ const busyRef = useRef(false);
57
+ const pendingCells = useRef(new Set());
58
+ const writes = useRef(createWriteQueue());
59
+ useEffect(() => { const queue = createWriteQueue(); writes.current = queue; return () => queue.dispose(); }, []);
60
+ const [quick, setQuick] = useState(null);
61
+ const bottomRef = useRef(null);
62
+ const [viewportHeight, setViewportHeight] = useState(560);
63
+ const [viewportWidth, setViewportWidth] = useState(0);
64
+ const metrics = resolveMetrics(activeView.density, props.metrics);
65
+ const runtimeCallback = useRef(props.onRuntimeChange);
66
+ runtimeCallback.current = props.onRuntimeChange;
67
+ const detailCallback = useRef(props.onViewChangeDetail);
68
+ detailCallback.current = props.onViewChangeDetail;
69
+ const runtimeWriter = useRef(createSnapshotWriter((v) => runtimeCallback.current?.(v)));
70
+ const detailWriter = useRef(createSnapshotWriter((v) => detailCallback.current?.(v)));
71
+ const features = props.features ?? {};
72
+ const availableFields = useMemo(() => fields.filter((f) => !f.internal), [fields]);
73
+ useEffect(() => {
74
+ setHeaderMenu(null);
75
+ setFieldAction(null);
76
+ }, [props.presentation, activeView.viewType]);
77
+ useEffect(() => {
78
+ setHeaderMenu((current) => current && (!current.anchor.isConnected || !availableFields.some((field) => field.id === current.fieldId)) ? null : current);
79
+ setFieldAction((current) => current && !availableFields.some((field) => field.id === current.fieldId) ? null : current);
80
+ }, [availableFields]);
81
+ const latest = useRef({ rows, fields, permissions, access, ganttDependencies, mapping: activeView.gantt });
82
+ useEffect(() => { latest.current = { rows, fields, permissions, access, ganttDependencies, mapping: activeView.gantt }; }, [rows, fields, permissions, access, ganttDependencies, activeView.gantt]);
83
+ useEffect(() => {
84
+ if (props.view)
85
+ layoutRef.current = normalizeViewConfig(fields, props.view);
86
+ setInternalView((current) => mergeViewState(normalizeViewConfig(fields, !pendingViewWrite.current && props.view ? props.view : current), props.runtimeState));
87
+ }, [fields, props.view, props.runtimeState]);
88
+ useEffect(() => { viewRef.current = activeView; }, [activeView]);
89
+ useEffect(() => {
90
+ if (mobileRef.current)
91
+ mobileRef.current.scrollTop = viewRef.current.location?.mobileTop ?? 0;
92
+ const el = scrollRef.current;
93
+ if (!el)
94
+ return;
95
+ el.scrollTop = viewRef.current.location?.tableTop ?? 0;
96
+ el.scrollLeft = viewRef.current.location?.tableLeft ?? 0;
97
+ setScrollTop(el.scrollTop);
98
+ }, [activeView.viewType]);
99
+ useEffect(() => { const leave = (e) => { if (pendingViewWrite.current) {
100
+ e.preventDefault();
101
+ e.returnValue = "";
102
+ } }; window.addEventListener("beforeunload", leave); return () => window.removeEventListener("beforeunload", leave); }, []);
103
+ const selected = selectedRowIds ?? localSelected;
104
+ 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);
107
+ const move = useRowMove(props, activeView, rootRef, (id, action) => writes.current.run([id], action));
108
+ const configurable = permissions?.canConfigureView !== false;
109
+ const canManageFields = Boolean(onFieldChange && permissions?.canManageFields === true && access === "granted");
110
+ const addColumnWidth = canManageFields ? 56 : 0;
111
+ const busy = operation.state === "saving";
112
+ const selectedSet = useMemo(() => new Set(selected), [selected]);
113
+ const selectedRows = useMemo(() => rows.filter((row) => selectedSet.has(getRowId(row))), [rows, selectedSet, getRowId]);
114
+ const detailRow = detailId ? rows.find((row) => getRowId(row) === detailId) : undefined;
115
+ function setView(next, scope = "layout") {
116
+ if (scope === "layout" && !configurable)
117
+ return;
118
+ const normalized = retainViewReferences(viewRef.current, normalizeViewConfig(fields, next));
119
+ viewRef.current = normalized;
120
+ setInternalView(normalized);
121
+ const runtime = { viewType: normalized.viewType, location: normalized.location, collapsedGroups: normalized.collapsedGroups, ...(props.runtimeState?.filters || scope === "query" ? { filters: normalized.filters, sorts: normalized.sorts } : {}) };
122
+ const callbacks = [];
123
+ if (detailCallback.current)
124
+ callbacks.push(detailWriter.current({ scope, view: normalized, runtime }));
125
+ if (scope !== "layout" && runtimeCallback.current)
126
+ callbacks.push(runtimeWriter.current(runtime));
127
+ else if (saveCallback.current) {
128
+ if (!writer.current)
129
+ writer.current = createSnapshotWriter((value) => saveCallback.current?.(value));
130
+ const savedLayout = runtimeCallback.current ? { ...splitViewState(normalized).layout, filters: layoutRef.current.filters, sorts: layoutRef.current.sorts } : normalized;
131
+ layoutRef.current = savedLayout;
132
+ callbacks.push(writer.current(savedLayout));
133
+ }
134
+ if (!callbacks.length)
135
+ return;
136
+ setViewSaveState("saving");
137
+ pendingViewWrite.current = true;
138
+ const saveNumber = ++latestSave.current;
139
+ void Promise.all(callbacks).then(() => {
140
+ if (latestSave.current === saveNumber) {
141
+ setViewSaveState("saved");
142
+ pendingViewWrite.current = false;
143
+ }
144
+ }, (e) => { if (latestSave.current === saveNumber)
145
+ setViewSaveState(e instanceof DataViewConflictError ? "conflict" : "failed"); });
146
+ }
147
+ function patchView(patch, scope = "layout") { setView({ ...viewRef.current, ...patch }, scope); }
148
+ function patchLocation(patch) { patchView({ location: { ...viewRef.current.location, ...patch } }, "navigation"); }
149
+ const query = useMemo(() => createDataViewQuery(activeView), [activeView.filters, activeView.sorts, activeView.groups, search]);
150
+ const queryKey = dataViewQueryKey(query);
151
+ const remoteStale = !!props.remote && props.remote.queryKey !== queryKey;
152
+ const queryCallback = useRef(props.onQueryChange);
153
+ queryCallback.current = props.onQueryChange;
154
+ const previousQuery = useRef(queryKey);
155
+ useEffect(() => { if (access === "granted" && !loading && !error)
156
+ queryCallback.current?.(query); }, [queryKey, props.onQueryChange, access, loading, error]);
157
+ useEffect(() => { if (previousQuery.current === queryKey)
158
+ return; previousQuery.current = queryKey; setLocalSelected([]); onSelectionChange?.([]); setFocus(null); setAnchor(null); setEditing(null); setUndoStack([]); setRedoStack([]); }, [queryKey]);
159
+ const canFilter = permissions?.canFilter ?? configurable;
160
+ const canSort = permissions?.canSort ?? configurable;
161
+ const processedRows = useMemo(() => {
162
+ if (props.remote)
163
+ return remoteStale ? [] : rows;
164
+ const searched = searchRows(rows, fields, search);
165
+ const filtered = filterRows(searched, fields, activeView.filters.rules, activeView.filters.conjunction);
166
+ return sortRows(filtered, fields, activeView.sorts);
167
+ }, [activeView.filters, activeView.sorts, fields, rows, search, Boolean(props.remote), remoteStale]);
168
+ const fullGroups = useMemo(() => buildGroupTree(processedRows, fields, activeView.groups), [processedRows, fields, activeView.groups]);
169
+ const groups = fullGroups;
170
+ const visible = useMemo(() => visibleFields(fields, activeView), [activeView, fields]);
171
+ 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]);
174
+ const ganttRightOffsets = useMemo(() => getRightPinnedOffsets(activeView.columns), [activeView.columns]);
175
+ const tableWidth = visible.reduce((total, item) => total + item.column.width, (controlWidth) + addColumnWidth);
176
+ const collapsed = useMemo(() => new Set(activeView.collapsedGroups), [activeView.collapsedGroups]);
177
+ const allGroupKeys = useMemo(() => collectGroupKeys(fullGroups), [fullGroups]);
178
+ const rightOffsets = useMemo(() => new Map([...getRightPinnedOffsets(activeView.columns)].map(([id, offset]) => [id, offset + addColumnWidth])), [activeView.columns, addColumnWidth]);
179
+ const virtual = activeView.virtualize !== false || processedRows.length > 200;
180
+ const rowHeight = metrics.rowHeight;
181
+ const flatEntries = useMemo(() => flattenGroups(fullGroups, collapsed), [fullGroups, collapsed]);
182
+ const desktopRows = useMemo(() => flatEntries.flatMap((entry) => entry.kind === "row" ? [entry.row] : []), [flatEntries]);
183
+ const entryHeights = useMemo(() => flatEntries.map((e) => e.kind === "group" ? metrics.groupHeight + (e.node.depth ? metrics.nestedGroupGap : metrics.groupGap) : rowHeight), [flatEntries, rowHeight, metrics.groupHeight, metrics.groupGap, metrics.nestedGroupGap]);
184
+ const windowed = useMemo(() => virtualWindow(entryHeights, scrollTop, viewportHeight), [entryHeights, scrollTop, viewportHeight]);
185
+ const virtualStart = virtual ? windowed.start : 0;
186
+ const virtualEnd = virtual ? windowed.end : flatEntries.length;
187
+ useEffect(() => {
188
+ const el = scrollRef.current;
189
+ if (!el)
190
+ return;
191
+ const observer = new ResizeObserver(() => { setViewportHeight(el.clientHeight); setViewportWidth(el.clientWidth); });
192
+ observer.observe(el);
193
+ return () => observer.disconnect();
194
+ }, [activeView.viewType, loading, error, access, processedRows.length === 0]);
195
+ const mobileFields = useMemo(() => activeView.mobile ? activeView.mobile.fieldIds.flatMap((id) => { const field = availableFields.find((f) => f.id === id); return field ? [field] : []; }) : visible.slice(0, 5).map((v) => v.field), [activeView.mobile, availableFields, visible]);
196
+ function readableLabel(row) {
197
+ const id = activeView.viewType === "gantt" ? activeView.gantt?.titleFieldId : activeView.viewType === "calendar" ? activeView.calendar?.titleFieldId : activeView.mobile?.summaryFieldId;
198
+ const field = fields.find((f) => f.id === id) ?? availableFields.find((f) => !f.renderAction && canRead(row, f));
199
+ if (field && (field.internal || !canRead(row, field)))
200
+ return "無權限";
201
+ return props.getRowLabel?.(row)?.trim() || (field ? formatReadableValue(getFieldValue(row, field), { ...field, internal: false }, row) : "") || "未命名工作";
202
+ }
203
+ function openRow(row) { if (features.rowOpen === false)
204
+ return; if (props.onRowOpen)
205
+ props.onRowOpen(row);
206
+ else
207
+ setDetailId(getRowId(row)); }
208
+ function groupLabel(node) {
209
+ const field = fields.find((f) => f.id === node.fieldId);
210
+ return _jsxs(_Fragment, { children: [_jsx("strong", { className: "hy-dv-group-label", style: { backgroundColor: node.color, color: node.textColor }, children: node.valueKey === "__restricted__" ? "無權限" : field?.renderGroup?.(node) ?? node.label }), _jsxs("span", { children: [props.remote?.groupCounts?.[node.key] ?? node.rows.length, props.remote && !props.remote.complete && props.remote.groupCounts?.[node.key] === undefined ? " 已載入" : ""] })] });
211
+ }
212
+ function detailContext(row) { return { row, fields: availableFields.filter((f) => canRead(row, f)), close: () => { setDetailId(null); setEditing(null); }, renderCell: (field) => renderCell(row, field, "detail") }; }
213
+ function loadNearEnd(el) { if (el.scrollHeight - el.scrollTop - el.clientHeight < 200 && props.remote && !remoteStale && !props.remote.complete && !props.remote.loading && !props.remote.error)
214
+ props.remote.onLoadMore?.(); }
215
+ const colorMap = useMemo(() => {
216
+ const result = new Map();
217
+ for (const row of rows) {
218
+ const colors = new Map();
219
+ for (const rule of activeView.colorRules ?? []) {
220
+ const field = fields.find((f) => f.id === rule.condition.fieldId);
221
+ if (!field || field.canRead?.({ row, field }) === false || colors.has(rule.target))
222
+ continue;
223
+ if (getFieldTypeDefinition(field.type).filter(getFieldValue(row, field), rule.condition, field))
224
+ colors.set(rule.target, rule.color);
225
+ }
226
+ if (colors.size)
227
+ result.set(getRowId(row), colors);
228
+ }
229
+ return result;
230
+ }, [rows, fields, getRowId, activeView.colorRules]);
231
+ function changeSelection(next) {
232
+ const unique = [...new Set(next)];
233
+ if (!selectedRowIds)
234
+ setLocalSelected(unique);
235
+ onSelectionChange?.(unique);
236
+ }
237
+ function toggleRow(rowId) {
238
+ changeSelection(selected.includes(rowId) ? selected.filter((id) => id !== rowId) : [...selected, rowId]);
239
+ }
240
+ const pageIds = processedRows.map(getRowId);
241
+ const allPageSelected = pageIds.length > 0 && pageIds.every((id) => selected.includes(id));
242
+ function togglePageSelection() {
243
+ if (allPageSelected)
244
+ changeSelection(selected.filter((id) => !pageIds.includes(id)));
245
+ else
246
+ changeSelection([...selected, ...pageIds]);
247
+ }
248
+ function toggleGroup(key) {
249
+ if (permissions?.canCollapseGroups === false)
250
+ return;
251
+ const next = new Set(activeView.collapsedGroups);
252
+ if (next.has(key))
253
+ next.delete(key);
254
+ else
255
+ next.add(key);
256
+ patchView({ collapsedGroups: [...next] }, "navigation");
257
+ }
258
+ function setAllGroups(close) {
259
+ if (permissions?.canCollapseGroups !== false)
260
+ patchView({ collapsedGroups: close ? allGroupKeys : [] }, "navigation");
261
+ }
262
+ function resizeColumn(fieldId, startWidth, event) {
263
+ event.preventDefault();
264
+ const startX = event.clientX;
265
+ function move(pointer) {
266
+ const width = normalizeColumnWidth(startWidth + pointer.clientX - startX);
267
+ setView({
268
+ ...viewRef.current,
269
+ columns: viewRef.current.columns.map((column) => column.fieldId === fieldId ? { ...column, width } : column),
270
+ });
271
+ }
272
+ function up() {
273
+ window.removeEventListener("pointermove", move);
274
+ window.removeEventListener("pointerup", up);
275
+ window.removeEventListener("pointercancel", up);
276
+ }
277
+ window.addEventListener("pointermove", move);
278
+ window.addEventListener("pointerup", up);
279
+ window.addEventListener("pointercancel", up);
280
+ }
281
+ function dropColumn(targetId) {
282
+ if (!draggedColumn)
283
+ return;
284
+ patchView({ columns: moveColumn(activeView.columns, draggedColumn, targetId) });
285
+ setDraggedColumn(null);
286
+ }
287
+ function canRead(row, field) {
288
+ return field.canRead?.({ row, field }) !== false;
289
+ }
290
+ function canEdit(row, field) {
291
+ const spec = getFieldTypeDefinition(field.type);
292
+ return Boolean(!busyRef.current && !pendingCells.current.has(`${getRowId(row)}:${field.id}`) && access === "granted" && !remoteStale && (field.Editor || spec.Editor) && canWriteField(row, field, permissions) &&
293
+ (field.type === "attachment" ? onRequestAttachment : onBatchCommit ?? onCellCommit));
294
+ }
295
+ async function commitCell(row, field, nextValue) {
296
+ if (!canEdit(row, field))
297
+ return;
298
+ const rowId = getRowId(row);
299
+ const key = `${rowId}:${field.id}`;
300
+ const previousValue = getFieldValue(row, field);
301
+ if (JSON.stringify(previousValue) === JSON.stringify(nextValue)) {
302
+ setEditing(null);
303
+ return;
304
+ }
305
+ const validation = validateValue(nextValue, field, row);
306
+ if (validation) {
307
+ setEditing(null);
308
+ setCellStatuses((current) => ({ ...current, [key]: { state: "failed", message: validation } }));
309
+ return;
310
+ }
311
+ setEditing(null);
312
+ if (onBatchCommit && !onCellCommit) {
313
+ await execute({ id: newOperationId(), reason: "edit", changes: [{ row, rowId, field, previousValue, nextValue }] });
314
+ return;
315
+ }
316
+ pendingCells.current.add(key);
317
+ setCellStatuses((current) => ({ ...current, [key]: { state: "saving" } }));
318
+ try {
319
+ await writes.current.run([rowId], async () => {
320
+ const fresh = latest.current.rows.find((r) => getRowId(r) === rowId);
321
+ const definition = latest.current.fields.find((f) => f.id === field.id);
322
+ if (!fresh || !definition || latest.current.access !== "granted" || !canWriteField(fresh, definition, latest.current.permissions))
323
+ throw new Error("資料不存在或無修改權限");
324
+ if (JSON.stringify(getFieldValue(fresh, definition)) !== JSON.stringify(previousValue))
325
+ throw new DataViewConflictError();
326
+ await onCellCommit?.({ row: fresh, rowId, field: definition, previousValue, nextValue });
327
+ });
328
+ setCellStatuses((current) => ({ ...current, [key]: { state: "saved" } }));
329
+ setTimeout(() => setCellStatuses((current) => {
330
+ if (current[key]?.state !== "saved")
331
+ return current;
332
+ const next = { ...current };
333
+ delete next[key];
334
+ return next;
335
+ }), 1400);
336
+ }
337
+ catch (commitError) {
338
+ setCellStatuses((current) => ({
339
+ ...current,
340
+ [key]: {
341
+ state: commitError instanceof DataViewConflictError ? "conflict" : "failed",
342
+ message: commitError instanceof Error ? commitError.message : "儲存失敗",
343
+ },
344
+ }));
345
+ }
346
+ finally {
347
+ pendingCells.current.delete(key);
348
+ }
349
+ }
350
+ function renderCell(row, field, surface) {
351
+ if (field.internal)
352
+ return null;
353
+ if (!canRead(row, field))
354
+ return _jsx("span", { className: "hy-dv-no-access", children: "\u7121\u6B0A\u9650" });
355
+ const rowId = getRowId(row);
356
+ const key = `${rowId}:${field.id}`;
357
+ const value = getFieldValue(row, field);
358
+ const status = cellStatuses[key];
359
+ const isEditing = editing?.rowId === rowId && editing.fieldId === field.id && editing.surface === surface;
360
+ const spec = getFieldTypeDefinition(field.type);
361
+ if (field.renderAction)
362
+ return _jsxs("fieldset", { className: "hy-dv-action-cell", disabled: field.canAct?.(row) === false || status?.state === "saving" || access !== "granted", children: [field.renderAction({ row, field, value, close: () => setEditing(null), openRow: () => openRow(row), disabled: field.canAct?.(row) === false || status?.state === "saving", pending: status?.state === "saving", run: async (action) => { if (field.canAct?.(row) === false || pendingCells.current.has(key))
363
+ return; pendingCells.current.add(key); setCellStatuses((s) => ({ ...s, [key]: { state: "saving" } })); try {
364
+ await writes.current.run([rowId], () => { const fresh = latest.current.rows.find((r) => getRowId(r) === rowId); const f = latest.current.fields.find((f) => f.id === field.id); if (!fresh || !f || latest.current.access !== "granted" || f.canRead?.({ row: fresh, field: f }) === false || f.canAct?.(fresh) === false)
365
+ throw new Error("無操作權限"); return action(); });
366
+ setCellStatuses((s) => ({ ...s, [key]: { state: "saved" } }));
367
+ }
368
+ catch (e) {
369
+ setCellStatuses((s) => ({ ...s, [key]: { state: e instanceof DataViewConflictError ? "conflict" : "failed", message: e instanceof Error ? e.message : "操作失敗" } }));
370
+ }
371
+ finally {
372
+ pendingCells.current.delete(key);
373
+ } } }), status && _jsx(SaveBadge, { state: status.state, message: status.message })] });
374
+ if (isEditing && !spec.toggleValue && (field.Editor || spec.Editor) && canEdit(row, field)) {
375
+ const Editor = field.Editor ?? spec.Editor;
376
+ return _jsx("div", { className: "hy-dv-editor-wrap", children: _jsx(Editor, { value: value, row: row, field: field, onCommit: (next) => void commitCell(row, field, next), onCancel: () => setEditing(null), onRequestAttachment: onRequestAttachment ? async (files) => {
377
+ if (pendingCells.current.has(key))
378
+ return;
379
+ pendingCells.current.add(key);
380
+ setEditing(null);
381
+ setCellStatuses((current) => ({ ...current, [key]: { state: "saving" } }));
382
+ try {
383
+ await writes.current.run([rowId], () => onRequestAttachment({ row, rowId, field, files }));
384
+ setCellStatuses((current) => ({ ...current, [key]: { state: "saved" } }));
385
+ }
386
+ catch (uploadError) {
387
+ setCellStatuses((current) => ({ ...current, [key]: { state: "failed", message: uploadError instanceof Error ? uploadError.message : "附件儲存失敗" } }));
388
+ }
389
+ finally {
390
+ pendingCells.current.delete(key);
391
+ }
392
+ } : undefined }) });
393
+ }
394
+ if (field.type === "relation" && !field.Editor && !field.renderQuickView && !field.render && canWriteField(row, field, permissions) && (onBatchCommit || onCellCommit))
395
+ 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 })] });
396
+ if (field.type === "attachment")
397
+ 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) => {
399
+ if (event.shiftKey && surface === "desktop")
400
+ return;
401
+ setFocus({ rowId, fieldId: field.id });
402
+ setAnchor(null);
403
+ if (spec.toggleValue) {
404
+ void commitCell(row, field, spec.toggleValue(value));
405
+ return;
406
+ }
407
+ if (field.renderQuickView) {
408
+ setQuick({ rowId, fieldId: field.id });
409
+ return;
410
+ }
411
+ if (!canEdit(row, field)) {
412
+ openRow(row);
413
+ return;
414
+ }
415
+ if (surface !== "detail" && (field.Editor || ["multiLineText", "multiSelect", "attachment", "relation", "person"].includes(field.type))) {
416
+ setDetailId(rowId);
417
+ setEditing({ rowId, fieldId: field.id, surface: "detail" });
418
+ return;
419
+ }
420
+ if (canEdit(row, field))
421
+ setEditing({ rowId, fieldId: field.id, surface });
422
+ }, disabled: status?.state === "saving" || (!!spec.toggleValue && !canEdit(row, field)), title: status?.message, children: [_jsx("span", { className: "hy-dv-cell-value", title: formatReadableValue(value, field, row), children: renderFieldValue(value, field, row) }), field.appearance?.arrow === true && !spec.toggleValue && _jsx("span", { className: "hy-dv-cell-arrow", "aria-hidden": true, children: "\u2304" }), status && _jsx(SaveBadge, { state: status.state, message: status.message })] });
423
+ }
424
+ function reportFailure(error) {
425
+ setOperation({ state: error instanceof DataViewConflictError ? "conflict" : "failed", message: error instanceof Error ? error.message : String(error) });
426
+ }
427
+ async function execute(request) {
428
+ if (!onBatchCommit || busyRef.current || latest.current.access !== "granted" || remoteStale)
429
+ return false;
430
+ const keys = request.changes.map((c) => `${c.rowId}:${c.field.id}`);
431
+ keys.forEach((k) => pendingCells.current.add(k));
432
+ setCellStatuses((s) => ({ ...s, ...Object.fromEntries(keys.map((k) => [k, { state: "saving" }])) }));
433
+ return writes.current.run(request.changes.map((c) => c.rowId), async () => {
434
+ const currentRows = new Map(latest.current.rows.map((row) => [getRowId(row), row]));
435
+ try {
436
+ if (request.scheduleGuard) {
437
+ const guard = request.scheduleGuard;
438
+ const mapping = latest.current.mapping;
439
+ if (!mapping || mapping.startFieldId !== guard.startFieldId || mapping.endFieldId !== guard.endFieldId || (mapping.workdaysOnly === true) !== guard.workdaysOnly)
440
+ throw new DataViewConflictError("日期設定已變更,請重新預覽");
441
+ validateGanttScheduleGuard(guard, latest.current.rows, latest.current.fields, getRowId, latest.current.ganttDependencies);
442
+ for (const c of request.changes) {
443
+ const row = currentRows.get(c.rowId);
444
+ if (row && latest.current.ganttDependencies?.isDateLocked?.(row))
445
+ throw new DataViewConflictError("排程已確認或鎖定,請重新協調日期");
446
+ }
447
+ }
448
+ const changes = request.changes.map((change) => {
449
+ const row = currentRows.get(change.rowId);
450
+ const field = latest.current.fields.find((f) => f.id === change.field.id);
451
+ if (!row || !field)
452
+ throw new DataViewConflictError("資料列或欄位已不存在,請重新選取");
453
+ if (JSON.stringify(getFieldValue(row, field)) !== JSON.stringify(change.previousValue))
454
+ throw new DataViewConflictError("預覽後資料已更新,請重新檢查修改內容");
455
+ return { ...change, row, field };
456
+ });
457
+ const validation = validateChanges(changes, latest.current.permissions);
458
+ if (validation)
459
+ throw new Error(validation);
460
+ setOperation({ state: "saving", message: `正在儲存 ${changes.length} 格` });
461
+ await onBatchCommit({ ...request, changes });
462
+ if (features.undo !== false && request.reason !== "undo" && request.reason !== "redo") {
463
+ 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
+ setRedoStack([]);
465
+ }
466
+ setCellStatuses((s) => ({ ...s, ...Object.fromEntries(keys.map((k) => [k, { state: "saved" }])) }));
467
+ setOperation({ state: "saved", message: `已儲存 ${changes.length} 格` });
468
+ return true;
469
+ }
470
+ catch (error) {
471
+ setCellStatuses((s) => ({ ...s, ...Object.fromEntries(keys.map((k) => [k, { state: error instanceof DataViewConflictError ? "conflict" : "failed", message: error instanceof Error ? error.message : "儲存失敗" }])) }));
472
+ reportFailure(error);
473
+ return false;
474
+ }
475
+ finally {
476
+ keys.forEach((k) => pendingCells.current.delete(k));
477
+ }
478
+ });
479
+ }
480
+ function reviewChanges(changes, reason) {
481
+ const validation = validateChanges(changes, permissions);
482
+ if (validation) {
483
+ reportFailure(new Error(validation));
484
+ return;
485
+ }
486
+ setPending({ id: newOperationId(), reason, changes });
487
+ setDialog("review");
488
+ setEditing(null);
489
+ }
490
+ async function applyFieldChange(change) {
491
+ if (!onFieldChange || busyRef.current || latest.current.access !== "granted")
492
+ return false;
493
+ const validation = validateFieldChange(latest.current.fields, change, latest.current.permissions);
494
+ if (validation) {
495
+ reportFailure(new Error(validation));
496
+ return false;
497
+ }
498
+ busyRef.current = true;
499
+ setOperation({ state: "saving", message: "正在儲存欄位結構" });
500
+ try {
501
+ await writes.current.run(["*"], () => onFieldChange(change));
502
+ setUndoStack([]);
503
+ setRedoStack([]);
504
+ setFocus(null);
505
+ setAnchor(null);
506
+ setEditing(null);
507
+ setOperation({ state: "saved", message: "欄位結構已儲存" });
508
+ return true;
509
+ }
510
+ catch (error) {
511
+ reportFailure(error);
512
+ return false;
513
+ }
514
+ finally {
515
+ busyRef.current = false;
516
+ }
517
+ }
518
+ async function changeDependency(change) {
519
+ const current = latest.current;
520
+ if (!onGanttDependencyChange || !current.ganttDependencies || busyRef.current || current.access !== "granted" || current.permissions?.canManageDependencies !== true)
521
+ return false;
522
+ try {
523
+ applyGanttDependencyChange(current.ganttDependencies, change);
524
+ const link = change.action === "add" ? change.link : current.ganttDependencies.links.find((l) => l.id === change.linkId);
525
+ if (!link || ![link.predecessorId, link.successorId].every((id) => {
526
+ const row = current.rows.find((r) => getRowId(r) === id);
527
+ return row && [current.mapping?.titleFieldId, current.mapping?.startFieldId, current.mapping?.endFieldId].every((fieldId) => { const field = current.fields.find((f) => f.id === fieldId); return field && field.canRead?.({ row, field }) !== false; });
528
+ }))
529
+ throw new Error("相關資料不存在或無權限");
530
+ busyRef.current = true;
531
+ setOperation({ state: "saving", message: "正在儲存依賴" });
532
+ await writes.current.run(["*"], () => onGanttDependencyChange(change));
533
+ setUndoStack([]);
534
+ setRedoStack([]);
535
+ setOperation({ state: "saved", message: "依賴已儲存,日期保持原值" });
536
+ return true;
537
+ }
538
+ catch (error) {
539
+ reportFailure(error);
540
+ return false;
541
+ }
542
+ finally {
543
+ busyRef.current = false;
544
+ }
545
+ }
546
+ async function historyAction(direction) {
547
+ if (features[direction] === false)
548
+ return;
549
+ const stack = direction === "undo" ? undoStack : redoStack;
550
+ const entry = stack.at(-1);
551
+ if (!entry)
552
+ return;
553
+ const changes = direction === "undo" ? entry.changes.map((c) => ({ ...c, previousValue: c.nextValue, nextValue: c.previousValue })) : entry.changes;
554
+ if (await execute({ id: newOperationId(), reason: direction, changes, scheduleGuard: ganttHistoryGuard(entry.scheduleGuard, entry.changes, direction === "undo") })) {
555
+ if (direction === "undo") {
556
+ setUndoStack((s) => s.slice(0, -1));
557
+ setRedoStack((s) => [...s, entry]);
558
+ }
559
+ else {
560
+ setRedoStack((s) => s.slice(0, -1));
561
+ setUndoStack((s) => [...s, entry]);
562
+ }
563
+ }
564
+ }
565
+ function gridRange() {
566
+ if (!focus)
567
+ return null;
568
+ const row = desktopRows.findIndex((r) => getRowId(r) === focus.rowId);
569
+ const col = visible.findIndex((v) => v.field.id === focus.fieldId);
570
+ if (row < 0 || col < 0)
571
+ return null;
572
+ const anchorRow = anchor ? desktopRows.findIndex((r) => getRowId(r) === anchor.rowId) : row;
573
+ const anchorCol = anchor ? visible.findIndex((v) => v.field.id === anchor.fieldId) : col;
574
+ return { row, col, fromRow: Math.min(row, anchorRow < 0 ? row : anchorRow), toRow: Math.max(row, anchorRow), fromCol: Math.min(col, anchorCol < 0 ? col : anchorCol), toCol: Math.max(col, anchorCol) };
575
+ }
576
+ function inputTarget(target) { return target instanceof HTMLElement && (target.matches("input,textarea,select,a") || !!target.closest(".hy-dv-action-cell") || target.isContentEditable); }
577
+ function handleCopy(event) {
578
+ if (features.copy === false || inputTarget(event.target) || !event.target || !isGridTarget(event.target))
579
+ return;
580
+ const range = gridRange();
581
+ if (!range)
582
+ return;
583
+ const values = desktopRows.slice(range.fromRow, range.toRow + 1).map((row) => visible.slice(range.fromCol, range.toCol + 1).map(({ field }) => !field.internal && !field.renderAction && canRead(row, field) ? formatClipboardValue(getFieldValue(row, field), field) : ""));
584
+ event.preventDefault();
585
+ event.clipboardData.setData("text/plain", encodeTSV(values));
586
+ setOperation({ state: "saved", message: `已複製 ${values.length} 列(未讀取無權限值)` });
587
+ }
588
+ function handlePaste(event) {
589
+ if (features.paste === false || inputTarget(event.target) || !isGridTarget(event.target) || !onBatchCommit || busy)
590
+ return;
591
+ const range = gridRange();
592
+ if (!range)
593
+ return;
594
+ event.preventDefault();
595
+ try {
596
+ reviewChanges(createPasteChanges(event.clipboardData.getData("text/plain"), desktopRows, visible.map((v) => v.field), getRowId, range.fromRow, range.fromCol), "paste");
597
+ }
598
+ catch (error) {
599
+ reportFailure(error);
600
+ }
601
+ }
602
+ function isGridTarget(target) { return target instanceof Element && !!target.closest("td[data-hy-cell]"); }
603
+ function handleKey(event) {
604
+ if (inputTarget(event.target) || !isGridTarget(event.target) || editing)
605
+ return;
606
+ const modifier = event.ctrlKey || event.metaKey;
607
+ if (modifier && (event.key.toLowerCase() === "z" || event.key.toLowerCase() === "y")) {
608
+ if (onBatchCommit) {
609
+ event.preventDefault();
610
+ void historyAction(event.shiftKey || event.key.toLowerCase() === "y" ? "redo" : "undo");
611
+ }
612
+ return;
613
+ }
614
+ const range = gridRange();
615
+ if (!range)
616
+ return;
617
+ if (event.key === "Escape") {
618
+ setAnchor(null);
619
+ setEditing(null);
620
+ return;
621
+ }
622
+ if (event.key === " " && modifier && selectable) {
623
+ event.preventDefault();
624
+ toggleRow(getRowId(desktopRows[range.row]));
625
+ return;
626
+ }
627
+ if (event.key === "Enter" || event.key === "F2" || (event.key === " " && !modifier && !event.shiftKey && getFieldTypeDefinition(visible[range.col].field.type).toggleValue)) {
628
+ event.preventDefault();
629
+ const row = desktopRows[range.row];
630
+ const field = visible[range.col].field;
631
+ const relationSelect = event.target.closest("td")?.querySelector(".hy-dv-relation-select");
632
+ if (relationSelect && !relationSelect.disabled) {
633
+ relationSelect.focus();
634
+ try {
635
+ relationSelect.showPicker?.();
636
+ }
637
+ catch { /* Focus still permits native keyboard selection. */ }
638
+ return;
639
+ }
640
+ const toggleValue = getFieldTypeDefinition(field.type).toggleValue;
641
+ if (toggleValue)
642
+ void commitCell(row, field, toggleValue(getFieldValue(row, field)));
643
+ else if (!canEdit(row, field))
644
+ openRow(row);
645
+ else if (field.Editor || ["person", "relation", "multiLineText", "multiSelect"].includes(field.type)) {
646
+ setDetailId(getRowId(row));
647
+ setEditing({ rowId: getRowId(row), fieldId: field.id, surface: "detail" });
648
+ }
649
+ else if (canEdit(row, field))
650
+ setEditing({ rowId: getRowId(row), fieldId: field.id, surface: "desktop" });
651
+ return;
652
+ }
653
+ if ((event.key === "Delete" || event.key === "Backspace") && onBatchCommit && features.clear !== false) {
654
+ event.preventDefault();
655
+ reviewChanges(desktopRows.slice(range.fromRow, range.toRow + 1).flatMap((row) => visible.slice(range.fromCol, range.toCol + 1).map(({ field }) => ({ row, rowId: getRowId(row), field, previousValue: getFieldValue(row, field), nextValue: field.type === "checkbox" ? false : null }))), "clear");
656
+ return;
657
+ }
658
+ let row = range.row;
659
+ let col = range.col;
660
+ if (event.key === "ArrowDown")
661
+ row++;
662
+ else if (event.key === "ArrowUp")
663
+ row--;
664
+ else if (event.key === "PageDown")
665
+ row += Math.max(1, Math.floor(viewportHeight / rowHeight));
666
+ else if (event.key === "PageUp")
667
+ row -= Math.max(1, Math.floor(viewportHeight / rowHeight));
668
+ else if (event.key === "ArrowLeft")
669
+ col--;
670
+ else if (event.key === "ArrowRight")
671
+ col++;
672
+ else if (event.key === "Home") {
673
+ col = 0;
674
+ if (modifier)
675
+ row = 0;
676
+ }
677
+ else if (event.key === "End") {
678
+ col = visible.length - 1;
679
+ if (modifier)
680
+ row = desktopRows.length - 1;
681
+ }
682
+ else if (event.key === "Tab") {
683
+ const next = row * visible.length + col + (event.shiftKey ? -1 : 1);
684
+ if (next < 0 || next >= desktopRows.length * visible.length)
685
+ return;
686
+ row = Math.floor(next / visible.length);
687
+ col = next % visible.length;
688
+ }
689
+ else
690
+ return;
691
+ event.preventDefault();
692
+ row = Math.max(0, Math.min(desktopRows.length - 1, row));
693
+ col = Math.max(0, Math.min(visible.length - 1, col));
694
+ const next = { rowId: getRowId(desktopRows[row]), fieldId: visible[col].field.id };
695
+ setAnchor(event.shiftKey && event.key !== "Tab" ? anchor ?? focus : null);
696
+ setFocus(next);
697
+ if (virtual) {
698
+ const index = flatEntries.findIndex((e) => e.kind === "row" && getRowId(e.row) === next.rowId);
699
+ if (index < virtualStart || index >= virtualEnd) {
700
+ if (scrollRef.current)
701
+ scrollRef.current.scrollTop = windowed.offsets[index];
702
+ setScrollTop(windowed.offsets[index]);
703
+ }
704
+ }
705
+ requestAnimationFrame(() => {
706
+ const cell = rootRef.current?.querySelector(`td[data-row-id="${CSS.escape(next.rowId)}"][data-field-id="${CSS.escape(next.fieldId)}"]`);
707
+ cell?.focus({ preventScroll: true });
708
+ cell?.scrollIntoView({ block: "nearest", inline: "nearest" });
709
+ });
710
+ }
711
+ if (access === "denied" || loading || error) {
712
+ const state = access === "denied" ? _jsx(StateCard, { title: "\u7121\u6B0A\u9650", message: "\u4F60\u6C92\u6709\u67E5\u770B\u9019\u4EFD\u8CC7\u6599\u7684\u6B0A\u9650\u3002", tone: "warning" }) : loading ? _jsx(LoadingState, {}) : _jsx(StateCard, { title: "\u8F09\u5165\u5931\u6557", message: error, tone: "error" });
713
+ return props.height === undefined ? state : _jsx("section", { className: "hy-dv", style: { height: props.height, overflow: "auto" }, "aria-label": title, children: state });
714
+ }
715
+ const range = gridRange();
716
+ 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)
718
+ 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 {
720
+ await navigator.clipboard.writeText(formatReadableRow(detailRow, availableFields));
721
+ setOperation({ state: "saved", message: "已複製可讀內容" });
722
+ }
723
+ catch {
724
+ 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 && (() => {
726
+ const field = availableFields.find((item) => item.id === headerMenu.fieldId);
727
+ 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
+ })(), fieldAction && (() => {
729
+ const field = availableFields.find((item) => item.id === fieldAction.fieldId);
730
+ if (!field)
731
+ return null;
732
+ const action = fieldAction.action;
733
+ return _jsxs(DataViewDialog, { title: `${action === "rename" ? "重新命名" : action === "copy" ? "複製欄位與值" : action === "delete" ? "刪除欄位" : "篩選"}:${field.label}`, busy: busy, onClose: () => setFieldAction(null), children: [action === "filter" ? _jsx(ColumnFilterForm, { field: field, filters: activeView.filters, disabled: busy || !canFilter || features.filter === false, onCancel: () => setFieldAction(null), onApply: (rule) => {
734
+ if (busyRef.current || !canFilter || features.filter === false)
735
+ return;
736
+ const current = viewRef.current.filters;
737
+ if (current.rules.length >= 20 && !current.rules.some((item) => item.id === rule.id)) {
738
+ setOperation({ state: "failed", message: "最多二十個篩選條件,請先移除其他條件。" });
739
+ return;
740
+ }
741
+ const rules = current.rules.some((item) => item.id === rule.id) ? current.rules.map((item) => item.id === rule.id ? rule : item) : [...current.rules, rule];
742
+ setView({ ...viewRef.current, filters: { ...current, rules } }, "query");
743
+ setFieldAction(null);
744
+ } }, field.id) : _jsx(FieldActionForm, { action: action, field: field, fields: fields, permissions: permissions, busy: busy, onApply: applyFieldChange, onCancel: () => setFieldAction(null) }, `${field.id}:${action}`), (operation.state === "failed" || operation.state === "conflict") && _jsx("p", { role: "alert", children: operation.message })] });
745
+ })(), dialog && _jsxs(DataViewDialog, { title: dialog === "add-field" ? "新增欄位" : dialog === "fields" ? "管理欄位" : dialog === "batch" ? "批次修改" : "確認資料修改", busy: busy, onClose: () => { setDialog(null); setPending(null); }, children: [dialog === "add-field" && _jsx(FieldCreator, { fields: fields, permissions: permissions, onApply: async (change) => { const saved = await applyFieldChange(change); if (saved)
746
+ setDialog(null); return saved; }, busy: busy, onCancel: () => setDialog(null) }), dialog === "fields" && _jsx(FieldManager, { fields: availableFields, permissions: permissions, onApply: applyFieldChange, busy: busy }), dialog === "batch" && _jsx(BatchEditor, { fields: availableFields, rows: selectedRows, getRowId: getRowId, permissions: permissions, onReview: reviewChanges }), dialog === "review" && pending && _jsxs("div", { className: "hy-dv-form", children: [_jsxs("p", { children: ["\u5171 ", pending.changes.length, " \u683C\u3002\u6240\u6709\u6B04\u4F4D\u9A57\u8B49\u901A\u904E\u5F8C\uFF0C\u7531\u5C08\u6848\u4E00\u6B21\u5BEB\u5165\uFF1B\u5931\u6557\u6216\u885D\u7A81\u6642\u4FDD\u7559\u539F\u8CC7\u6599\u3002"] }), _jsx("div", { className: "hy-dv-review-list", children: pending.changes.slice(0, 30).map((change) => _jsxs("div", { children: [_jsxs("strong", { children: [change.rowId, "\uFF0F", change.field.label] }), _jsxs("span", { children: [formatClipboardValue(change.previousValue, change.field) || "空白", " \u2192 ", formatClipboardValue(change.nextValue, change.field) || "空白"] })] }, `${change.rowId}:${change.field.id}`)) }), pending.changes.length > 30 && _jsxs("p", { children: ["\u9810\u89BD\u524D 30 \u683C\uFF0C\u5168\u90E8 ", pending.changes.length, " \u683C\u90FD\u6703\u9A57\u8B49\u3002"] }), _jsxs("button", { disabled: busy, onClick: async () => { if (await execute(pending)) {
747
+ setDialog(null);
748
+ setPending(null);
749
+ } }, children: ["\u78BA\u8A8D\u5957\u7528 ", pending.changes.length, " \u683C"] })] }), operation.state !== "idle" && _jsx("p", { role: operation.state === "failed" || operation.state === "conflict" ? "alert" : "status", children: operation.message })] })] });
750
+ function renderDesktopRow(row) {
751
+ const rowId = getRowId(row);
752
+ const index = desktopRowIndices.get(rowId) ?? 0;
753
+ 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) => {
755
+ const left = pinnedOffsets.get(field.id);
756
+ const right = rightOffsets.get(field.id);
757
+ 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) {
759
+ setAnchor(anchor ?? focus);
760
+ setFocus({ rowId, fieldId: field.id });
761
+ } }, 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);
762
+ }), canManageFields && _jsx("td", { className: "hy-dv-add-column is-pinned", style: { right: 0 } })] }, rowId);
763
+ }
764
+ function chooseHeaderAction(field, action) {
765
+ const allowed = headerItems({ field, fields, view: viewRef.current, permissions, features, hasFieldCallback: !!onFieldChange, busy: busyRef.current }).find((item) => item.action === action);
766
+ if (!allowed || allowed.disabled)
767
+ return;
768
+ setHeaderMenu(null);
769
+ if (["rename", "copy", "delete", "filter"].includes(action)) {
770
+ setFieldAction({ fieldId: field.id, action: action });
771
+ return;
772
+ }
773
+ if (action === "columns") {
774
+ setPanel("columns");
775
+ return;
776
+ }
777
+ const next = changeHeaderView(viewRef.current, field.id, action);
778
+ if (next !== viewRef.current)
779
+ setView(next, action === "sort-asc" || action === "sort-desc" || action === "clear-sort" ? "query" : "layout");
780
+ if (action === "group")
781
+ setPanel("group");
782
+ }
783
+ function renderColumnHeader(field, column, gantt = false) {
784
+ const left = (gantt ? ganttLeftOffsets : pinnedOffsets).get(field.id);
785
+ 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);
791
+ }
792
+ function renderMobileCard(row) {
793
+ 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);
795
+ }
796
+ }
797
+ function ToolbarButton({ children, active = false, onClick }) {
798
+ const label = Children.toArray(children).filter((v) => typeof v === "string" || typeof v === "number").join("").trim();
799
+ const icon = label.startsWith("篩選") ? "▽" : label.startsWith("排序") ? "↕" : label.startsWith("分組") ? "▤" : null;
800
+ const count = label.match(/\d+$/)?.[0];
801
+ return _jsxs("button", { type: "button", title: label, "aria-label": label, "aria-pressed": active, className: `${active ? "is-active" : ""} ${icon ? "hy-dv-tool" : ""}`, onClick: onClick, children: [icon && _jsxs("span", { className: "hy-dv-tool-icon", "aria-hidden": true, children: [icon, count && _jsx("small", { children: count })] }), _jsx("span", { className: "hy-dv-tool-label", children: children })] });
802
+ }
803
+ function PanelShell({ title, children, onClose }) {
804
+ return _jsxs("aside", { className: "hy-dv-panel", children: [_jsxs("header", { children: [_jsx("h3", { children: title }), _jsx("button", { type: "button", onClick: onClose, "aria-label": `關閉${title}`, children: "\u00D7" })] }), children] });
805
+ }
806
+ function SortPanel({ fields, view, onChange, onClose }) {
807
+ function update(index, patch) {
808
+ onChange({ ...view, sorts: view.sorts.map((rule, ruleIndex) => ruleIndex === index ? { ...rule, ...patch } : rule) });
809
+ }
810
+ return _jsxs(PanelShell, { title: "\u591A\u6B04\u6392\u5E8F", onClose: onClose, children: [_jsx("p", { children: "\u7531\u4E0A\u5230\u4E0B\u5957\u7528\uFF0C\u7B2C\u4E00\u5217\u512A\u5148\u3002" }), _jsxs("div", { className: "hy-dv-rules", children: [view.sorts.map((rule, index) => _jsxs("div", { className: "hy-dv-rule", children: [_jsx("span", { children: index + 1 }), _jsx("select", { value: rule.fieldId, onChange: (event) => update(index, { fieldId: event.target.value }), children: fields.map((field) => _jsx("option", { value: field.id, disabled: view.sorts.some((item, i) => i !== index && item.fieldId === field.id), children: field.label }, field.id)) }), _jsxs("select", { value: rule.direction, onChange: (event) => update(index, { direction: event.target.value }), children: [_jsx("option", { value: "asc", children: "\u5C0F \u2192 \u5927" }), _jsx("option", { value: "desc", children: "\u5927 \u2192 \u5C0F" })] }), _jsx("button", { type: "button", onClick: () => onChange({ ...view, sorts: view.sorts.filter((_, i) => i !== index) }), children: "\u79FB\u9664" })] }, `${rule.fieldId}-${index}`)), _jsx("button", { type: "button", className: "hy-dv-add", disabled: view.sorts.length >= fields.length, onClick: () => {
811
+ const field = fields.find((item) => !view.sorts.some((rule) => rule.fieldId === item.id));
812
+ if (field)
813
+ onChange({ ...view, sorts: [...view.sorts, { fieldId: field.id, direction: "asc" }] });
814
+ }, children: "\uFF0B \u589E\u52A0\u6392\u5E8F" })] })] });
815
+ }
816
+ function FilterPanel({ fields, view, onChange, onClose }) {
817
+ function update(index, patch) {
818
+ onChange({ ...view, filters: { ...view.filters, rules: view.filters.rules.map((rule, i) => i === index ? { ...rule, ...patch } : rule) } });
819
+ }
820
+ return _jsxs(PanelShell, { title: "\u7BE9\u9078\u689D\u4EF6", onClose: onClose, children: [_jsxs("div", { className: "hy-dv-conjunction", children: [_jsx("span", { children: "\u689D\u4EF6\u95DC\u4FC2" }), _jsxs("select", { value: view.filters.conjunction, onChange: (event) => onChange({ ...view, filters: { ...view.filters, conjunction: event.target.value } }), children: [_jsx("option", { value: "and", children: "\u5168\u90E8\u7B26\u5408\uFF08AND\uFF09" }), _jsx("option", { value: "or", children: "\u4EFB\u4E00\u7B26\u5408\uFF08OR\uFF09" })] })] }), _jsxs("div", { className: "hy-dv-rules", children: [view.filters.rules.map((rule, index) => {
821
+ const field = fields.find((item) => item.id === rule.fieldId) ?? fields[0];
822
+ const operators = getFieldTypeDefinition(field.type).operators;
823
+ return _jsxs("div", { className: "hy-dv-rule hy-dv-filter-rule", children: [_jsx("select", { value: rule.fieldId, onChange: (event) => {
824
+ const nextField = fields.find((item) => item.id === event.target.value);
825
+ update(index, { fieldId: nextField.id, operator: getFieldTypeDefinition(nextField.type).operators[0], value: undefined });
826
+ }, children: fields.map((item) => _jsx("option", { value: item.id, children: item.label }, item.id)) }), _jsx("select", { value: rule.operator, onChange: (event) => update(index, { operator: event.target.value }), children: operators.map((operator) => _jsx("option", { value: operator, children: FILTER_OPERATOR_LABELS[operator] }, operator)) }), _jsx(FilterValueInput, { field: field, rule: rule, onChange: (value) => update(index, { value }) }), _jsx("button", { type: "button", onClick: () => onChange({ ...view, filters: { ...view.filters, rules: view.filters.rules.filter((_, i) => i !== index) } }), children: "\u79FB\u9664" })] }, rule.id);
827
+ }), _jsx("button", { type: "button", className: "hy-dv-add", disabled: fields.length === 0 || view.filters.rules.length >= 20, onClick: () => {
828
+ const field = fields[0];
829
+ if (!field)
830
+ return;
831
+ onChange({ ...view, filters: { ...view.filters, rules: [...view.filters.rules, { id: createRuleId(), fieldId: field.id, operator: getFieldTypeDefinition(field.type).operators[0] }] } });
832
+ }, children: "\uFF0B \u589E\u52A0\u689D\u4EF6" })] })] });
833
+ }
834
+ function ColumnPanel({ fields, view, onChange, onClose, dragged, setDragged, drop }) {
835
+ const definitions = new Map(fields.map((field) => [field.id, field]));
836
+ const visibleCount = view.columns.filter((column) => column.visible).length;
837
+ function update(fieldId, patch) { onChange({ ...view, columns: view.columns.map((column) => column.fieldId === fieldId ? { ...column, ...patch } : column) }); }
838
+ function move(fieldId, direction) {
839
+ const columns = moveColumnSetting(view.columns, fieldId, direction);
840
+ if (columns !== view.columns)
841
+ onChange({ ...view, columns });
842
+ }
843
+ return _jsxs(PanelShell, { title: "\u96FB\u8166\u8868\u683C\u6B04\u4F4D", onClose: onClose, children: [_jsx("p", { children: "\u8ABF\u6574\u672C\u8996\u5716\u96FB\u8166\u8868\u683C\u8207\u7518\u7279\u5DE6\u8868\u7684\u6B04\u4F4D\uFF1B\u4E0D\u6703\u8986\u84CB\u624B\u6A5F\u986F\u793A\u6B04\u4F4D\u3002\u53EF\u62D6\u66F3\u6216\u7528\u6309\u9215\u6392\u5E8F\uFF1B\u56FA\u5B9A\u6B04\u4F4D\u7DAD\u6301\u5DE6\u53F3\u5340\u57DF\uFF0C\u79FB\u52D5\u9650\u540C\u4E00\u56FA\u5B9A\u5340\u57DF\u3002\u5BEC\u5EA6 56\u2013600px\u3002" }), _jsx("div", { className: "hy-dv-column-list", children: view.columns.map((column) => {
844
+ const label = definitions.get(column.fieldId)?.label ?? "未命名欄位";
845
+ const peers = columnMovePeers(view.columns, column.fieldId);
846
+ const index = peers.findIndex((peer) => peer.fieldId === column.fieldId);
847
+ return _jsxs("div", { draggable: true, onDragStart: () => setDragged(column.fieldId), onDragEnd: () => setDragged(null), onDragOver: (event) => event.preventDefault(), onDrop: () => drop(column.fieldId), className: dragged === column.fieldId ? "is-dragging" : "", children: [_jsx("span", { className: "hy-dv-drag", "aria-hidden": true, children: "\u283F" }), _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: column.visible, disabled: column.visible && visibleCount === 1, onChange: (event) => update(column.fieldId, { visible: event.target.checked }) }), _jsx("span", { children: label })] }), _jsxs("select", { "aria-label": `${label}固定位置`, value: column.pinned ?? "none", onChange: (event) => update(column.fieldId, { pinned: event.target.value === "none" ? null : event.target.value }), children: [_jsx("option", { value: "none", children: "\u4E0D\u56FA\u5B9A" }), _jsx("option", { value: "left", children: "\u56FA\u5B9A\u5DE6\u5074" }), _jsx("option", { value: "right", children: "\u56FA\u5B9A\u53F3\u5074" })] }), _jsx("input", { "aria-label": `${label}欄寬`, type: "number", min: 56, max: 600, value: column.width, onChange: (event) => update(column.fieldId, { width: normalizeColumnWidth(Number(event.target.value)) }) }), _jsxs("div", { className: "hy-dv-field-order-actions", children: [_jsx("button", { type: "button", title: "\u79FB\u5230\u6700\u4E0A", "aria-label": `移到最上 ${label}`, disabled: index <= 0, onClick: () => move(column.fieldId, "first"), children: "\u21C8" }), _jsx("button", { type: "button", title: "\u4E0A\u79FB", "aria-label": `上移 ${label}`, disabled: index <= 0, onClick: () => move(column.fieldId, "previous"), children: "\u2191" }), _jsx("button", { type: "button", title: "\u4E0B\u79FB", "aria-label": `下移 ${label}`, disabled: index < 0 || index === peers.length - 1, onClick: () => move(column.fieldId, "next"), children: "\u2193" }), _jsx("button", { type: "button", title: "\u79FB\u5230\u6700\u4E0B", "aria-label": `移到最下 ${label}`, disabled: index < 0 || index === peers.length - 1, onClick: () => move(column.fieldId, "last"), children: "\u21CA" })] })] }, column.fieldId);
848
+ }) })] });
849
+ }
850
+ function SaveBadge({ state, prefix, message }) {
851
+ if (state === "idle")
852
+ return null;
853
+ const labels = { saving: "儲存中…", saved: "已儲存", failed: "儲存失敗", conflict: "版本衝突", readonly: "唯讀" };
854
+ return _jsxs("small", { className: `hy-dv-save is-${state}`, role: "status", title: message, children: [prefix ? `${prefix}:` : "", labels[state]] });
855
+ }
856
+ function StateCard({ title, message, tone = "normal" }) {
857
+ return _jsxs("div", { className: `hy-dv-state is-${tone}`, role: tone === "error" ? "alert" : "status", children: [_jsx("strong", { children: title }), _jsx("p", { children: message })] });
858
+ }
859
+ function LoadingState() {
860
+ return _jsxs("div", { className: "hy-dv-loading", "aria-label": "\u8F09\u5165\u4E2D", children: [_jsx("span", {}), _jsx("span", {}), _jsx("span", {}), _jsx("span", {})] });
861
+ }
862
+ function collectGroupKeys(nodes) {
863
+ return nodes.flatMap((node) => node.fieldId ? [node.key, ...collectGroupKeys(node.children)] : collectGroupKeys(node.children));
864
+ }
865
+ function createRuleId() {
866
+ return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `filter-${Date.now()}-${Math.random().toString(36).slice(2)}`;
867
+ }
868
+ function flattenGroups(nodes, collapsed) {
869
+ const result = [];
870
+ for (const node of nodes) {
871
+ if (node.fieldId)
872
+ result.push({ kind: "group", node });
873
+ if (collapsed.has(node.key))
874
+ continue;
875
+ if (node.children.length)
876
+ result.push(...flattenGroups(node.children, collapsed));
877
+ else
878
+ for (const row of node.rows)
879
+ result.push({ kind: "row", row });
880
+ }
881
+ return result;
882
+ }
883
+ //# sourceMappingURL=data-view.js.map