@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,47 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import type { UiSize } from '../lib/ui-size.js';
|
|
3
|
+
import { type ValuePath } from '@script-it/json-views-core';
|
|
4
|
+
import { type CompiledJsonViewMetadata } from '@script-it/json-views-core';
|
|
5
|
+
import { type JsonViewSchemaExternalWidgetRenderer } from './schema-value.js';
|
|
6
|
+
import type { JsonValueReplacement } from '@script-it/json-views-core';
|
|
7
|
+
export interface JsonViewJsonEditing {
|
|
8
|
+
canEditViews?: boolean;
|
|
9
|
+
/** Why view creation is visible but unavailable. */
|
|
10
|
+
viewCreationUnavailableReason?: string;
|
|
11
|
+
/** Embedded/session metadata saves immediately; structural conversions wait for an explicit save. */
|
|
12
|
+
viewSaveMode?: 'automatic' | 'explicit';
|
|
13
|
+
/** Whether a replacement can preserve the source representation at this path. */
|
|
14
|
+
canReplace?: (path: ValuePath) => boolean;
|
|
15
|
+
addColumn?: (arrayPath: ValuePath, property: string, schemaPath: string, descriptor: Record<string, unknown>, viewDeclarationIndex: number) => Promise<void>;
|
|
16
|
+
deleteColumn?: (arrayPath: ValuePath, property: string, schemaPath: string) => Promise<void>;
|
|
17
|
+
append: (arrayPath: ValuePath, value: unknown) => Promise<void>;
|
|
18
|
+
/** Marks a newly appended record as an unfinished draft until its first edit. */
|
|
19
|
+
registerDraftRow?: (path: ValuePath) => void;
|
|
20
|
+
isDraftRow?: (path: ValuePath) => boolean;
|
|
21
|
+
clear: (path: ValuePath) => Promise<void>;
|
|
22
|
+
replace: (path: ValuePath, value: unknown) => Promise<void>;
|
|
23
|
+
replaceMany: (replacements: readonly JsonValueReplacement[]) => Promise<void>;
|
|
24
|
+
setOptionColor?: (schemaPath: string, optionValue: string, color: string) => Promise<void>;
|
|
25
|
+
replaceViews: (views: readonly unknown[]) => Promise<void>;
|
|
26
|
+
removeMany: (paths: ValuePath[]) => Promise<void>;
|
|
27
|
+
saving: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface JsonViewerProps {
|
|
30
|
+
compiled: CompiledJsonViewMetadata;
|
|
31
|
+
defaultKey: string;
|
|
32
|
+
editing?: JsonViewJsonEditing;
|
|
33
|
+
filePath?: string;
|
|
34
|
+
fillHeight?: boolean;
|
|
35
|
+
renderMarkdown?: (content: string) => ReactNode;
|
|
36
|
+
renderExternalWidget?: JsonViewSchemaExternalWidgetRenderer;
|
|
37
|
+
/** Reports the current location and, when narrowed by a filter/search, matching paths in display order. */
|
|
38
|
+
onCurrentPathChange?: (path: ValuePath, sourcePaths?: readonly ValuePath[]) => void;
|
|
39
|
+
sourceContent?: ReactNode;
|
|
40
|
+
sourceVisible?: boolean;
|
|
41
|
+
onSourceVisibleChange?: (visible: boolean) => void;
|
|
42
|
+
sourceControl?: ReactNode;
|
|
43
|
+
uiSize?: UiSize;
|
|
44
|
+
viewsEnabled?: boolean;
|
|
45
|
+
/** CSV is already tabular, irrespective of JSON prediction confidence. */
|
|
46
|
+
tabularRoot?: boolean;
|
|
47
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import type { UiSize } from '../../../lib/ui-size.js';
|
|
3
|
+
import { type ValuePath } from '@script-it/json-views-core';
|
|
4
|
+
import type { JsonViewSchemaDescriptor } from '@script-it/json-views-core';
|
|
5
|
+
import type { JsonViewSchemaExternalWidgetRenderer } from '../../schema-value.js';
|
|
6
|
+
export interface GeneralJsonRecordTarget {
|
|
7
|
+
path: ValuePath;
|
|
8
|
+
titleKey?: string;
|
|
9
|
+
onBack?: () => void;
|
|
10
|
+
breadcrumbLabels?: readonly string[];
|
|
11
|
+
backLabel?: string;
|
|
12
|
+
}
|
|
13
|
+
interface GeneralJsonViewProps {
|
|
14
|
+
root: unknown;
|
|
15
|
+
defaultKey: string;
|
|
16
|
+
breadcrumb?: {
|
|
17
|
+
ancestors: readonly string[];
|
|
18
|
+
backLabel: string;
|
|
19
|
+
onBack?: () => void;
|
|
20
|
+
};
|
|
21
|
+
/** Absolute source path used as this navigator's root. */
|
|
22
|
+
initialPath?: ValuePath;
|
|
23
|
+
filePath?: string;
|
|
24
|
+
editing?: {
|
|
25
|
+
canReplace?: (path: ValuePath) => boolean;
|
|
26
|
+
clear: (path: ValuePath) => Promise<void>;
|
|
27
|
+
replace: (path: ValuePath, value: unknown) => Promise<void>;
|
|
28
|
+
removeMany: (paths: ValuePath[]) => Promise<void>;
|
|
29
|
+
saving: boolean;
|
|
30
|
+
};
|
|
31
|
+
renderMarkdown?: (content: string) => ReactNode;
|
|
32
|
+
uiSize?: UiSize;
|
|
33
|
+
fillHeight?: boolean;
|
|
34
|
+
schemaForPath?: (path: ValuePath) => JsonViewSchemaDescriptor | undefined;
|
|
35
|
+
renderExternalWidget?: JsonViewSchemaExternalWidgetRenderer;
|
|
36
|
+
onCurrentPathChange?: (path: ValuePath, sourcePaths?: readonly ValuePath[]) => void;
|
|
37
|
+
renderRecord?: (target: GeneralJsonRecordTarget) => ReactNode;
|
|
38
|
+
}
|
|
39
|
+
export declare function NavigationBreadcrumb({ labels, onBack, backLabel }: {
|
|
40
|
+
labels: readonly string[];
|
|
41
|
+
onBack?: () => void;
|
|
42
|
+
backLabel?: string;
|
|
43
|
+
}): import("react").JSX.Element | null;
|
|
44
|
+
/** Owns navigation and display state for the general JSON renderer. */
|
|
45
|
+
export declare function GeneralJsonView({ breadcrumb, root, defaultKey, initialPath, filePath, editing, renderMarkdown, uiSize, fillHeight, schemaForPath, renderExternalWidget, onCurrentPathChange, renderRecord, }: GeneralJsonViewProps): import("react").JSX.Element;
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { ArrowLeft } from 'lucide-react';
|
|
4
|
+
import { useViewerState } from '../../../viewer-state.js';
|
|
5
|
+
import { getValueAtPath, isJsonViewTableCandidate, valuePathKey, VALUE_PATH_MISSING, } from '@script-it/json-views-core';
|
|
6
|
+
import { useJsonViewsRegistries } from '../../../widget-registry.js';
|
|
7
|
+
import { GeneralJsonNodeView } from './json-node-view.js';
|
|
8
|
+
function BreadcrumbSegment({ label }) {
|
|
9
|
+
const characters = Array.from(label);
|
|
10
|
+
const clipped = characters.length > 20;
|
|
11
|
+
const visible = clipped ? `${characters.slice(0, 10).join('')}…` : label;
|
|
12
|
+
return _jsx("span", { title: clipped ? label : undefined, "aria-label": clipped ? label : undefined, children: visible });
|
|
13
|
+
}
|
|
14
|
+
export function NavigationBreadcrumb({ labels, onBack, backLabel }) {
|
|
15
|
+
if (!onBack)
|
|
16
|
+
return null;
|
|
17
|
+
return _jsxs("nav", { "aria-label": "Breadcrumb", className: "mb-3 flex items-center gap-2", children: [_jsx("button", { type: "button", className: "rounded p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground", "aria-label": backLabel ?? 'Back', onClick: onBack, title: "Back", children: _jsx(ArrowLeft, { className: "h-4 w-4" }) }), _jsx("span", { "data-id": "jsonView-breadcrumb-path", className: "flex min-w-0 items-center whitespace-nowrap text-xs text-muted-foreground", children: labels.map((label, index) => _jsxs("span", { className: "contents", children: [index > 0 && _jsx("span", { "aria-hidden": true, className: "mx-1", children: ' / ' }), _jsx(BreadcrumbSegment, { label: label })] }, `${index}:${label}`)) })] });
|
|
18
|
+
}
|
|
19
|
+
function rootHistory(defaultKey, initialPath) {
|
|
20
|
+
return [{ key: defaultKey, path: initialPath, kind: 'value' }];
|
|
21
|
+
}
|
|
22
|
+
function restoreHistory(root, defaultKey, initialPath, state, filePath) {
|
|
23
|
+
const saved = filePath ? state.get(filePath) : undefined;
|
|
24
|
+
if (!saved)
|
|
25
|
+
return rootHistory(defaultKey, initialPath);
|
|
26
|
+
const currentIsInitial = saved.currentPath.length === initialPath.length
|
|
27
|
+
&& initialPath.every((segment, index) => saved.currentPath[index] === segment);
|
|
28
|
+
const currentKey = currentIsInitial
|
|
29
|
+
? defaultKey
|
|
30
|
+
: saved.currentPath.length > 0
|
|
31
|
+
? String(saved.currentPath[saved.currentPath.length - 1])
|
|
32
|
+
: defaultKey;
|
|
33
|
+
const history = [
|
|
34
|
+
...saved.stack.map(({ key, path, kind = 'value', titleKey }) => ({ key, path, kind, titleKey })),
|
|
35
|
+
{ key: currentKey, path: saved.currentPath, kind: saved.currentKind ?? 'value', titleKey: saved.currentTitleKey },
|
|
36
|
+
];
|
|
37
|
+
const current = history[history.length - 1];
|
|
38
|
+
const remainsInsideView = current && initialPath.every((segment, index) => current.path[index] === segment);
|
|
39
|
+
return current && remainsInsideView && getValueAtPath(root, current.path) !== VALUE_PATH_MISSING
|
|
40
|
+
? history
|
|
41
|
+
: rootHistory(defaultKey, initialPath);
|
|
42
|
+
}
|
|
43
|
+
/** Owns navigation and display state for the general JSON renderer. */
|
|
44
|
+
export function GeneralJsonView({ breadcrumb, root, defaultKey, initialPath = [], filePath, editing, renderMarkdown, uiSize, fillHeight = false, schemaForPath, renderExternalWidget, onCurrentPathChange, renderRecord, }) {
|
|
45
|
+
const { widgets } = useJsonViewsRegistries();
|
|
46
|
+
const { general, navigation } = useViewerState();
|
|
47
|
+
const savedGeneralState = filePath ? general.get(filePath) : undefined;
|
|
48
|
+
const [history, setHistory] = useState(() => restoreHistory(root, defaultKey, initialPath, navigation, filePath));
|
|
49
|
+
const scrollContainerRef = useRef(null);
|
|
50
|
+
const scrollSaveTimer = useRef(undefined);
|
|
51
|
+
const scrollPositionsRef = useRef({ ...savedGeneralState?.scrollPositions });
|
|
52
|
+
const tableCellPositionsRef = useRef({});
|
|
53
|
+
const [visibleSourcePaths, setVisibleSourcePaths] = useState({ pathKey: '' });
|
|
54
|
+
const current = history[history.length - 1] ?? rootHistory(defaultKey, initialPath)[0];
|
|
55
|
+
const resolvedNode = getValueAtPath(root, current.path);
|
|
56
|
+
const navigationIsValid = resolvedNode !== VALUE_PATH_MISSING;
|
|
57
|
+
const effectiveHistory = navigationIsValid ? history : rootHistory(defaultKey, initialPath);
|
|
58
|
+
const effectiveCurrent = effectiveHistory[effectiveHistory.length - 1];
|
|
59
|
+
const initialNode = getValueAtPath(root, initialPath);
|
|
60
|
+
const currentNode = navigationIsValid
|
|
61
|
+
? resolvedNode
|
|
62
|
+
: initialNode === VALUE_PATH_MISSING ? root : initialNode;
|
|
63
|
+
const currentPathKey = valuePathKey(effectiveCurrent.path);
|
|
64
|
+
const currentSourcePaths = visibleSourcePaths.pathKey === currentPathKey ? visibleSourcePaths.paths : undefined;
|
|
65
|
+
const currentSourcePathsKey = JSON.stringify(currentSourcePaths);
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
onCurrentPathChange?.(effectiveCurrent.path, currentSourcePaths);
|
|
68
|
+
}, [currentPathKey, currentSourcePathsKey, onCurrentPathChange]);
|
|
69
|
+
const reportVisibleSourcePaths = useCallback((paths) => {
|
|
70
|
+
setVisibleSourcePaths((current) => current.pathKey === currentPathKey && JSON.stringify(current.paths) === JSON.stringify(paths)
|
|
71
|
+
? current
|
|
72
|
+
: { pathKey: currentPathKey, paths });
|
|
73
|
+
}, [currentPathKey]);
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (!filePath)
|
|
76
|
+
return;
|
|
77
|
+
const next = {
|
|
78
|
+
stack: effectiveHistory.slice(0, -1),
|
|
79
|
+
currentPath: effectiveCurrent.path,
|
|
80
|
+
currentKind: effectiveCurrent.kind,
|
|
81
|
+
currentTitleKey: effectiveCurrent.titleKey,
|
|
82
|
+
};
|
|
83
|
+
if (JSON.stringify(navigation.get(filePath)) !== JSON.stringify(next))
|
|
84
|
+
navigation.set(filePath, next);
|
|
85
|
+
}, [effectiveCurrent.path, effectiveHistory, filePath, navigation]);
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
const frameId = window.requestAnimationFrame(() => {
|
|
88
|
+
if (!scrollContainerRef.current)
|
|
89
|
+
return;
|
|
90
|
+
scrollContainerRef.current.scrollTop = scrollPositionsRef.current[currentPathKey] ?? 0;
|
|
91
|
+
});
|
|
92
|
+
return () => window.cancelAnimationFrame(frameId);
|
|
93
|
+
}, [currentPathKey]);
|
|
94
|
+
const flushScrollPosition = useCallback(() => {
|
|
95
|
+
if (!scrollSaveTimer.current || !filePath)
|
|
96
|
+
return;
|
|
97
|
+
clearTimeout(scrollSaveTimer.current);
|
|
98
|
+
scrollSaveTimer.current = undefined;
|
|
99
|
+
general.set(filePath, {
|
|
100
|
+
...general.get(filePath),
|
|
101
|
+
scrollPositions: { ...scrollPositionsRef.current },
|
|
102
|
+
});
|
|
103
|
+
}, [filePath, general]);
|
|
104
|
+
const captureScrollPosition = () => {
|
|
105
|
+
scrollPositionsRef.current[currentPathKey] = scrollContainerRef.current?.scrollTop ?? 0;
|
|
106
|
+
if (!filePath)
|
|
107
|
+
return;
|
|
108
|
+
if (scrollSaveTimer.current)
|
|
109
|
+
clearTimeout(scrollSaveTimer.current);
|
|
110
|
+
scrollSaveTimer.current = setTimeout(flushScrollPosition, 120);
|
|
111
|
+
};
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
window.addEventListener('pagehide', flushScrollPosition, true);
|
|
114
|
+
return () => {
|
|
115
|
+
window.removeEventListener('pagehide', flushScrollPosition, true);
|
|
116
|
+
flushScrollPosition();
|
|
117
|
+
};
|
|
118
|
+
}, [flushScrollPosition]);
|
|
119
|
+
const navigateTo = (key, relativePath = [key], record) => {
|
|
120
|
+
const nextPath = [...effectiveCurrent.path, ...relativePath];
|
|
121
|
+
if (getValueAtPath(root, nextPath) === VALUE_PATH_MISSING)
|
|
122
|
+
return;
|
|
123
|
+
captureScrollPosition();
|
|
124
|
+
setHistory((currentHistory) => [
|
|
125
|
+
...(navigationIsValid ? currentHistory : rootHistory(defaultKey, initialPath)),
|
|
126
|
+
{ key, path: nextPath, kind: record ? 'record' : 'value', titleKey: record?.titleKey },
|
|
127
|
+
]);
|
|
128
|
+
};
|
|
129
|
+
const goBack = () => {
|
|
130
|
+
captureScrollPosition();
|
|
131
|
+
setHistory((currentHistory) => currentHistory.length > 1
|
|
132
|
+
? currentHistory.slice(0, -1)
|
|
133
|
+
: currentHistory);
|
|
134
|
+
};
|
|
135
|
+
const atomicEditing = editing
|
|
136
|
+
? {
|
|
137
|
+
canReplace: editing.canReplace,
|
|
138
|
+
clear: editing.clear,
|
|
139
|
+
commit: editing.replace,
|
|
140
|
+
removeMany: editing.removeMany,
|
|
141
|
+
saving: editing.saving,
|
|
142
|
+
}
|
|
143
|
+
: undefined;
|
|
144
|
+
const individualObject = currentNode !== null && typeof currentNode === 'object' && !Array.isArray(currentNode)
|
|
145
|
+
&& !isJsonViewTableCandidate(currentNode);
|
|
146
|
+
const currentDescriptor = schemaForPath?.(effectiveCurrent.path);
|
|
147
|
+
const registeredContainer = currentDescriptor && (widgets.get(currentDescriptor.type) || widgets.getDisplay(currentDescriptor.type) || renderExternalWidget);
|
|
148
|
+
if (renderRecord && !registeredContainer && (effectiveCurrent.kind === 'record' || individualObject)) {
|
|
149
|
+
return _jsx("div", { className: "flex min-h-0 flex-1 flex-col", children: renderRecord({
|
|
150
|
+
path: effectiveCurrent.path,
|
|
151
|
+
titleKey: effectiveCurrent.titleKey,
|
|
152
|
+
onBack: effectiveHistory.length > 1 ? goBack : breadcrumb?.onBack,
|
|
153
|
+
breadcrumbLabels: [...(breadcrumb?.ancestors ?? []), ...effectiveHistory.map(({ key }) => key)],
|
|
154
|
+
backLabel: effectiveHistory.length > 1 ? 'Back' : breadcrumb?.backLabel,
|
|
155
|
+
}) });
|
|
156
|
+
}
|
|
157
|
+
return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col", children: [_jsx(NavigationBreadcrumb, { labels: [...(breadcrumb?.ancestors ?? []), ...effectiveHistory.map(({ key }) => key)], onBack: effectiveHistory.length > 1 ? goBack : breadcrumb?.onBack, backLabel: effectiveHistory.length > 1 ? 'Back' : breadcrumb?.backLabel }), _jsx("div", { ref: scrollContainerRef, className: fillHeight
|
|
158
|
+
? 'flex min-h-0 flex-1 flex-col overflow-auto overscroll-x-none'
|
|
159
|
+
: 'flex-1 overflow-auto overscroll-x-none', onScroll: captureScrollPosition, children: _jsx(GeneralJsonNodeView, { node: currentNode, navigateTo: navigateTo, filePath: filePath, currentPathKey: currentPathKey, currentJsonPath: effectiveCurrent.path, currentLabel: effectiveCurrent.key, renderAsMarkdown: typeof currentNode === 'string'
|
|
160
|
+
&& schemaForPath?.(effectiveCurrent.path)?.type === 'markdown', getTableCellPosition: (path) => tableCellPositionsRef.current[path], setTableCellPosition: (path, cell) => {
|
|
161
|
+
tableCellPositionsRef.current[path] = cell;
|
|
162
|
+
}, renderMarkdown: renderMarkdown, uiSize: uiSize, fillHeight: fillHeight, atomicEditing: atomicEditing, schemaForPath: schemaForPath, renderExternalWidget: renderExternalWidget, onVisibleSourcePathsChange: reportVisibleSourcePaths }) })] }));
|
|
163
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for deciding how the JSON viewer should present a value.
|
|
3
|
+
*
|
|
4
|
+
* Kept free of React/DOM so the detection logic is unit-testable on its own.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* True for `{...}` values. The `[object Object]` tag excludes arrays, null, and
|
|
8
|
+
* boxed built-ins (Date, Map, RegExp, …) that a bare `typeof` check would admit,
|
|
9
|
+
* keeping this safe if reused outside `JSON.parse` output.
|
|
10
|
+
*/
|
|
11
|
+
export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
|
|
12
|
+
/**
|
|
13
|
+
* Describes a `{ key: object, ... }` map that can be flattened into a table:
|
|
14
|
+
* one row per top-level key, the nested object's fields spread across columns.
|
|
15
|
+
*/
|
|
16
|
+
export interface DictTableInfo {
|
|
17
|
+
/** Synthetic leading column holding each entry's top-level key. */
|
|
18
|
+
keyColumn: string;
|
|
19
|
+
/** All columns in display order: `keyColumn` first, then nested fields. */
|
|
20
|
+
columns: string[];
|
|
21
|
+
/** One record per entry, shaped `{ [keyColumn]: key, ...nestedObject }`. */
|
|
22
|
+
records: Record<string, unknown>[];
|
|
23
|
+
/** The original top-level keys, parallel to `records`, for path building. */
|
|
24
|
+
keys: string[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Detect a map of similarly-shaped records — `{ key: {…}, key: {…} }` — so it
|
|
28
|
+
* can be rendered as a flat table instead of a key→"{…}" drill-in list.
|
|
29
|
+
*
|
|
30
|
+
* Uses the same conservative eligibility check as inferred tables. The map key
|
|
31
|
+
* is only a navigation column, not evidence that its values form useful rows.
|
|
32
|
+
*/
|
|
33
|
+
export declare function getDictOfObjectsInfo(obj: Record<string, unknown>): DictTableInfo | null;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for deciding how the JSON viewer should present a value.
|
|
3
|
+
*
|
|
4
|
+
* Kept free of React/DOM so the detection logic is unit-testable on its own.
|
|
5
|
+
*/
|
|
6
|
+
import { isJsonViewTableCandidate } from '@script-it/json-views-core';
|
|
7
|
+
/**
|
|
8
|
+
* True for `{...}` values. The `[object Object]` tag excludes arrays, null, and
|
|
9
|
+
* boxed built-ins (Date, Map, RegExp, …) that a bare `typeof` check would admit,
|
|
10
|
+
* keeping this safe if reused outside `JSON.parse` output.
|
|
11
|
+
*/
|
|
12
|
+
export function isPlainObject(value) {
|
|
13
|
+
return Object.prototype.toString.call(value) === '[object Object]';
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Detect a map of similarly-shaped records — `{ key: {…}, key: {…} }` — so it
|
|
17
|
+
* can be rendered as a flat table instead of a key→"{…}" drill-in list.
|
|
18
|
+
*
|
|
19
|
+
* Uses the same conservative eligibility check as inferred tables. The map key
|
|
20
|
+
* is only a navigation column, not evidence that its values form useful rows.
|
|
21
|
+
*/
|
|
22
|
+
export function getDictOfObjectsInfo(obj) {
|
|
23
|
+
const entries = Object.entries(obj);
|
|
24
|
+
if (!isJsonViewTableCandidate(obj))
|
|
25
|
+
return null;
|
|
26
|
+
const seenColumns = new Set();
|
|
27
|
+
const nestedColumns = [];
|
|
28
|
+
for (const [, value] of entries) {
|
|
29
|
+
if (!isPlainObject(value))
|
|
30
|
+
return null;
|
|
31
|
+
const keys = Object.keys(value);
|
|
32
|
+
for (const key of keys) {
|
|
33
|
+
if (!seenColumns.has(key)) {
|
|
34
|
+
seenColumns.add(key);
|
|
35
|
+
nestedColumns.push(key);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (nestedColumns.length === 0)
|
|
40
|
+
return null;
|
|
41
|
+
// Reserve a column for the map keys that can't clash with a nested field.
|
|
42
|
+
let keyColumn = 'key';
|
|
43
|
+
while (seenColumns.has(keyColumn))
|
|
44
|
+
keyColumn = `_${keyColumn}`;
|
|
45
|
+
const keys = entries.map(([key]) => key);
|
|
46
|
+
const records = entries.map(([key, value]) => ({
|
|
47
|
+
[keyColumn]: key,
|
|
48
|
+
...value,
|
|
49
|
+
}));
|
|
50
|
+
return { keyColumn, columns: [keyColumn, ...nestedColumns], records, keys };
|
|
51
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Renders one location in a general JSON document. */
|
|
2
|
+
import { type ReactNode } from 'react';
|
|
3
|
+
import type { UiSize } from '../../../lib/ui-size.js';
|
|
4
|
+
import { type ValuePath } from '@script-it/json-views-core';
|
|
5
|
+
import type { JsonViewSchemaDescriptor } from '@script-it/json-views-core';
|
|
6
|
+
import { type JsonViewSchemaExternalWidgetRenderer } from '../../schema-value.js';
|
|
7
|
+
export interface GeneralJsonAtomicEditing {
|
|
8
|
+
canReplace?: (path: ValuePath) => boolean;
|
|
9
|
+
clear: (path: ValuePath) => Promise<void>;
|
|
10
|
+
commit: (path: ValuePath, value: unknown) => Promise<void>;
|
|
11
|
+
/** Removes several values as one commit (one file write). Positional paths
|
|
12
|
+
* must arrive deepest-index-first so earlier removals don't shift them. */
|
|
13
|
+
removeMany: (paths: ValuePath[]) => Promise<void>;
|
|
14
|
+
saving: boolean;
|
|
15
|
+
}
|
|
16
|
+
export type GeneralJsonNavigateTo = (key: string, relativePath?: ValuePath, record?: {
|
|
17
|
+
titleKey?: string;
|
|
18
|
+
}) => void;
|
|
19
|
+
export interface GeneralJsonNodeViewProps {
|
|
20
|
+
node: unknown;
|
|
21
|
+
navigateTo: GeneralJsonNavigateTo;
|
|
22
|
+
filePath?: string;
|
|
23
|
+
currentPathKey: string;
|
|
24
|
+
currentJsonPath: ValuePath;
|
|
25
|
+
currentLabel: string;
|
|
26
|
+
renderAsMarkdown: boolean;
|
|
27
|
+
getTableCellPosition?: (path: string) => {
|
|
28
|
+
row: number;
|
|
29
|
+
col: string;
|
|
30
|
+
} | undefined;
|
|
31
|
+
setTableCellPosition?: (path: string, cell: {
|
|
32
|
+
row: number;
|
|
33
|
+
col: string;
|
|
34
|
+
}) => void;
|
|
35
|
+
renderMarkdown?: (content: string) => ReactNode;
|
|
36
|
+
uiSize?: UiSize;
|
|
37
|
+
fillHeight?: boolean;
|
|
38
|
+
atomicEditing?: GeneralJsonAtomicEditing;
|
|
39
|
+
schemaForPath?: (path: ValuePath) => JsonViewSchemaDescriptor | undefined;
|
|
40
|
+
renderExternalWidget?: JsonViewSchemaExternalWidgetRenderer;
|
|
41
|
+
onVisibleSourcePathsChange?: (paths?: readonly ValuePath[]) => void;
|
|
42
|
+
}
|
|
43
|
+
export declare function GeneralJsonNodeView(props: GeneralJsonNodeViewProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/** Renders one location in a general JSON document. */
|
|
3
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
4
|
+
import { JsonSourceNumber, useJsonSourceLiterals } from '../../source-literals.js';
|
|
5
|
+
import { useJsonViewsRegistries } from '../../../widget-registry.js';
|
|
6
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '../../../primitives/tooltip.js';
|
|
7
|
+
import { cn } from '../../../lib/cn.js';
|
|
8
|
+
import { ExpandValueButton, TABULAR_DATA_FRAME_CLASS, TabularObjectArrayView, ValueCell, ValueCellContent, } from '../../../tabular-data-view.js';
|
|
9
|
+
import { getDictOfObjectsInfo, isPlainObject } from './json-flatten.js';
|
|
10
|
+
import { AtomicValueEditor, StructuredValueCellFrame, } from '../../../structured-data/atomic-value-editor.js';
|
|
11
|
+
import { isEditableAtomicValue } from '../../../structured-data/atomic-value.js';
|
|
12
|
+
import { inferJsonViewRecordTitleKey, isJsonViewTableCandidate } from '@script-it/json-views-core';
|
|
13
|
+
import { JsonViewSchemaValueDisplay, JsonViewSchemaValueCell, } from '../../schema-value.js';
|
|
14
|
+
export function GeneralJsonNodeView(props) {
|
|
15
|
+
return _jsx(AnnotatedJsonNodeView, { ...props }, props.currentPathKey);
|
|
16
|
+
}
|
|
17
|
+
function AnnotatedJsonNodeView({ currentPathKey, ...props }) {
|
|
18
|
+
const [expanded, setExpanded] = useState(false);
|
|
19
|
+
const { widgets } = useJsonViewsRegistries();
|
|
20
|
+
const descriptor = props.schemaForPath?.(props.currentJsonPath);
|
|
21
|
+
const container = props.node !== null && typeof props.node === 'object';
|
|
22
|
+
const registered = descriptor && (widgets.get(descriptor.type) || widgets.getDisplay(descriptor.type) || props.renderExternalWidget);
|
|
23
|
+
if (container && descriptor && registered && !expanded) {
|
|
24
|
+
const editing = props.atomicEditing?.canReplace?.(props.currentJsonPath) === false ? undefined : props.atomicEditing;
|
|
25
|
+
return _jsx(JsonViewSchemaValueCell, { sourcePath: props.currentJsonPath, descriptor: descriptor, label: descriptor.title ?? props.currentLabel, value: props.node, onOpen: () => setExpanded(true), onCommit: editing ? (value) => editing.commit(props.currentJsonPath, value) : undefined, onClear: editing ? () => editing.clear(props.currentJsonPath) : undefined, renderExternalWidget: props.renderExternalWidget, renderMarkdown: props.renderMarkdown, saving: editing?.saving });
|
|
26
|
+
}
|
|
27
|
+
return _jsxs(_Fragment, { children: [expanded && _jsx("button", { type: "button", className: "mb-2 w-fit rounded-md px-2 py-1 text-xs text-muted-foreground hover:bg-accent hover:text-foreground", onClick: () => setExpanded(false), children: "Back to annotated value" }), _jsx(NodeView, { ...props, currentPath: currentPathKey })] });
|
|
28
|
+
}
|
|
29
|
+
function GeneralJsonValueCell({ atomicEditing, compact = false, label, navigateTo, path, renderMarkdown, renderExternalWidget, schemaForPath, value, }) {
|
|
30
|
+
if (path && atomicEditing?.canReplace?.(path) === false)
|
|
31
|
+
atomicEditing = undefined;
|
|
32
|
+
const descriptor = path ? schemaForPath?.(path) : undefined;
|
|
33
|
+
if (descriptor && path) {
|
|
34
|
+
return (_jsx(JsonViewSchemaValueCell, { sourcePath: path, compact: compact, descriptor: descriptor, inputKey: String(path[path.length - 1] ?? label), label: descriptor.title ?? label, onClear: atomicEditing ? () => atomicEditing.clear(path) : undefined, onCommit: atomicEditing ? (nextValue) => atomicEditing.commit(path, nextValue) : undefined, onOpen: navigateTo, renderExternalWidget: renderExternalWidget, renderMarkdown: renderMarkdown, saving: atomicEditing?.saving, value: value }));
|
|
35
|
+
}
|
|
36
|
+
const editable = atomicEditing !== undefined && path !== null && isEditableAtomicValue(value);
|
|
37
|
+
if (!editable || !atomicEditing || path === null)
|
|
38
|
+
return _jsx(ValueCell, { sourcePath: path ?? undefined, value: value, onOpen: navigateTo });
|
|
39
|
+
return (_jsx(AtomicValueEditor, { actions: _jsx(ExpandValueButton, { onClick: navigateTo }), label: label, multiline: typeof value === 'string' && value.includes('\n'), onCommit: (nextValue) => atomicEditing.commit(path, nextValue), saving: atomicEditing.saving, value: value, children: _jsx(ValueCellContent, { sourcePath: path ?? undefined, value: value, onOpen: navigateTo }) }));
|
|
40
|
+
}
|
|
41
|
+
function NodeView({ node, navigateTo, filePath, currentPath, currentJsonPath, currentLabel, renderAsMarkdown, getTableCellPosition, setTableCellPosition, renderMarkdown, uiSize, fillHeight = false, atomicEditing, schemaForPath, renderExternalWidget, onVisibleSourcePathsChange, }) {
|
|
42
|
+
const sourceLiteral = useJsonSourceLiterals()(currentJsonPath);
|
|
43
|
+
// A map of similarly-shaped records renders as a flat table (key column +
|
|
44
|
+
// the nested fields); any other object falls back to the key/value view.
|
|
45
|
+
const dictTableInfo = useMemo(() => (isPlainObject(node) ? getDictOfObjectsInfo(node) : null), [node]);
|
|
46
|
+
if (Array.isArray(node)) {
|
|
47
|
+
return (_jsx(ArrayView, { arr: node, navigateTo: navigateTo, filePath: filePath, currentPath: currentPath, currentJsonPath: currentJsonPath, getTableCellPosition: getTableCellPosition, setTableCellPosition: setTableCellPosition, renderMarkdown: renderMarkdown, uiSize: uiSize, fillHeight: fillHeight, atomicEditing: atomicEditing, schemaForPath: schemaForPath, renderExternalWidget: renderExternalWidget, onVisibleSourcePathsChange: onVisibleSourcePathsChange }));
|
|
48
|
+
}
|
|
49
|
+
if (dictTableInfo) {
|
|
50
|
+
return (_jsx(DictObjectTableView, { info: dictTableInfo, navigateTo: navigateTo, filePath: filePath, currentPath: currentPath, currentJsonPath: currentJsonPath, getTableCellPosition: getTableCellPosition, setTableCellPosition: setTableCellPosition, renderMarkdown: renderMarkdown, uiSize: uiSize, fillHeight: fillHeight, atomicEditing: atomicEditing, schemaForPath: schemaForPath, renderExternalWidget: renderExternalWidget, onVisibleSourcePathsChange: onVisibleSourcePathsChange }));
|
|
51
|
+
}
|
|
52
|
+
if (isPlainObject(node)) {
|
|
53
|
+
return _jsx(ObjectView, { obj: node, navigateTo: navigateTo, currentJsonPath: currentJsonPath, atomicEditing: atomicEditing, renderMarkdown: renderMarkdown, renderExternalWidget: renderExternalWidget, schemaForPath: schemaForPath });
|
|
54
|
+
}
|
|
55
|
+
if (atomicEditing?.canReplace?.(currentJsonPath) === false)
|
|
56
|
+
atomicEditing = undefined;
|
|
57
|
+
const currentDescriptor = schemaForPath?.(currentJsonPath);
|
|
58
|
+
if (currentDescriptor) {
|
|
59
|
+
return (_jsx(JsonViewSchemaValueCell, { sourcePath: currentJsonPath, descriptor: currentDescriptor, label: currentDescriptor.title ?? currentLabel, onClear: atomicEditing ? () => atomicEditing.clear(currentJsonPath) : undefined, onCommit: atomicEditing ? (nextValue) => atomicEditing.commit(currentJsonPath, nextValue) : undefined, onOpen: () => undefined, inputKey: String(currentJsonPath[currentJsonPath.length - 1] ?? currentLabel), renderExternalWidget: renderExternalWidget, renderMarkdown: renderMarkdown, saving: atomicEditing?.saving, value: node }));
|
|
60
|
+
}
|
|
61
|
+
if (typeof node === 'string') {
|
|
62
|
+
const content = (_jsx(ExpandedStringValue, { value: node, renderAsMarkdown: renderAsMarkdown, renderMarkdown: renderMarkdown }));
|
|
63
|
+
return atomicEditing ? (_jsx(AtomicValueEditor, { label: currentLabel, multiline: node.includes('\n'), onCommit: (nextValue) => atomicEditing.commit(currentJsonPath, nextValue), saving: atomicEditing.saving, value: node, children: content })) : content;
|
|
64
|
+
}
|
|
65
|
+
const content = (_jsx("div", { className: "whitespace-pre-wrap text-sm text-foreground", style: { overflowWrap: 'anywhere' }, children: sourceLiteral !== undefined ? _jsx(JsonSourceNumber, { literal: sourceLiteral }) : String(node) }));
|
|
66
|
+
return atomicEditing && isEditableAtomicValue(node) ? (_jsx(AtomicValueEditor, { label: currentLabel, onCommit: (nextValue) => atomicEditing.commit(currentJsonPath, nextValue), saving: atomicEditing.saving, value: node, children: content })) : content;
|
|
67
|
+
}
|
|
68
|
+
function ExpandedStringValue({ value, renderAsMarkdown, renderMarkdown, }) {
|
|
69
|
+
return renderAsMarkdown && renderMarkdown ? (renderMarkdown(value)) : (_jsx("div", { className: "whitespace-pre-wrap text-sm text-foreground", style: { overflowWrap: 'anywhere' }, children: value }));
|
|
70
|
+
}
|
|
71
|
+
function getTabularInfo(arr) {
|
|
72
|
+
if (arr.length === 0)
|
|
73
|
+
return null;
|
|
74
|
+
const first = arr[0];
|
|
75
|
+
// Check if all items are arrays (tuples) with consistent length
|
|
76
|
+
if (Array.isArray(first)) {
|
|
77
|
+
const tupleLength = first.length;
|
|
78
|
+
if (tupleLength === 0)
|
|
79
|
+
return null;
|
|
80
|
+
// Verify all items are arrays with the same length and contain primitives
|
|
81
|
+
for (const item of arr) {
|
|
82
|
+
if (!Array.isArray(item))
|
|
83
|
+
return null;
|
|
84
|
+
if (item.length !== tupleLength)
|
|
85
|
+
return null;
|
|
86
|
+
// Check that tuple contains only primitives (not nested objects/arrays)
|
|
87
|
+
for (const val of item) {
|
|
88
|
+
if (val !== null && typeof val === 'object')
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { type: 'tuples', length: tupleLength };
|
|
93
|
+
}
|
|
94
|
+
if (!isJsonViewTableCandidate(arr))
|
|
95
|
+
return null;
|
|
96
|
+
// Collect all unique keys, preserving order of first appearance
|
|
97
|
+
const seenKeys = new Set();
|
|
98
|
+
const columns = [];
|
|
99
|
+
for (const item of arr) {
|
|
100
|
+
for (const key of Object.keys(item)) {
|
|
101
|
+
if (!seenKeys.has(key)) {
|
|
102
|
+
seenKeys.add(key);
|
|
103
|
+
columns.push(key);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Only show as table if we have at least one column
|
|
108
|
+
return columns.length > 0 ? { type: 'objects', columns } : null;
|
|
109
|
+
}
|
|
110
|
+
function ArrayView({ arr, navigateTo, filePath, currentPath, currentJsonPath, getTableCellPosition, setTableCellPosition, uiSize, fillHeight = false, atomicEditing, schemaForPath, renderMarkdown, renderExternalWidget, onVisibleSourcePathsChange, }) {
|
|
111
|
+
const sourceLiteral = useJsonSourceLiterals();
|
|
112
|
+
const getCellSourcePath = useCallback((rowIndex, column) => [...currentJsonPath, rowIndex, column], [currentJsonPath]);
|
|
113
|
+
if (arr.length === 0)
|
|
114
|
+
return _jsx("div", { className: "text-muted-foreground italic", children: "(empty list)" });
|
|
115
|
+
const tabularInfo = getTabularInfo(arr);
|
|
116
|
+
if (tabularInfo?.type === 'objects') {
|
|
117
|
+
const records = arr;
|
|
118
|
+
const titleKey = inferJsonViewRecordTitleKey(records) ?? tabularInfo.columns[0];
|
|
119
|
+
return (_jsx(TabularObjectArrayView, { arr: records, getCellSourcePath: getCellSourcePath, columns: [titleKey, ...tabularInfo.columns.filter((column) => column !== titleKey)], filePath: filePath, viewStateKey: currentPath, onOpenCell: ({ rowIndex, column }) => {
|
|
120
|
+
setTableCellPosition?.(currentPath, { row: rowIndex, col: column });
|
|
121
|
+
navigateTo(`${rowIndex}.${column}`, [rowIndex, column]);
|
|
122
|
+
}, initialCell: getTableCellPosition?.(currentPath), uiSize: uiSize, fillHeight: fillHeight, onVisibleRowsChange: (indices, filtered) => onVisibleSourcePathsChange?.(filtered
|
|
123
|
+
? indices.map((index) => [...currentJsonPath, index])
|
|
124
|
+
: undefined), recordNavigation: {
|
|
125
|
+
titleColumn: titleKey,
|
|
126
|
+
getLabel: (rowIndex) => sourceLiteral([...currentJsonPath, rowIndex, titleKey])
|
|
127
|
+
?? String(records[rowIndex][titleKey] ?? `Item ${rowIndex + 1}`),
|
|
128
|
+
onOpen: (rowIndex) => navigateTo(String(rowIndex), [rowIndex], { titleKey }),
|
|
129
|
+
renderTitle: ({ value, rowIndex }) => {
|
|
130
|
+
const path = [...currentJsonPath, rowIndex, titleKey];
|
|
131
|
+
const descriptor = schemaForPath?.(path);
|
|
132
|
+
return descriptor
|
|
133
|
+
? _jsx(JsonViewSchemaValueDisplay, { compact: true, decorative: true, passive: true, descriptor: descriptor, onOpen: () => undefined, renderMarkdown: renderMarkdown, sourcePath: path, value: value })
|
|
134
|
+
: value === undefined
|
|
135
|
+
? _jsx("span", { className: "italic text-muted-foreground", children: "Untitled" })
|
|
136
|
+
: _jsx(ValueCellContent, { containerInteractive: false, sourcePath: path, value: value, onOpen: () => undefined });
|
|
137
|
+
},
|
|
138
|
+
}, renderCell: ({ value, rowIndex, column, open }) => (_jsx(GeneralJsonValueCell, { atomicEditing: atomicEditing, compact: true, label: column, navigateTo: open, path: [...currentJsonPath, rowIndex, column], renderMarkdown: renderMarkdown, renderExternalWidget: renderExternalWidget, schemaForPath: schemaForPath, value: value })), onDeleteRows: atomicEditing
|
|
139
|
+
? (rowIndices) => atomicEditing.removeMany(rowIndices.map((rowIndex) => [...currentJsonPath, rowIndex]))
|
|
140
|
+
: undefined, rowDeletionDisabled: atomicEditing?.saving }));
|
|
141
|
+
}
|
|
142
|
+
if (tabularInfo?.type === 'tuples') {
|
|
143
|
+
return (_jsx(TupleArrayView, { arr: arr, tupleLength: tabularInfo.length, navigateTo: navigateTo, filePath: filePath, arrayPath: currentPath, currentJsonPath: currentJsonPath, uiSize: uiSize, fillHeight: fillHeight, atomicEditing: atomicEditing, schemaForPath: schemaForPath, renderMarkdown: renderMarkdown, renderExternalWidget: renderExternalWidget, onVisibleSourcePathsChange: onVisibleSourcePathsChange }));
|
|
144
|
+
}
|
|
145
|
+
return (_jsx(SingleColumnArrayView, { arr: arr, navigateTo: navigateTo, filePath: filePath, arrayPath: currentPath, currentJsonPath: currentJsonPath, uiSize: uiSize, fillHeight: fillHeight, atomicEditing: atomicEditing, schemaForPath: schemaForPath, renderMarkdown: renderMarkdown, renderExternalWidget: renderExternalWidget, onVisibleSourcePathsChange: onVisibleSourcePathsChange }));
|
|
146
|
+
}
|
|
147
|
+
function TupleArrayView({ arr, tupleLength, navigateTo, filePath, arrayPath, currentJsonPath, uiSize, fillHeight = false, atomicEditing, schemaForPath, renderMarkdown, renderExternalWidget, onVisibleSourcePathsChange, }) {
|
|
148
|
+
const getCellSourcePath = useCallback((rowIndex, column) => [...currentJsonPath, rowIndex, Number(column)], [currentJsonPath]);
|
|
149
|
+
const columns = useMemo(() => Array.from({ length: tupleLength }, (_, i) => String(i)), [tupleLength]);
|
|
150
|
+
const records = useMemo(() => arr.map((tuple) => tuple.reduce((record, value, index) => {
|
|
151
|
+
record[String(index)] = value;
|
|
152
|
+
return record;
|
|
153
|
+
}, {})), [arr]);
|
|
154
|
+
return (_jsx(TabularObjectArrayView, { arr: records, getCellSourcePath: getCellSourcePath, columns: columns, filePath: filePath, viewStateKey: arrayPath, onOpenCell: ({ rowIndex, column }) => {
|
|
155
|
+
navigateTo(`${rowIndex}.${column}`, [rowIndex, Number(column)]);
|
|
156
|
+
}, uiSize: uiSize, fillHeight: fillHeight, onVisibleRowsChange: (indices, filtered) => onVisibleSourcePathsChange?.(filtered
|
|
157
|
+
? indices.map((index) => [...currentJsonPath, index])
|
|
158
|
+
: undefined), renderCell: ({ value, rowIndex, column, open }) => (_jsx(GeneralJsonValueCell, { atomicEditing: atomicEditing, compact: true, label: column, navigateTo: open, path: [...currentJsonPath, rowIndex, Number(column)], renderMarkdown: renderMarkdown, renderExternalWidget: renderExternalWidget, schemaForPath: schemaForPath, value: value })), onDeleteRows: atomicEditing
|
|
159
|
+
? (rowIndices) => atomicEditing.removeMany(rowIndices.map((rowIndex) => [...currentJsonPath, rowIndex]))
|
|
160
|
+
: undefined, rowDeletionDisabled: atomicEditing?.saving }));
|
|
161
|
+
}
|
|
162
|
+
function SingleColumnArrayView({ arr, navigateTo, filePath, arrayPath, currentJsonPath, uiSize, fillHeight = false, atomicEditing, schemaForPath, renderMarkdown, renderExternalWidget, onVisibleSourcePathsChange, }) {
|
|
163
|
+
const getCellSourcePath = useCallback((rowIndex) => [...currentJsonPath, rowIndex], [currentJsonPath]);
|
|
164
|
+
const records = useMemo(() => arr.map((item) => ({ Value: item })), [arr]);
|
|
165
|
+
return (_jsx(TabularObjectArrayView, { arr: records, getCellSourcePath: getCellSourcePath, columns: ['Value'], filePath: filePath, viewStateKey: arrayPath, onOpenCell: ({ rowIndex }) => navigateTo(String(rowIndex), [rowIndex]), uiSize: uiSize, fillHeight: fillHeight, onVisibleRowsChange: (indices, filtered) => onVisibleSourcePathsChange?.(filtered
|
|
166
|
+
? indices.map((index) => [...currentJsonPath, index])
|
|
167
|
+
: undefined), renderCell: ({ value, rowIndex, open }) => (_jsx(GeneralJsonValueCell, { atomicEditing: atomicEditing, compact: true, label: "Value", navigateTo: open, path: [...currentJsonPath, rowIndex], renderMarkdown: renderMarkdown, renderExternalWidget: renderExternalWidget, schemaForPath: schemaForPath, value: value })), onDeleteRows: atomicEditing
|
|
168
|
+
? (rowIndices) => atomicEditing.removeMany(rowIndices.map((rowIndex) => [...currentJsonPath, rowIndex]))
|
|
169
|
+
: undefined, rowDeletionDisabled: atomicEditing?.saving }));
|
|
170
|
+
}
|
|
171
|
+
function DictObjectTableView({ info, navigateTo, filePath, currentPath, currentJsonPath, getTableCellPosition, setTableCellPosition, uiSize, fillHeight = false, atomicEditing, schemaForPath, renderMarkdown, renderExternalWidget, onVisibleSourcePathsChange, }) {
|
|
172
|
+
const getCellSourcePath = useCallback((rowIndex, column) => column === info.keyColumn ? undefined : [...currentJsonPath, info.keys[rowIndex], column], [currentJsonPath, info]);
|
|
173
|
+
// Rows are keyed by their original map key (not row index). The key column
|
|
174
|
+
// drills into the whole record (`obj[key]`); every other column drills into
|
|
175
|
+
// `obj[key].<field>`. Keying both the node and the path off the map key means
|
|
176
|
+
// a click and a later replay land on the same node.
|
|
177
|
+
// The key column shows the map key as text but has no field of its own, so a
|
|
178
|
+
// click drills into the record object rather than the (dead-end) key string.
|
|
179
|
+
return (_jsx(TabularObjectArrayView, { arr: info.records, getCellSourcePath: getCellSourcePath, columns: info.columns, filePath: filePath, viewStateKey: currentPath, onOpenCell: ({ rowIndex, column }) => {
|
|
180
|
+
const key = info.keys[rowIndex];
|
|
181
|
+
const navigationKey = column === info.keyColumn ? key : `${key}.${column}`;
|
|
182
|
+
const relativePath = column === info.keyColumn ? [key] : [key, column];
|
|
183
|
+
setTableCellPosition?.(currentPath, { row: rowIndex, col: column });
|
|
184
|
+
navigateTo(navigationKey, relativePath);
|
|
185
|
+
}, initialCell: getTableCellPosition?.(currentPath), uiSize: uiSize, fillHeight: fillHeight, onVisibleRowsChange: (indices, filtered) => onVisibleSourcePathsChange?.(filtered
|
|
186
|
+
? indices.map((index) => [...currentJsonPath, info.keys[index]])
|
|
187
|
+
: undefined), recordNavigation: {
|
|
188
|
+
titleColumn: info.keyColumn,
|
|
189
|
+
getLabel: (rowIndex) => info.keys[rowIndex],
|
|
190
|
+
onOpen: (rowIndex) => navigateTo(info.keys[rowIndex], [info.keys[rowIndex]], {}),
|
|
191
|
+
renderTitle: ({ value }) => _jsx("span", { className: "min-w-0 truncate", children: String(value) }),
|
|
192
|
+
}, renderCell: ({ value, rowIndex, column, open }) => (_jsx(GeneralJsonValueCell, { atomicEditing: atomicEditing, compact: true, label: column, navigateTo: open, path: column === info.keyColumn ? null : [...currentJsonPath, info.keys[rowIndex], column], renderMarkdown: renderMarkdown, renderExternalWidget: renderExternalWidget, schemaForPath: schemaForPath, value: value })), onDeleteRows: atomicEditing
|
|
193
|
+
? (rowIndices) => atomicEditing.removeMany(rowIndices.map((rowIndex) => [...currentJsonPath, info.keys[rowIndex]]))
|
|
194
|
+
: undefined, rowDeletionDisabled: atomicEditing?.saving }));
|
|
195
|
+
}
|
|
196
|
+
function ObjectView({ obj, navigateTo, currentJsonPath, atomicEditing, renderMarkdown, renderExternalWidget, schemaForPath }) {
|
|
197
|
+
return (
|
|
198
|
+
// `shrink-0`: when the viewer fills a bounded viewport its scroller is a
|
|
199
|
+
// flex column, and this frame is a scroll container, so its `min-height:
|
|
200
|
+
// auto` resolves to 0 — without the guard a tall object is squeezed and
|
|
201
|
+
// clipped with no scrollbar of its own.
|
|
202
|
+
_jsx("div", { className: cn(TABULAR_DATA_FRAME_CLASS, 'shrink-0'), children: _jsx("div", { className: "@container max-w-full overflow-x-auto overscroll-x-none [word-break:normal] [overflow-wrap:normal]", children: _jsx("table", { className: "mb-0 w-full table-fixed border-separate border-spacing-0 text-sm", children: _jsx("tbody", { children: Object.entries(obj).map(([k, v], i, arr) => (_jsxs("tr", { children: [_jsx("th", { className: `w-[6rem] @[208px]:w-[45%] @[432px]:w-[12rem] border-r border-border bg-background px-4 py-1.5 text-left align-top font-normal text-muted-foreground ${i < arr.length - 1 ? 'border-b' : ''}`, children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("span", { className: "flex min-h-7 items-center leading-5", children: _jsx("span", { className: "block min-w-0 truncate", children: k }) }) }), _jsx(TooltipContent, { side: "top", children: k })] }) }), _jsx("td", { className: `min-w-0 overflow-hidden break-words px-4 py-1.5 align-top transition-colors hover:bg-accent/50 ${i < arr.length - 1 ? 'border-b border-border' : ''}`, children: k === 'content'
|
|
203
|
+
&& typeof v === 'string'
|
|
204
|
+
&& renderMarkdown
|
|
205
|
+
&& !schemaForPath?.([...currentJsonPath, k]) ? (() => {
|
|
206
|
+
const open = () => navigateTo(k, [k]);
|
|
207
|
+
const expandAction = _jsx(ExpandValueButton, { onClick: open });
|
|
208
|
+
const content = renderMarkdown(v);
|
|
209
|
+
const jsonPath = [...currentJsonPath, k];
|
|
210
|
+
if (!atomicEditing || atomicEditing.canReplace?.(jsonPath) === false) {
|
|
211
|
+
return (_jsx(StructuredValueCellFrame, { actions: expandAction, children: content }));
|
|
212
|
+
}
|
|
213
|
+
return (_jsx(AtomicValueEditor, { actions: expandAction, label: k, multiline: true, onCommit: (nextValue) => atomicEditing.commit(jsonPath, nextValue), saving: atomicEditing.saving, value: v, children: content }));
|
|
214
|
+
})() : (_jsx(GeneralJsonValueCell, { atomicEditing: atomicEditing, label: k, navigateTo: () => navigateTo(k, [k]), path: [...currentJsonPath, k], renderMarkdown: renderMarkdown, renderExternalWidget: renderExternalWidget, schemaForPath: schemaForPath, value: v })) })] }, k))) }) }) }) }));
|
|
215
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function copyTextToClipboard(value: string): Promise<void>;
|