@script-it/json-views-react 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +301 -0
- package/THIRD_PARTY_NOTICES.txt +4917 -0
- package/dist/browser-device.d.ts +21 -0
- package/dist/browser-device.js +22 -0
- package/dist/file-content-edit-state.d.ts +10 -0
- package/dist/file-content-edit-state.js +1 -0
- package/dist/file-content-view.d.ts +8 -0
- package/dist/file-content-view.js +7 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +8 -0
- package/dist/json/copy-json.d.ts +15 -0
- package/dist/json/copy-json.js +52 -0
- package/dist/json/html-preview.d.ts +5 -0
- package/dist/json/html-preview.js +12 -0
- package/dist/json/html-view/native-control.d.ts +44 -0
- package/dist/json/html-view/native-control.js +96 -0
- package/dist/json/html-view/settings.d.ts +2 -0
- package/dist/json/html-view/settings.js +58 -0
- package/dist/json/html-view/template.d.ts +12 -0
- package/dist/json/html-view/template.js +55 -0
- package/dist/json/html-view.d.ts +9 -0
- package/dist/json/html-view.js +163 -0
- package/dist/json/json-content.d.ts +61 -0
- package/dist/json/json-content.js +433 -0
- package/dist/json/json-source-editor.d.ts +36 -0
- package/dist/json/json-source-editor.js +203 -0
- package/dist/json/json-view.d.ts +3 -0
- package/dist/json/json-view.js +322 -0
- package/dist/json/kanban-view.d.ts +20 -0
- package/dist/json/kanban-view.js +328 -0
- package/dist/json/long-text.d.ts +3 -0
- package/dist/json/long-text.js +6 -0
- package/dist/json/record-view.d.ts +38 -0
- package/dist/json/record-view.js +85 -0
- package/dist/json/row-mutations.d.ts +7 -0
- package/dist/json/row-mutations.js +93 -0
- package/dist/json/schema-value.d.ts +57 -0
- package/dist/json/schema-value.js +294 -0
- package/dist/json/source-default.d.ts +1 -0
- package/dist/json/source-default.js +1 -0
- package/dist/json/source-edit-capabilities.d.ts +8 -0
- package/dist/json/source-edit-capabilities.js +22 -0
- package/dist/json/source-literals.d.ts +15 -0
- package/dist/json/source-literals.js +24 -0
- package/dist/json/source-recovery.d.ts +32 -0
- package/dist/json/source-recovery.js +38 -0
- package/dist/json/source-selection.d.ts +3 -0
- package/dist/json/source-selection.js +16 -0
- package/dist/json/table-view.d.ts +20 -0
- package/dist/json/table-view.js +300 -0
- package/dist/json/use-kanban-card-drag.d.ts +13 -0
- package/dist/json/use-kanban-card-drag.js +255 -0
- package/dist/json/use-kanban-column-drag.d.ts +14 -0
- package/dist/json/use-kanban-column-drag.js +179 -0
- package/dist/json/view-diagnostics.d.ts +4 -0
- package/dist/json/view-diagnostics.js +26 -0
- package/dist/json/view-model.d.ts +27 -0
- package/dist/json/view-model.js +180 -0
- package/dist/json/view-save-status.d.ts +12 -0
- package/dist/json/view-save-status.js +38 -0
- package/dist/json/view-settings.d.ts +36 -0
- package/dist/json/view-settings.js +661 -0
- package/dist/json/view-types.d.ts +47 -0
- package/dist/json/view-types.js +4 -0
- package/dist/json/viewers/general/general-json-view.d.ts +46 -0
- package/dist/json/viewers/general/general-json-view.js +163 -0
- package/dist/json/viewers/general/json-flatten.d.ts +33 -0
- package/dist/json/viewers/general/json-flatten.js +51 -0
- package/dist/json/viewers/general/json-node-view.d.ts +43 -0
- package/dist/json/viewers/general/json-node-view.js +215 -0
- package/dist/lib/clipboard.d.ts +1 -0
- package/dist/lib/clipboard.js +24 -0
- package/dist/lib/cn.d.ts +2 -0
- package/dist/lib/cn.js +5 -0
- package/dist/lib/file-data.d.ts +7 -0
- package/dist/lib/file-data.js +5 -0
- package/dist/lib/floating-portal-selectors.d.ts +4 -0
- package/dist/lib/floating-portal-selectors.js +4 -0
- package/dist/lib/time.d.ts +24 -0
- package/dist/lib/time.js +60 -0
- package/dist/lib/ui-size.d.ts +28 -0
- package/dist/lib/ui-size.js +39 -0
- package/dist/lib/use-anchored-position.d.ts +5 -0
- package/dist/lib/use-anchored-position.js +65 -0
- package/dist/lib/use-dismiss.d.ts +31 -0
- package/dist/lib/use-dismiss.js +65 -0
- package/dist/primitives/boolean-toggle.d.ts +10 -0
- package/dist/primitives/boolean-toggle.js +11 -0
- package/dist/primitives/button.d.ts +11 -0
- package/dist/primitives/button.js +34 -0
- package/dist/primitives/checkbox.d.ts +4 -0
- package/dist/primitives/checkbox.js +8 -0
- package/dist/primitives/choice-control.d.ts +29 -0
- package/dist/primitives/choice-control.js +131 -0
- package/dist/primitives/dialog.d.ts +29 -0
- package/dist/primitives/dialog.js +25 -0
- package/dist/primitives/dropdown-menu.d.ts +37 -0
- package/dist/primitives/dropdown-menu.js +39 -0
- package/dist/primitives/inline-feedback-action.d.ts +13 -0
- package/dist/primitives/inline-feedback-action.js +6 -0
- package/dist/primitives/menu-select.d.ts +12 -0
- package/dist/primitives/menu-select.js +7 -0
- package/dist/primitives/multi-select.d.ts +18 -0
- package/dist/primitives/multi-select.js +7 -0
- package/dist/primitives/number-input.d.ts +10 -0
- package/dist/primitives/number-input.js +8 -0
- package/dist/primitives/option-pill.d.ts +13 -0
- package/dist/primitives/option-pill.js +50 -0
- package/dist/primitives/tabs.d.ts +7 -0
- package/dist/primitives/tabs.js +12 -0
- package/dist/primitives/textarea.d.ts +4 -0
- package/dist/primitives/textarea.js +8 -0
- package/dist/primitives/tooltip.d.ts +31 -0
- package/dist/primitives/tooltip.js +37 -0
- package/dist/standalone.css +113 -0
- package/dist/structured-data/atomic-value-editor.d.ts +33 -0
- package/dist/structured-data/atomic-value-editor.js +195 -0
- package/dist/structured-data/atomic-value.d.ts +3 -0
- package/dist/structured-data/atomic-value.js +6 -0
- package/dist/structured-data/column-controls.d.ts +21 -0
- package/dist/structured-data/column-controls.js +180 -0
- package/dist/structured-data/copy-json-action.d.ts +10 -0
- package/dist/structured-data/copy-json-action.js +87 -0
- package/dist/structured-data/delete-row-action.d.ts +10 -0
- package/dist/structured-data/delete-row-action.js +37 -0
- package/dist/structured-data/edit-base.d.ts +3 -0
- package/dist/structured-data/edit-base.js +8 -0
- package/dist/structured-data/filter-value.d.ts +2 -0
- package/dist/structured-data/filter-value.js +28 -0
- package/dist/structured-data/row-selection-icon.d.ts +9 -0
- package/dist/structured-data/row-selection-icon.js +6 -0
- package/dist/structured-data/structured-cell-fill-context.d.ts +6 -0
- package/dist/structured-data/structured-cell-fill-context.js +7 -0
- package/dist/structured-data/structured-data-content.d.ts +1 -0
- package/dist/structured-data/structured-data-content.js +1 -0
- package/dist/structured-data/table-types.d.ts +108 -0
- package/dist/structured-data/table-types.js +1 -0
- package/dist/structured-data/use-metadata-persistence.d.ts +15 -0
- package/dist/structured-data/use-metadata-persistence.js +18 -0
- package/dist/structured-data/use-optimistic-text-document.d.ts +22 -0
- package/dist/structured-data/use-optimistic-text-document.js +30 -0
- package/dist/structured-data/value-cell.d.ts +15 -0
- package/dist/structured-data/value-cell.js +81 -0
- package/dist/styles.css +2 -0
- package/dist/surface.d.ts +14 -0
- package/dist/surface.js +73 -0
- package/dist/tabular-data-view.d.ts +5 -0
- package/dist/tabular-data-view.js +493 -0
- package/dist/viewer-state.d.ts +72 -0
- package/dist/viewer-state.js +79 -0
- package/dist/widget-registry.d.ts +60 -0
- package/dist/widget-registry.js +126 -0
- package/dist/widgets/boolean-widget.d.ts +2 -0
- package/dist/widgets/boolean-widget.js +15 -0
- package/dist/widgets/date-picker-widget.d.ts +18 -0
- package/dist/widgets/date-picker-widget.js +265 -0
- package/dist/widgets/dropdown-widget.d.ts +10 -0
- package/dist/widgets/dropdown-widget.js +19 -0
- package/dist/widgets/multi-select-widget.d.ts +11 -0
- package/dist/widgets/multi-select-widget.js +46 -0
- package/dist/widgets/number-widget.d.ts +2 -0
- package/dist/widgets/number-widget.js +19 -0
- package/dist/widgets/read-only-field.d.ts +36 -0
- package/dist/widgets/read-only-field.js +39 -0
- package/dist/widgets/text-widget.d.ts +2 -0
- package/dist/widgets/text-widget.js +19 -0
- package/dist/widgets/textarea-widget.d.ts +2 -0
- package/dist/widgets/textarea-widget.js +34 -0
- package/dist/widgets/types.d.ts +53 -0
- package/dist/widgets/types.js +1 -0
- package/package.json +83 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { parseFilterValue } from './structured-data/filter-value.js';
|
|
3
|
+
import { compareJsonValues, matchesJsonValueFilter, jsonValueText } from '@script-it/json-views-core';
|
|
4
|
+
import { useJsonSourceLiterals } from './json/source-literals.js';
|
|
5
|
+
import { AddColumnHeader, ColumnHeaderMenu } from './structured-data/column-controls.js';
|
|
6
|
+
import { ValueCell } from './structured-data/value-cell.js';
|
|
7
|
+
export { ExpandValueButton, ValueCell, ValueCellContent } from './structured-data/value-cell.js';
|
|
8
|
+
import { useViewerState } from './viewer-state.js';
|
|
9
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
10
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
11
|
+
import { File, Plus } from 'lucide-react';
|
|
12
|
+
import { UI_SIZE_SCALE } from './lib/ui-size.js';
|
|
13
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from './primitives/tooltip.js';
|
|
14
|
+
import { cn } from './lib/cn.js';
|
|
15
|
+
import { Checkbox } from './primitives/checkbox.js';
|
|
16
|
+
import { StructuredCellFillContext } from './structured-data/structured-cell-fill-context.js';
|
|
17
|
+
import { DeleteSelectedRowsAction } from './structured-data/delete-row-action.js';
|
|
18
|
+
import { RowSelectionIcon } from './structured-data/row-selection-icon.js';
|
|
19
|
+
import { CopyJsonAction } from './structured-data/copy-json-action.js';
|
|
20
|
+
import { useJsonViewsDevice } from './browser-device.js';
|
|
21
|
+
import { StructuredValueCellFrame } from './structured-data/atomic-value-editor.js';
|
|
22
|
+
const DEFAULT_COLUMN_WIDTH = 200;
|
|
23
|
+
const MIN_COLUMN_WIDTH = 80;
|
|
24
|
+
const MAX_CACHED_COLUMN_WIDTH = 4000;
|
|
25
|
+
const MIN_VISIBLE_COLUMNS = 3;
|
|
26
|
+
/** Row height at the small (100%) UI size; scaled by the active `UI_SIZE_SCALE`. */
|
|
27
|
+
const ROW_HEIGHT = 45;
|
|
28
|
+
export const TABULAR_DATA_FRAME_CLASS = 'overflow-hidden border-y border-border bg-background';
|
|
29
|
+
function normalizeCollapsedColumns(hiddenColumns, allColumns) {
|
|
30
|
+
const minimumVisibleColumns = Math.min(MIN_VISIBLE_COLUMNS, allColumns.length);
|
|
31
|
+
const maxHiddenColumns = Math.max(0, allColumns.length - minimumVisibleColumns);
|
|
32
|
+
const normalizedHiddenColumns = allColumns
|
|
33
|
+
.filter((column) => hiddenColumns.includes(column))
|
|
34
|
+
.slice(0, maxHiddenColumns);
|
|
35
|
+
return new Set(normalizedHiddenColumns);
|
|
36
|
+
}
|
|
37
|
+
function normalizeColumnWidths(widths, columns) {
|
|
38
|
+
if (!widths)
|
|
39
|
+
return {};
|
|
40
|
+
return Object.fromEntries(columns.flatMap((column) => {
|
|
41
|
+
const width = widths[column];
|
|
42
|
+
return Number.isFinite(width) && width >= MIN_COLUMN_WIDTH
|
|
43
|
+
? [[column, Math.min(width, MAX_CACHED_COLUMN_WIDTH)]]
|
|
44
|
+
: [];
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
function restoreSort(sort, columns, stateKeys) {
|
|
48
|
+
if (!sort || (sort.direction !== 'asc' && sort.direction !== 'desc'))
|
|
49
|
+
return null;
|
|
50
|
+
const column = columns.find((candidate) => (stateKeys?.[candidate] ?? candidate) === sort.column);
|
|
51
|
+
return column === undefined ? null : { column, direction: sort.direction };
|
|
52
|
+
}
|
|
53
|
+
function restoreCollapsedColumns(hidden, columns, stateKeys) {
|
|
54
|
+
return normalizeCollapsedColumns(columns.filter((column) => hidden.includes(stateKeys?.[column] ?? column)), columns);
|
|
55
|
+
}
|
|
56
|
+
function restoreFilters(filters, columns, stateKeys) {
|
|
57
|
+
if (!filters)
|
|
58
|
+
return [];
|
|
59
|
+
return filters.flatMap((filter) => {
|
|
60
|
+
const column = columns.find((candidate) => (stateKeys?.[candidate] ?? candidate) === filter.column);
|
|
61
|
+
return column && filter.operator.trim()
|
|
62
|
+
? [{ column, operator: filter.operator, ...(filter.value === undefined ? {} : { value: filter.value }) }]
|
|
63
|
+
: [];
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function primitiveFilterValue(value, sample) {
|
|
67
|
+
const populated = sample.filter((item) => item !== undefined && item !== null);
|
|
68
|
+
const kinds = new Set(populated.map((item) => typeof item));
|
|
69
|
+
return parseFilterValue(value, kinds.size === 1 ? [...kinds][0] : undefined);
|
|
70
|
+
}
|
|
71
|
+
function parseColumnFilter(filter, sample) {
|
|
72
|
+
if (filter.operator === 'isEmpty' || filter.operator === 'isNotEmpty')
|
|
73
|
+
return { operator: filter.operator };
|
|
74
|
+
const text = typeof filter.value === 'string' ? filter.value : jsonValueText(filter.value ?? '');
|
|
75
|
+
if (filter.operator === 'in' || filter.operator === 'notIn') {
|
|
76
|
+
return {
|
|
77
|
+
operator: filter.operator,
|
|
78
|
+
value: text.split(',').map((value) => primitiveFilterValue(value.trim(), sample)).filter((value) => value !== ''),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return { operator: filter.operator, value: primitiveFilterValue(text, sample) };
|
|
82
|
+
}
|
|
83
|
+
function filterValueSuggestions(values) {
|
|
84
|
+
const suggestions = new Map();
|
|
85
|
+
for (const value of values) {
|
|
86
|
+
if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean')
|
|
87
|
+
continue;
|
|
88
|
+
const key = `${typeof value}:${String(value)}`;
|
|
89
|
+
if (!suggestions.has(key))
|
|
90
|
+
suggestions.set(key, value);
|
|
91
|
+
if (suggestions.size > 20)
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
return Array.from(suggestions.values());
|
|
95
|
+
}
|
|
96
|
+
export function TabularObjectArrayView({ arr, rowKeys, getCellSourcePath, columns, columnLabels, columnStateKeys, filePath, viewStateKey, onOpenCell, onVisibleRowsChange, renderCell, recordNavigation, onAddRow, onDeleteRows, getRowsCopyText, getRowsReferenceText, rowAdditionDisabled = false, rowDeletionDisabled = false, initialCell, uiSize = 'small', fillHeight = false, presentationControls = true, activeColumnFilters, activeColumnSort, columnFilterOperators, columnFilterValues, onColumnFilter, onColumnSort, onColumnRename, onColumnHide, onColumnMove, onColumnDelete, onAddColumn, }) {
|
|
97
|
+
const numberAtPath = useJsonSourceLiterals();
|
|
98
|
+
const { tables: tableState } = useViewerState();
|
|
99
|
+
const compact = useJsonViewsDevice() === 'mobile';
|
|
100
|
+
const cacheKey = JSON.stringify([filePath, viewStateKey]);
|
|
101
|
+
const savedTableState = filePath ? tableState.get(cacheKey) : undefined;
|
|
102
|
+
const columnLabel = (column) => columnLabels?.[column] ?? column;
|
|
103
|
+
const columnStateKey = (column) => columnStateKeys?.[column] ?? column;
|
|
104
|
+
const savedCollapsedColumns = presentationControls && filePath
|
|
105
|
+
? (savedTableState?.collapsedColumns ?? [])
|
|
106
|
+
: [];
|
|
107
|
+
const [collapsedColumns, setLocalCollapsedColumns] = useState(() => restoreCollapsedColumns(savedCollapsedColumns, columns, columnStateKeys));
|
|
108
|
+
const selectable = onDeleteRows !== undefined || getRowsCopyText !== undefined;
|
|
109
|
+
const [selectedRows, setSelectedRows] = useState(() => new Set());
|
|
110
|
+
const [rowAdditionError, setRowAdditionError] = useState();
|
|
111
|
+
// Selected indices address rows of the current source; any content change
|
|
112
|
+
// may reshuffle them, so a new array drops the selection during render
|
|
113
|
+
// (React's "reset state on prop change" pattern) before it can mis-target.
|
|
114
|
+
const [seenArr, setSeenArr] = useState(arr);
|
|
115
|
+
if (seenArr !== arr) {
|
|
116
|
+
setSeenArr(arr);
|
|
117
|
+
if (selectedRows.size > 0)
|
|
118
|
+
setSelectedRows(new Set());
|
|
119
|
+
}
|
|
120
|
+
const [columnWidths, setColumnWidths] = useState(() => normalizeColumnWidths(savedTableState?.columnWidths, columns.map(columnStateKey)));
|
|
121
|
+
const columnWidthsRef = useRef(columnWidths);
|
|
122
|
+
const [dialogContainer, setDialogContainer] = useState(null);
|
|
123
|
+
const [hiddenRowHeight, setHiddenRowHeight] = useState(0);
|
|
124
|
+
const [draggedColumn, setDraggedColumn] = useState();
|
|
125
|
+
const [dropTarget, setDropTarget] = useState();
|
|
126
|
+
const draggedColumnRef = useRef(undefined);
|
|
127
|
+
const resizeRef = useRef(null);
|
|
128
|
+
const resizeCleanupRef = useRef(null);
|
|
129
|
+
const hiddenRowRef = useRef(null);
|
|
130
|
+
const saveTableState = useCallback((patch) => {
|
|
131
|
+
if (!filePath)
|
|
132
|
+
return;
|
|
133
|
+
tableState.set(cacheKey, { ...tableState.get(cacheKey), ...patch });
|
|
134
|
+
}, [cacheKey, filePath, tableState]);
|
|
135
|
+
const onResizeStart = useCallback((col, e) => {
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
e.stopPropagation();
|
|
138
|
+
const th = e.target.closest('th');
|
|
139
|
+
if (!th)
|
|
140
|
+
return;
|
|
141
|
+
resizeRef.current = {
|
|
142
|
+
col,
|
|
143
|
+
startX: e.clientX,
|
|
144
|
+
startWidth: th.getBoundingClientRect().width,
|
|
145
|
+
};
|
|
146
|
+
let rafId = null;
|
|
147
|
+
const onMouseMove = (ev) => {
|
|
148
|
+
if (!resizeRef.current)
|
|
149
|
+
return;
|
|
150
|
+
const newWidth = Math.max(MIN_COLUMN_WIDTH, resizeRef.current.startWidth + (ev.clientX - resizeRef.current.startX));
|
|
151
|
+
columnWidthsRef.current = { ...columnWidthsRef.current, [col]: newWidth };
|
|
152
|
+
if (rafId !== null)
|
|
153
|
+
cancelAnimationFrame(rafId);
|
|
154
|
+
rafId = requestAnimationFrame(() => {
|
|
155
|
+
setColumnWidths(columnWidthsRef.current);
|
|
156
|
+
rafId = null;
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
const onMouseUp = () => {
|
|
160
|
+
if (rafId !== null)
|
|
161
|
+
cancelAnimationFrame(rafId);
|
|
162
|
+
setColumnWidths(columnWidthsRef.current);
|
|
163
|
+
saveTableState({ columnWidths: columnWidthsRef.current });
|
|
164
|
+
resizeRef.current = null;
|
|
165
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
166
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
167
|
+
document.body.style.cursor = previousCursor;
|
|
168
|
+
document.body.style.userSelect = previousUserSelect;
|
|
169
|
+
resizeCleanupRef.current = null;
|
|
170
|
+
};
|
|
171
|
+
const previousCursor = document.body.style.cursor;
|
|
172
|
+
const previousUserSelect = document.body.style.userSelect;
|
|
173
|
+
document.body.style.cursor = 'col-resize';
|
|
174
|
+
document.body.style.userSelect = 'none';
|
|
175
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
176
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
177
|
+
resizeCleanupRef.current = onMouseUp;
|
|
178
|
+
}, [saveTableState]);
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
return () => {
|
|
181
|
+
resizeCleanupRef.current?.();
|
|
182
|
+
};
|
|
183
|
+
}, []);
|
|
184
|
+
const columnsKey = JSON.stringify([columns, columnStateKeys]);
|
|
185
|
+
// Column contents, rather than the caller's array identity, define table state.
|
|
186
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
187
|
+
const stableColumns = useMemo(() => columns, [columnsKey]);
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
setLocalCollapsedColumns(restoreCollapsedColumns(presentationControls && filePath ? (tableState.get(cacheKey)?.collapsedColumns ?? []) : [], stableColumns, columnStateKeys));
|
|
190
|
+
const nextWidths = normalizeColumnWidths(filePath ? tableState.get(cacheKey)?.columnWidths : undefined, stableColumns.map(columnStateKey));
|
|
191
|
+
columnWidthsRef.current = nextWidths;
|
|
192
|
+
setColumnWidths(nextWidths);
|
|
193
|
+
}, [filePath, presentationControls, stableColumns, columnsKey, cacheKey, tableState]);
|
|
194
|
+
const visibleColumns = stableColumns.filter((col) => !collapsedColumns.has(col));
|
|
195
|
+
const visibleTitleColumn = recordNavigation && visibleColumns.includes(recordNavigation.titleColumn)
|
|
196
|
+
? recordNavigation.titleColumn
|
|
197
|
+
: undefined;
|
|
198
|
+
const controlColumn = visibleTitleColumn ?? visibleColumns[0];
|
|
199
|
+
const hiddenColumns = stableColumns.filter((col) => collapsedColumns.has(col));
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
const element = hiddenRowRef.current;
|
|
202
|
+
if (!element) {
|
|
203
|
+
setHiddenRowHeight(0);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const updateHeight = () => {
|
|
207
|
+
setHiddenRowHeight(element.getBoundingClientRect().height);
|
|
208
|
+
};
|
|
209
|
+
updateHeight();
|
|
210
|
+
if (typeof ResizeObserver === 'undefined')
|
|
211
|
+
return;
|
|
212
|
+
const observer = new ResizeObserver(updateHeight);
|
|
213
|
+
observer.observe(element);
|
|
214
|
+
return () => {
|
|
215
|
+
observer.disconnect();
|
|
216
|
+
};
|
|
217
|
+
}, [hiddenColumns.length]);
|
|
218
|
+
const toggleColumn = (col) => {
|
|
219
|
+
if (!presentationControls)
|
|
220
|
+
return;
|
|
221
|
+
const next = new Set(collapsedColumns);
|
|
222
|
+
if (next.has(col))
|
|
223
|
+
next.delete(col);
|
|
224
|
+
else
|
|
225
|
+
next.add(col);
|
|
226
|
+
const normalized = normalizeCollapsedColumns(Array.from(next), stableColumns);
|
|
227
|
+
setLocalCollapsedColumns(normalized);
|
|
228
|
+
saveTableState({ collapsedColumns: Array.from(normalized, columnStateKey) });
|
|
229
|
+
};
|
|
230
|
+
const [sortConfig, setSortConfig] = useState(() => (presentationControls ? restoreSort(savedTableState?.sort, columns, columnStateKeys) : null));
|
|
231
|
+
const [filterConfig, setFilterConfig] = useState(() => (presentationControls ? restoreFilters(savedTableState?.filters, columns, columnStateKeys) : []));
|
|
232
|
+
const effectiveSort = activeColumnSort === undefined ? sortConfig : activeColumnSort;
|
|
233
|
+
const effectiveFilters = activeColumnFilters === undefined ? filterConfig : activeColumnFilters;
|
|
234
|
+
const sortColumn = onColumnSort ?? (presentationControls ? (column, direction) => {
|
|
235
|
+
const next = direction ? { column, direction } : null;
|
|
236
|
+
setSortConfig(next);
|
|
237
|
+
saveTableState({ sort: direction ? { column: columnStateKey(column), direction } : null });
|
|
238
|
+
} : undefined);
|
|
239
|
+
const filterColumn = onColumnFilter ? (column, filter) => {
|
|
240
|
+
const sample = (columnFilterValues?.[column] ?? arr.map((row) => row[column]));
|
|
241
|
+
return onColumnFilter(column, filter ? parseColumnFilter(filter, sample) : null);
|
|
242
|
+
} : presentationControls ? (column, filter) => {
|
|
243
|
+
const sample = (columnFilterValues?.[column] ?? arr.map((row) => row[column]));
|
|
244
|
+
const next = [
|
|
245
|
+
...filterConfig.filter((candidate) => candidate.column !== column),
|
|
246
|
+
...(filter ? [{ column, ...parseColumnFilter(filter, sample) }] : []),
|
|
247
|
+
];
|
|
248
|
+
setSelectedRows(new Set());
|
|
249
|
+
setFilterConfig(next);
|
|
250
|
+
saveTableState({ filters: next.map((candidate) => ({
|
|
251
|
+
...candidate,
|
|
252
|
+
column: columnStateKey(candidate.column),
|
|
253
|
+
})) });
|
|
254
|
+
} : undefined;
|
|
255
|
+
const hideColumn = onColumnHide ?? (presentationControls ? (column) => toggleColumn(column) : undefined);
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
setSortConfig(presentationControls ? restoreSort(tableState.get(cacheKey)?.sort, stableColumns, columnStateKeys) : null);
|
|
258
|
+
setFilterConfig(presentationControls ? restoreFilters(tableState.get(cacheKey)?.filters, stableColumns, columnStateKeys) : []);
|
|
259
|
+
}, [cacheKey, presentationControls, stableColumns, columnsKey, tableState]);
|
|
260
|
+
const indexedData = useMemo(() => {
|
|
261
|
+
return arr.map((item, i) => ({ item, originalIndex: i }));
|
|
262
|
+
}, [arr]);
|
|
263
|
+
const filteredData = useMemo(() => {
|
|
264
|
+
if (activeColumnFilters !== undefined || filterConfig.length === 0)
|
|
265
|
+
return indexedData;
|
|
266
|
+
return indexedData.filter(({ item, originalIndex }) => filterConfig.every((filter) => matchesJsonValueFilter(Object.hasOwn(item, filter.column) ? item[filter.column] : undefined, filter.operator, filter.value, { numberAtPath, leftPath: getCellSourcePath?.(originalIndex, filter.column) })));
|
|
267
|
+
}, [activeColumnFilters, filterConfig, indexedData, numberAtPath, getCellSourcePath]);
|
|
268
|
+
const sortedData = useMemo(() => {
|
|
269
|
+
if (!sortConfig || activeColumnSort !== undefined)
|
|
270
|
+
return filteredData;
|
|
271
|
+
const { column, direction } = sortConfig;
|
|
272
|
+
return [...filteredData].sort((a, b) => {
|
|
273
|
+
const aVal = Object.hasOwn(a.item, column) ? a.item[column] : undefined;
|
|
274
|
+
const bVal = Object.hasOwn(b.item, column) ? b.item[column] : undefined;
|
|
275
|
+
if (aVal == null && bVal == null)
|
|
276
|
+
return 0;
|
|
277
|
+
if (aVal == null)
|
|
278
|
+
return 1;
|
|
279
|
+
if (bVal == null)
|
|
280
|
+
return -1;
|
|
281
|
+
const cmp = compareJsonValues(aVal, bVal, { numberAtPath, leftPath: getCellSourcePath?.(a.originalIndex, column), rightPath: getCellSourcePath?.(b.originalIndex, column) });
|
|
282
|
+
return direction === 'asc' ? cmp : -cmp;
|
|
283
|
+
});
|
|
284
|
+
}, [activeColumnSort, filteredData, sortConfig, numberAtPath, getCellSourcePath]);
|
|
285
|
+
const hasActiveFilters = effectiveFilters.length > 0;
|
|
286
|
+
useEffect(() => {
|
|
287
|
+
onVisibleRowsChange?.(sortedData.map(({ originalIndex }) => originalIndex), hasActiveFilters);
|
|
288
|
+
}, [hasActiveFilters, onVisibleRowsChange, sortedData]);
|
|
289
|
+
// Compact widths are a presentation constraint, never persisted over the
|
|
290
|
+
// user's desktop column sizes. Every column and the same editors remain.
|
|
291
|
+
const visibleColumnWidths = visibleColumns.map((col) => compact
|
|
292
|
+
? Math.min(columnWidths[columnStateKey(col)] ?? DEFAULT_COLUMN_WIDTH, col === controlColumn ? 168 : 184)
|
|
293
|
+
: columnWidths[columnStateKey(col)] ?? DEFAULT_COLUMN_WIDTH);
|
|
294
|
+
const rowCountLabel = `${sortedData.length.toLocaleString()} ${sortedData.length === 1 ? 'row' : 'rows'} · ${stableColumns.length.toLocaleString()} ${stableColumns.length === 1 ? 'field' : 'fields'}`;
|
|
295
|
+
const canHideMoreColumns = visibleColumns.length > Math.min(MIN_VISIBLE_COLUMNS, stableColumns.length);
|
|
296
|
+
const scrollContainerRef = useRef(null);
|
|
297
|
+
const scrollSaveTimer = useRef(undefined);
|
|
298
|
+
const pendingScrollPosition = useRef(undefined);
|
|
299
|
+
const flushScrollPosition = useCallback(() => {
|
|
300
|
+
if (scrollSaveTimer.current)
|
|
301
|
+
clearTimeout(scrollSaveTimer.current);
|
|
302
|
+
scrollSaveTimer.current = undefined;
|
|
303
|
+
if (!pendingScrollPosition.current)
|
|
304
|
+
return;
|
|
305
|
+
saveTableState(pendingScrollPosition.current);
|
|
306
|
+
pendingScrollPosition.current = undefined;
|
|
307
|
+
}, [saveTableState]);
|
|
308
|
+
const saveScrollPosition = useCallback(() => {
|
|
309
|
+
if (scrollSaveTimer.current)
|
|
310
|
+
clearTimeout(scrollSaveTimer.current);
|
|
311
|
+
const element = scrollContainerRef.current;
|
|
312
|
+
if (!element)
|
|
313
|
+
return;
|
|
314
|
+
pendingScrollPosition.current = { scrollLeft: element.scrollLeft, scrollTop: element.scrollTop };
|
|
315
|
+
scrollSaveTimer.current = setTimeout(flushScrollPosition, 120);
|
|
316
|
+
}, [flushScrollPosition]);
|
|
317
|
+
useEffect(() => {
|
|
318
|
+
const element = scrollContainerRef.current;
|
|
319
|
+
if (!element || !filePath)
|
|
320
|
+
return;
|
|
321
|
+
const saved = tableState.get(cacheKey);
|
|
322
|
+
const frame = requestAnimationFrame(() => {
|
|
323
|
+
element.scrollLeft = saved?.scrollLeft ?? 0;
|
|
324
|
+
element.scrollTop = saved?.scrollTop ?? 0;
|
|
325
|
+
});
|
|
326
|
+
return () => cancelAnimationFrame(frame);
|
|
327
|
+
}, [cacheKey, filePath, tableState]);
|
|
328
|
+
useEffect(() => {
|
|
329
|
+
window.addEventListener('pagehide', flushScrollPosition, true);
|
|
330
|
+
return () => {
|
|
331
|
+
window.removeEventListener('pagehide', flushScrollPosition, true);
|
|
332
|
+
flushScrollPosition();
|
|
333
|
+
};
|
|
334
|
+
}, [flushScrollPosition]);
|
|
335
|
+
// Rows hold rem-sized text, so their height must track the host's UI size.
|
|
336
|
+
const rowHeight = Math.round((compact ? 60 : ROW_HEIGHT) * UI_SIZE_SCALE[uiSize]);
|
|
337
|
+
const rowVirtualizer = useVirtualizer({
|
|
338
|
+
count: sortedData.length,
|
|
339
|
+
getItemKey: (index) => rowKeys?.[sortedData[index].originalIndex] ?? sortedData[index].originalIndex,
|
|
340
|
+
getScrollElement: () => scrollContainerRef.current,
|
|
341
|
+
estimateSize: () => rowHeight,
|
|
342
|
+
overscan: 10,
|
|
343
|
+
});
|
|
344
|
+
useEffect(() => {
|
|
345
|
+
rowVirtualizer.measure();
|
|
346
|
+
}, [rowHeight, rowVirtualizer]);
|
|
347
|
+
const virtualItems = rowVirtualizer.getVirtualItems();
|
|
348
|
+
const totalTableWidth = visibleColumnWidths.reduce((sum, width) => sum + width, 0) + (onAddColumn ? 180 : 0);
|
|
349
|
+
const tableColumnCount = visibleColumns.length + (onAddColumn ? 1 : 0);
|
|
350
|
+
const allSelected = selectable && sortedData.length > 0 && sortedData.every(({ originalIndex }) => selectedRows.has(originalIndex));
|
|
351
|
+
const toggleAllRows = () => {
|
|
352
|
+
setSelectedRows((previous) => {
|
|
353
|
+
const next = new Set(previous);
|
|
354
|
+
sortedData.forEach(({ originalIndex }) => {
|
|
355
|
+
if (allSelected)
|
|
356
|
+
next.delete(originalIndex);
|
|
357
|
+
else
|
|
358
|
+
next.add(originalIndex);
|
|
359
|
+
});
|
|
360
|
+
return next;
|
|
361
|
+
});
|
|
362
|
+
};
|
|
363
|
+
const toggleRow = (index) => {
|
|
364
|
+
setSelectedRows((previous) => {
|
|
365
|
+
const next = new Set(previous);
|
|
366
|
+
if (next.has(index))
|
|
367
|
+
next.delete(index);
|
|
368
|
+
else
|
|
369
|
+
next.add(index);
|
|
370
|
+
return next;
|
|
371
|
+
});
|
|
372
|
+
};
|
|
373
|
+
const deleteSelectedRows = async () => {
|
|
374
|
+
if (!onDeleteRows || selectedRows.size === 0)
|
|
375
|
+
return;
|
|
376
|
+
await onDeleteRows(Array.from(selectedRows).sort((a, b) => b - a));
|
|
377
|
+
};
|
|
378
|
+
const selectedRowsInVisibleOrder = () => sortedData.flatMap(({ originalIndex }) => (selectedRows.has(originalIndex) ? [originalIndex] : []));
|
|
379
|
+
const addRow = async () => {
|
|
380
|
+
if (!onAddRow)
|
|
381
|
+
return;
|
|
382
|
+
setRowAdditionError(undefined);
|
|
383
|
+
try {
|
|
384
|
+
await onAddRow();
|
|
385
|
+
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
setRowAdditionError(error instanceof Error ? error.message : 'Could not add a row');
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
const [highlightCell, setHighlightCell] = useState(initialCell ?? null);
|
|
391
|
+
useEffect(() => {
|
|
392
|
+
if (!initialCell)
|
|
393
|
+
return;
|
|
394
|
+
rowVirtualizer.scrollToIndex(initialCell.row, { align: 'center' });
|
|
395
|
+
// Scroll column into view
|
|
396
|
+
const colIdx = visibleColumns.indexOf(initialCell.col);
|
|
397
|
+
if (colIdx >= 0 && scrollContainerRef.current) {
|
|
398
|
+
const left = visibleColumnWidths.slice(0, colIdx).reduce((sum, width) => sum + width, 0);
|
|
399
|
+
scrollContainerRef.current.scrollLeft = Math.max(0, left - 50);
|
|
400
|
+
}
|
|
401
|
+
const timeout = setTimeout(() => setHighlightCell(null), 1500);
|
|
402
|
+
return () => clearTimeout(timeout);
|
|
403
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
404
|
+
}, []);
|
|
405
|
+
return (_jsxs("div", { ref: setDialogContainer, "data-id": "tabular-data-dialog-host", className: cn('relative isolate', fillHeight ? 'flex max-h-full min-h-0 flex-col gap-1' : 'mb-4'), children: [compact && visibleColumns.length > 1 && _jsx("p", { className: "json-views-table-hint", children: "Swipe for more fields \u00B7 tap a value to open or edit" }), _jsx("div", { "data-id": "tabular-data-frame", className: cn(TABULAR_DATA_FRAME_CLASS, fillHeight && 'flex min-h-0 flex-col'), children: _jsx("div", { ref: scrollContainerRef, "data-id": "tabular-scroll-area", tabIndex: 0, role: "region", "aria-label": "Scrollable records", onScroll: saveScrollPosition, className: cn('overflow-auto overscroll-x-none', fillHeight && 'min-h-0'), style: fillHeight ? undefined : { maxHeight: 'min(600px, 70vh)' }, children: _jsx(StructuredCellFillContext.Provider, { value: true, children: _jsxs("table", { "aria-label": "JSON records", "aria-rowcount": sortedData.length + (hiddenColumns.length > 0 ? 2 : 1), "aria-colcount": tableColumnCount, "data-selection-active": selectable && selectedRows.size > 0, className: "tabular-data-table w-full table-fixed border-separate border-spacing-0 text-sm", style: { minWidth: totalTableWidth }, children: [_jsxs("colgroup", { children: [visibleColumns.map((col, index) => (_jsx("col", { style: { width: visibleColumnWidths[index] } }, col))), onAddColumn && _jsx("col", { style: { width: 180 } })] }), _jsxs("thead", { children: [hiddenColumns.length > 0 && (_jsx("tr", { children: _jsx("th", { ref: hiddenRowRef, colSpan: Math.max(tableColumnCount, 1), className: "sticky top-0 z-30 bg-background px-3 py-2 text-left font-normal", children: _jsx("div", { className: "flex w-full gap-1.5 overflow-x-auto overscroll-x-none whitespace-nowrap", children: hiddenColumns.map((col) => (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { onClick: () => toggleColumn(col), className: "shrink-0 rounded border border-border bg-card px-2 py-1 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-foreground", "aria-label": `Show column: ${columnLabel(col)}`, children: columnLabel(col) }) }), _jsxs(TooltipContent, { side: "top", children: ["Show column: ", columnLabel(col)] })] }, col))) }) }) })), _jsxs("tr", { children: [visibleColumns.map((col, index) => {
|
|
406
|
+
const width = visibleColumnWidths[index];
|
|
407
|
+
return (_jsxs("th", { "data-id": "tabular-column-header", "data-column": col, "data-pinned": compact && col === controlColumn || undefined, className: `relative sticky z-20 border-r border-b border-border bg-background p-0 text-left font-normal text-muted-foreground whitespace-nowrap last:border-r-0 ${onColumnMove ? 'cursor-grab active:cursor-grabbing' : ''} ${hiddenColumns.length > 0 ? 'border-t border-border' : ''} ${draggedColumn === col ? 'opacity-50' : ''} ${dropTarget?.column === col && dropTarget.position === 'before' ? 'border-l-2 border-l-primary' : ''} ${dropTarget?.column === col && dropTarget.position === 'after' ? 'border-r-2 border-r-primary' : ''}`, style: {
|
|
408
|
+
top: hiddenColumns.length > 0 ? hiddenRowHeight : 0,
|
|
409
|
+
width,
|
|
410
|
+
minWidth: width,
|
|
411
|
+
maxWidth: width,
|
|
412
|
+
}, onDragStart: (event) => {
|
|
413
|
+
if (!onColumnMove || event.target.closest('[role="separator"]')) {
|
|
414
|
+
event.preventDefault();
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
418
|
+
event.dataTransfer.setData('application/x-json-views-column', col);
|
|
419
|
+
draggedColumnRef.current = col;
|
|
420
|
+
setDraggedColumn(col);
|
|
421
|
+
}, onDragOver: (event) => {
|
|
422
|
+
const source = draggedColumnRef.current ?? draggedColumn;
|
|
423
|
+
if (!onColumnMove || !source || source === col)
|
|
424
|
+
return;
|
|
425
|
+
event.preventDefault();
|
|
426
|
+
event.dataTransfer.dropEffect = 'move';
|
|
427
|
+
const bounds = event.currentTarget.getBoundingClientRect();
|
|
428
|
+
const pointerX = Number.isFinite(event.clientX) ? event.clientX : bounds.left;
|
|
429
|
+
setDropTarget({ column: col, position: pointerX <= bounds.left + bounds.width / 2 ? 'before' : 'after' });
|
|
430
|
+
}, onDragLeave: (event) => {
|
|
431
|
+
if (event.currentTarget === event.target)
|
|
432
|
+
setDropTarget((current) => current?.column === col ? undefined : current);
|
|
433
|
+
}, onDragEnd: () => {
|
|
434
|
+
draggedColumnRef.current = undefined;
|
|
435
|
+
setDraggedColumn(undefined);
|
|
436
|
+
setDropTarget(undefined);
|
|
437
|
+
}, onDrop: (event) => {
|
|
438
|
+
const source = draggedColumnRef.current ?? draggedColumn ?? event.dataTransfer.getData('application/x-json-views-column');
|
|
439
|
+
if (!onColumnMove || !source || source === col)
|
|
440
|
+
return;
|
|
441
|
+
event.preventDefault();
|
|
442
|
+
const bounds = event.currentTarget.getBoundingClientRect();
|
|
443
|
+
const pointerX = Number.isFinite(event.clientX) ? event.clientX : bounds.left;
|
|
444
|
+
const position = dropTarget?.column === col
|
|
445
|
+
? dropTarget.position
|
|
446
|
+
: pointerX <= bounds.left + bounds.width / 2 ? 'before' : 'after';
|
|
447
|
+
void onColumnMove(source, col, position);
|
|
448
|
+
draggedColumnRef.current = undefined;
|
|
449
|
+
setDraggedColumn(undefined);
|
|
450
|
+
setDropTarget(undefined);
|
|
451
|
+
}, children: [_jsxs("div", { className: "flex items-center", children: [col === controlColumn && selectable && (_jsx("span", { className: "ml-3 flex h-3.5 w-3.5 shrink-0 items-center", "data-id": "tabular-select-all-slot", children: _jsx(Checkbox, { "aria-label": allSelected ? 'Deselect all rows' : 'Select all rows', className: "tabular-selection-checkbox h-3.5 w-3.5 border-muted-foreground", "data-id": "tabular-select-all-rows", checked: allSelected ? true : selectedRows.size > 0 ? 'indeterminate' : false, disabled: rowDeletionDisabled || sortedData.length === 0, onCheckedChange: toggleAllRows }) })), _jsx("div", { className: cn('min-w-0 flex-1', col === controlColumn && selectable && '[&>button]:pl-1.5'), children: _jsx(ColumnHeaderMenu, { activeFilter: effectiveFilters.find((filter) => filter.column === col), activeSort: effectiveSort, canHide: onColumnHide ? visibleColumns.length > 1 : canHideMoreColumns, column: col, draggable: onColumnMove !== undefined, dragging: draggedColumn === col, filterOperators: columnFilterOperators?.[col], filterValues: filterValueSuggestions(columnFilterValues?.[col] ?? arr.map((row) => row[col])), label: columnLabel(col), onFilter: filterColumn, onHide: hideColumn, onDelete: onColumnDelete && columns.length > 1 ? onColumnDelete : undefined, onMoveFirst: onColumnMove && index > 0
|
|
452
|
+
? () => onColumnMove(col, visibleColumns[0], 'before')
|
|
453
|
+
: undefined, onRename: onColumnRename, onSort: sortColumn }) })] }), _jsx("div", { role: "separator", tabIndex: 0, "aria-label": `Resize ${columnLabel(col)} column`, "aria-orientation": "vertical", "aria-valuemin": MIN_COLUMN_WIDTH, "aria-valuenow": Math.round(width), className: "absolute bottom-0 right-0 top-0 w-1.5 cursor-col-resize hover:bg-primary/30 active:bg-primary/50 focus-visible:bg-primary/40 focus-visible:outline-none", onMouseDown: (e) => onResizeStart(columnStateKey(col), e), onKeyDown: (event) => {
|
|
454
|
+
if (!['ArrowLeft', 'ArrowRight', 'Home'].includes(event.key))
|
|
455
|
+
return;
|
|
456
|
+
event.preventDefault();
|
|
457
|
+
event.stopPropagation();
|
|
458
|
+
const next = event.key === 'Home' ? MIN_COLUMN_WIDTH : Math.max(MIN_COLUMN_WIDTH, width + (event.key === 'ArrowRight' ? 1 : -1) * (event.shiftKey ? 50 : 10));
|
|
459
|
+
const widths = { ...columnWidthsRef.current, [columnStateKey(col)]: next };
|
|
460
|
+
columnWidthsRef.current = widths;
|
|
461
|
+
setColumnWidths(widths);
|
|
462
|
+
saveTableState({ columnWidths: widths });
|
|
463
|
+
} })] }, col));
|
|
464
|
+
}), onAddColumn && _jsx("th", { className: "sticky z-20 border-b border-border bg-background p-0 text-left font-normal", style: { top: hiddenColumns.length > 0 ? hiddenRowHeight : 0, width: 180 }, children: _jsx(AddColumnHeader, { onAdd: onAddColumn }) })] })] }), _jsx("tbody", { children: visibleColumns.length === 0 ? (_jsx("tr", { children: _jsx("td", { className: "px-3 py-4 text-sm italic text-muted-foreground", children: "All columns are hidden" }) })) : (_jsxs(_Fragment, { children: [virtualItems.length > 0 && (_jsx("tr", { style: { height: virtualItems[0].start }, "aria-hidden": true, children: _jsx("td", { colSpan: tableColumnCount, className: "border-0 p-0" }) })), virtualItems.map((virtualRow) => {
|
|
465
|
+
const { item, originalIndex } = sortedData[virtualRow.index];
|
|
466
|
+
const isLastRow = virtualRow.index === sortedData.length - 1;
|
|
467
|
+
const rowControl = _jsx(RowSelectionIcon, { icon: visibleTitleColumn ? _jsx(File, { "aria-hidden": true, "data-id": "jsonView-row-page-icon", className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" }) : null, checked: selectedRows.has(originalIndex), disabled: rowDeletionDisabled, label: `Select row ${originalIndex + 1}`, onToggle: selectable ? () => toggleRow(originalIndex) : undefined });
|
|
468
|
+
return (_jsxs("tr", { ref: rowVirtualizer.measureElement, "data-index": virtualRow.index, "aria-rowindex": virtualRow.index + (hiddenColumns.length > 0 ? 3 : 2), "aria-selected": selectable ? selectedRows.has(originalIndex) : undefined, className: "group bg-background hover:bg-accent/40", children: [visibleColumns.map((col, colIdx) => {
|
|
469
|
+
const hasKey = Object.prototype.hasOwnProperty.call(item, col);
|
|
470
|
+
const width = visibleColumnWidths[colIdx];
|
|
471
|
+
const value = hasKey ? item[col] : undefined;
|
|
472
|
+
const cellContent = (() => {
|
|
473
|
+
if (recordNavigation && col === visibleTitleColumn) {
|
|
474
|
+
return (_jsx(StructuredValueCellFrame, { leading: rowControl, actions: _jsx("button", { type: "button", "data-id": "tabular-open-record", "aria-label": `Open record ${recordNavigation.getLabel(originalIndex)}`, className: "cursor-pointer opacity-0 group-hover/structured-cell:opacity-100 group-focus-within/structured-cell:opacity-100 transition-opacity [@media(hover:none)]:opacity-100 h-6 rounded-sm border border-border bg-background px-2 text-[0.625rem] font-medium tracking-wide text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", onClick: () => recordNavigation.onOpen(originalIndex), children: "OPEN" }), activationLabel: `Open ${recordNavigation.getLabel(originalIndex)}`, className: "cursor-pointer [&_[data-id=atomic-edit-value]]:cursor-pointer", onActivate: () => recordNavigation.onOpen(originalIndex), children: _jsx("span", { className: "flex min-w-0 items-center font-medium", children: recordNavigation.renderTitle({ value, rowIndex: originalIndex, column: col }) }) }));
|
|
475
|
+
}
|
|
476
|
+
if (!hasKey)
|
|
477
|
+
return _jsx("span", { className: "block px-3 py-2 italic text-muted-foreground", children: "\u2014" });
|
|
478
|
+
const openCell = () => onOpenCell({
|
|
479
|
+
value,
|
|
480
|
+
rowIndex: originalIndex,
|
|
481
|
+
column: col,
|
|
482
|
+
});
|
|
483
|
+
return renderCell
|
|
484
|
+
? renderCell({ value, rowIndex: originalIndex, column: col, open: openCell })
|
|
485
|
+
: _jsx(ValueCell, { value: value, onOpen: openCell });
|
|
486
|
+
})();
|
|
487
|
+
return (_jsx("td", { "data-pinned": compact && col === controlColumn || undefined, className: `tabular-data-cell overflow-hidden border-r border-border p-0 last:border-r-0 ${isLastRow ? '' : 'border-b border-border'} ${highlightCell?.row === originalIndex && highlightCell?.col === col ? 'animate-[highlight-fade_1.5s_ease-out]' : ''}`, style: { width, minWidth: width, maxWidth: width }, children: col === controlColumn && col !== visibleTitleColumn && selectable ? _jsxs("div", { className: "relative h-full pl-5", children: [_jsx("span", { className: "absolute inset-y-0 left-3 z-10 flex items-center", children: rowControl }), cellContent] }) : cellContent }, col));
|
|
488
|
+
}), onAddColumn && _jsx("td", { "aria-hidden": true, className: `${isLastRow ? '' : 'border-b border-border'} p-0` })] }, virtualRow.key));
|
|
489
|
+
}), virtualItems.length > 0 && (_jsx("tr", { style: {
|
|
490
|
+
height: rowVirtualizer.getTotalSize() -
|
|
491
|
+
virtualItems[virtualItems.length - 1].end,
|
|
492
|
+
}, "aria-hidden": true, children: _jsx("td", { colSpan: tableColumnCount, className: "border-0 p-0" }) }))] })) })] }) }) }) }), _jsxs("div", { "data-id": "tabular-footer", className: cn('flex min-h-7 items-center gap-2 pt-1 text-xs text-muted-foreground', fillHeight && 'shrink-0'), children: [onAddRow && selectedRows.size === 0 && (_jsxs("button", { type: "button", "data-id": "tabular-add-row", disabled: rowAdditionDisabled, className: "flex h-6 items-center gap-1 rounded-md px-3 font-medium text-foreground transition-colors hover:bg-accent disabled:pointer-events-none disabled:opacity-50", onClick: () => { void addRow(); }, children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), " Add row"] })), selectedRows.size === 0 && rowAdditionError && _jsx("span", { role: "alert", className: "text-destructive", children: rowAdditionError }), selectable && selectedRows.size > 0 && (_jsxs("div", { "data-id": "tabular-selection-bar", className: "flex items-center gap-1", children: [_jsxs("button", { type: "button", "data-id": "tabular-clear-selection", "aria-label": `Clear ${selectedRows.size.toLocaleString()} selected ${selectedRows.size === 1 ? 'row' : 'rows'}`, title: "Clear selection", className: "flex h-6 items-center rounded-md px-1 font-medium text-foreground hover:underline focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", onClick: () => setSelectedRows(new Set()), children: [selectedRows.size.toLocaleString(), " selected"] }), onDeleteRows && (_jsx(DeleteSelectedRowsAction, { count: selectedRows.size, disabled: rowDeletionDisabled, portalContainer: dialogContainer, onDelete: deleteSelectedRows })), getRowsCopyText && getRowsReferenceText && (_jsx(CopyJsonAction, { dataId: "structured-data-copy-selected-rows", getPathText: () => getRowsReferenceText(selectedRowsInVisibleOrder()), getRecordText: () => getRowsCopyText(selectedRowsInVisibleOrder()), label: "Copy paths", pathLabel: "paths", recordLabel: "records", tone: "strong" }, Array.from(selectedRows).join(',')))] })), _jsx("span", { className: "ml-auto", children: rowCountLabel })] })] }));
|
|
493
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import type { ValuePath } from '@script-it/json-views-core';
|
|
3
|
+
export interface JsonViewsTablePresentationState {
|
|
4
|
+
collapsedColumns?: string[];
|
|
5
|
+
columnWidths?: Record<string, number>;
|
|
6
|
+
filters?: Array<{
|
|
7
|
+
column: string;
|
|
8
|
+
operator: string;
|
|
9
|
+
value?: unknown;
|
|
10
|
+
}>;
|
|
11
|
+
scrollLeft?: number;
|
|
12
|
+
scrollTop?: number;
|
|
13
|
+
sort?: {
|
|
14
|
+
column: string;
|
|
15
|
+
direction: 'asc' | 'desc';
|
|
16
|
+
} | null;
|
|
17
|
+
}
|
|
18
|
+
export interface JsonViewsNavigationPresentationState {
|
|
19
|
+
stack: Array<{
|
|
20
|
+
path: ValuePath;
|
|
21
|
+
key: string;
|
|
22
|
+
kind?: 'value' | 'record';
|
|
23
|
+
titleKey?: string;
|
|
24
|
+
}>;
|
|
25
|
+
currentPath: ValuePath;
|
|
26
|
+
currentKind?: 'value' | 'record';
|
|
27
|
+
currentTitleKey?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface JsonViewsGeneralPresentationState {
|
|
30
|
+
scrollPositions?: Record<string, number>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Serializable, visual-only state for one document. Hosts may cache this beside
|
|
34
|
+
* a document, but must not write it into the document's `$jsonviews` metadata.
|
|
35
|
+
*/
|
|
36
|
+
export interface JsonViewsPresentationState {
|
|
37
|
+
version: 1;
|
|
38
|
+
activeView?: string;
|
|
39
|
+
queries?: Record<string, string>;
|
|
40
|
+
/** Unsaved explicit view configuration, scoped by view and data path. */
|
|
41
|
+
viewDrafts?: Record<string, Record<string, unknown>>;
|
|
42
|
+
navigation?: Record<string, JsonViewsNavigationPresentationState>;
|
|
43
|
+
tables?: Record<string, JsonViewsTablePresentationState>;
|
|
44
|
+
general?: Record<string, JsonViewsGeneralPresentationState>;
|
|
45
|
+
}
|
|
46
|
+
type PresentationStateChange = (state: JsonViewsPresentationState) => void;
|
|
47
|
+
/** Bounded, instance-owned presentation state. Never shared across host viewers. */
|
|
48
|
+
export declare class ViewStateCache<T> {
|
|
49
|
+
private readonly onChange?;
|
|
50
|
+
private readonly entries;
|
|
51
|
+
constructor(initial?: Record<string, T>, onChange?: (() => void) | undefined);
|
|
52
|
+
get(key: string): T | undefined;
|
|
53
|
+
set(key: string, value: T): void;
|
|
54
|
+
delete(key: string): void;
|
|
55
|
+
toRecord(): Record<string, T>;
|
|
56
|
+
}
|
|
57
|
+
export declare function ViewerStateProvider({ children, initialState, onChange, }: {
|
|
58
|
+
children: ReactNode;
|
|
59
|
+
initialState?: JsonViewsPresentationState;
|
|
60
|
+
onChange?: PresentationStateChange;
|
|
61
|
+
}): import("react").JSX.Element;
|
|
62
|
+
export declare function useViewerState(): {
|
|
63
|
+
readonly activeView: string | undefined;
|
|
64
|
+
setActiveView(value: string): void;
|
|
65
|
+
queries: ViewStateCache<string>;
|
|
66
|
+
viewDrafts: ViewStateCache<Record<string, unknown>>;
|
|
67
|
+
navigation: ViewStateCache<JsonViewsNavigationPresentationState>;
|
|
68
|
+
tables: ViewStateCache<JsonViewsTablePresentationState>;
|
|
69
|
+
general: ViewStateCache<JsonViewsGeneralPresentationState>;
|
|
70
|
+
snapshot(): JsonViewsPresentationState;
|
|
71
|
+
};
|
|
72
|
+
export {};
|