@treenity/react 3.0.0 → 3.0.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/README.md +91 -0
- package/dist/AclEditor.d.ts +1 -1
- package/dist/AclEditor.d.ts.map +1 -1
- package/dist/AclEditor.js +5 -5
- package/dist/AclEditor.js.map +1 -1
- package/dist/ActionCards.d.ts +9 -0
- package/dist/ActionCards.d.ts.map +1 -0
- package/dist/ActionCards.js +96 -0
- package/dist/ActionCards.js.map +1 -0
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +71 -185
- package/dist/App.js.map +1 -1
- package/dist/ComponentSection.d.ts +15 -0
- package/dist/ComponentSection.d.ts.map +1 -0
- package/dist/ComponentSection.js +25 -0
- package/dist/ComponentSection.js.map +1 -0
- package/dist/ErrorBoundary.d.ts +18 -0
- package/dist/ErrorBoundary.d.ts.map +1 -0
- package/dist/ErrorBoundary.js +17 -0
- package/dist/ErrorBoundary.js.map +1 -0
- package/dist/Inspector.d.ts +1 -0
- package/dist/Inspector.d.ts.map +1 -1
- package/dist/Inspector.js +22 -347
- package/dist/Inspector.js.map +1 -1
- package/dist/Login.d.ts +8 -0
- package/dist/Login.d.ts.map +1 -0
- package/dist/Login.js +45 -0
- package/dist/Login.js.map +1 -0
- package/dist/NodeEditor.d.ts +11 -0
- package/dist/NodeEditor.d.ts.map +1 -0
- package/dist/NodeEditor.js +157 -0
- package/dist/NodeEditor.js.map +1 -0
- package/dist/Tree.d.ts +1 -0
- package/dist/Tree.d.ts.map +1 -1
- package/dist/Tree.js +8 -27
- package/dist/Tree.js.map +1 -1
- package/dist/bind/engine.js +1 -1
- package/dist/bind/engine.js.map +1 -1
- package/dist/bind/eval.d.ts +1 -1
- package/dist/bind/eval.d.ts.map +1 -1
- package/dist/bind/hook.d.ts +1 -1
- package/dist/bind/hook.d.ts.map +1 -1
- package/dist/bind/hook.js +1 -1
- package/dist/bind/hook.js.map +1 -1
- package/dist/cache.d.ts +1 -1
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +4 -0
- package/dist/cache.js.map +1 -1
- package/dist/client-tree.d.ts +1 -2
- package/dist/client-tree.d.ts.map +1 -1
- package/dist/client-tree.js +12 -5
- package/dist/client-tree.js.map +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +2 -4
- package/dist/client.js.map +1 -1
- package/dist/components/ConfirmDialog.d.ts +9 -0
- package/dist/components/ConfirmDialog.d.ts.map +1 -0
- package/dist/components/ConfirmDialog.js +6 -0
- package/dist/components/ConfirmDialog.js.map +1 -0
- package/dist/components/ConfirmPopover.d.ts +8 -0
- package/dist/components/ConfirmPopover.d.ts.map +1 -0
- package/dist/components/ConfirmPopover.js +9 -0
- package/dist/components/ConfirmPopover.js.map +1 -0
- package/dist/components/PathBreadcrumb.d.ts +5 -0
- package/dist/components/PathBreadcrumb.d.ts.map +1 -0
- package/dist/components/PathBreadcrumb.js +16 -0
- package/dist/components/PathBreadcrumb.js.map +1 -0
- package/dist/components/lib/utils.d.ts +3 -0
- package/dist/components/lib/utils.d.ts.map +1 -0
- package/dist/components/lib/utils.js +6 -0
- package/dist/components/lib/utils.js.map +1 -0
- package/dist/components/ui/accordion.js +1 -1
- package/dist/components/ui/accordion.js.map +1 -1
- package/dist/components/ui/alert-dialog.d.ts +19 -0
- package/dist/components/ui/alert-dialog.d.ts.map +1 -0
- package/dist/components/ui/alert-dialog.js +42 -0
- package/dist/components/ui/alert-dialog.js.map +1 -0
- package/dist/components/ui/badge.js +1 -1
- package/dist/components/ui/badge.js.map +1 -1
- package/dist/components/ui/breadcrumb.d.ts +12 -0
- package/dist/components/ui/breadcrumb.d.ts.map +1 -0
- package/dist/components/ui/breadcrumb.js +28 -0
- package/dist/components/ui/breadcrumb.js.map +1 -0
- package/dist/components/ui/button.d.ts +9 -8
- package/dist/components/ui/button.d.ts.map +1 -1
- package/dist/components/ui/button.js +26 -21
- package/dist/components/ui/button.js.map +1 -1
- package/dist/components/ui/card.d.ts +10 -0
- package/dist/components/ui/card.d.ts.map +1 -0
- package/dist/components/ui/card.js +25 -0
- package/dist/components/ui/card.js.map +1 -0
- package/dist/components/ui/checkbox.js +1 -1
- package/dist/components/ui/checkbox.js.map +1 -1
- package/dist/components/ui/collapsible.d.ts +6 -0
- package/dist/components/ui/collapsible.d.ts.map +1 -0
- package/dist/components/ui/collapsible.js +13 -0
- package/dist/components/ui/collapsible.js.map +1 -0
- package/dist/components/ui/command.d.ts +19 -0
- package/dist/components/ui/command.d.ts.map +1 -0
- package/dist/components/ui/command.js +35 -0
- package/dist/components/ui/command.js.map +1 -0
- package/dist/components/ui/dialog.d.ts.map +1 -1
- package/dist/components/ui/dialog.js +1 -1
- package/dist/components/ui/dialog.js.map +1 -1
- package/dist/components/ui/drawer.js +1 -1
- package/dist/components/ui/drawer.js.map +1 -1
- package/dist/components/ui/dropdown-menu.d.ts +26 -0
- package/dist/components/ui/dropdown-menu.d.ts.map +1 -0
- package/dist/components/ui/dropdown-menu.js +52 -0
- package/dist/components/ui/dropdown-menu.js.map +1 -0
- package/dist/components/ui/form-field.d.ts +7 -0
- package/dist/components/ui/form-field.d.ts.map +1 -0
- package/dist/components/ui/form-field.js +17 -0
- package/dist/components/ui/form-field.js.map +1 -0
- package/dist/components/ui/input.js +1 -1
- package/dist/components/ui/input.js.map +1 -1
- package/dist/components/ui/label.js +1 -1
- package/dist/components/ui/label.js.map +1 -1
- package/dist/components/ui/pagination.d.ts +14 -0
- package/dist/components/ui/pagination.d.ts.map +1 -0
- package/dist/components/ui/pagination.js +30 -0
- package/dist/components/ui/pagination.js.map +1 -0
- package/dist/components/ui/popover.js +2 -2
- package/dist/components/ui/popover.js.map +1 -1
- package/dist/components/ui/progress.js +1 -1
- package/dist/components/ui/progress.js.map +1 -1
- package/dist/components/ui/resizable.d.ts +8 -0
- package/dist/components/ui/resizable.d.ts.map +1 -0
- package/dist/components/ui/resizable.js +14 -0
- package/dist/components/ui/resizable.js.map +1 -0
- package/dist/components/ui/scroll-area.d.ts +6 -0
- package/dist/components/ui/scroll-area.d.ts.map +1 -0
- package/dist/components/ui/scroll-area.js +13 -0
- package/dist/components/ui/scroll-area.js.map +1 -0
- package/dist/components/ui/select.js +1 -1
- package/dist/components/ui/select.js.map +1 -1
- package/dist/components/ui/separator.d.ts +5 -0
- package/dist/components/ui/separator.d.ts.map +1 -0
- package/dist/components/ui/separator.js +9 -0
- package/dist/components/ui/separator.js.map +1 -0
- package/dist/components/ui/sheet.d.ts +15 -0
- package/dist/components/ui/sheet.d.ts.map +1 -0
- package/dist/components/ui/sheet.js +40 -0
- package/dist/components/ui/sheet.js.map +1 -0
- package/dist/components/ui/skeleton.d.ts +3 -0
- package/dist/components/ui/skeleton.d.ts.map +1 -0
- package/dist/components/ui/skeleton.js +7 -0
- package/dist/components/ui/skeleton.js.map +1 -0
- package/dist/components/ui/slider.js +1 -1
- package/dist/components/ui/slider.js.map +1 -1
- package/dist/components/ui/switch.js +1 -1
- package/dist/components/ui/switch.js.map +1 -1
- package/dist/components/ui/table.d.ts +11 -0
- package/dist/components/ui/table.d.ts.map +1 -0
- package/dist/components/ui/table.js +29 -0
- package/dist/components/ui/table.js.map +1 -0
- package/dist/components/ui/tabs.d.ts +12 -0
- package/dist/components/ui/tabs.d.ts.map +1 -0
- package/dist/components/ui/tabs.js +29 -0
- package/dist/components/ui/tabs.js.map +1 -0
- package/dist/components/ui/textarea.js +1 -1
- package/dist/components/ui/textarea.js.map +1 -1
- package/dist/components/ui/toggle-group.d.ts +10 -0
- package/dist/components/ui/toggle-group.d.ts.map +1 -0
- package/dist/components/ui/toggle-group.js +23 -0
- package/dist/components/ui/toggle-group.js.map +1 -0
- package/dist/components/ui/toggle.d.ts +10 -0
- package/dist/components/ui/toggle.d.ts.map +1 -0
- package/dist/components/ui/toggle.js +27 -0
- package/dist/components/ui/toggle.js.map +1 -0
- package/dist/components/ui/tooltip.js +1 -1
- package/dist/components/ui/tooltip.js.map +1 -1
- package/dist/context/index.d.ts +27 -10
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +43 -36
- package/dist/context/index.js.map +1 -1
- package/dist/events.d.ts +10 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +78 -0
- package/dist/events.js.map +1 -0
- package/dist/fiber-tree.d.ts +3 -0
- package/dist/fiber-tree.d.ts.map +1 -0
- package/dist/fiber-tree.js +93 -0
- package/dist/fiber-tree.js.map +1 -0
- package/dist/hooks.d.ts +5 -2
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +66 -6
- package/dist/hooks.js.map +1 -1
- package/dist/idb.d.ts +1 -1
- package/dist/idb.d.ts.map +1 -1
- package/dist/lib/to-plain.d.ts +2 -0
- package/dist/lib/to-plain.d.ts.map +1 -0
- package/dist/lib/to-plain.js +21 -0
- package/dist/lib/to-plain.js.map +1 -0
- package/dist/main.d.ts +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +4 -3
- package/dist/main.js.map +1 -1
- package/dist/mods/clients.d.ts +3 -0
- package/dist/mods/clients.d.ts.map +1 -0
- package/dist/mods/clients.js +4 -0
- package/dist/mods/clients.js.map +1 -0
- package/dist/mods/editor-ui/FieldLabel.d.ts +15 -0
- package/dist/mods/editor-ui/FieldLabel.d.ts.map +1 -0
- package/dist/mods/editor-ui/FieldLabel.js +55 -0
- package/dist/mods/editor-ui/FieldLabel.js.map +1 -0
- package/dist/mods/editor-ui/client.d.ts +1 -1
- package/dist/mods/editor-ui/client.d.ts.map +1 -1
- package/dist/mods/editor-ui/client.js +1 -1
- package/dist/mods/editor-ui/client.js.map +1 -1
- package/dist/mods/editor-ui/default-edit.d.ts +2 -0
- package/dist/mods/editor-ui/default-edit.d.ts.map +1 -0
- package/dist/mods/editor-ui/default-edit.js +54 -0
- package/dist/mods/editor-ui/default-edit.js.map +1 -0
- package/dist/mods/editor-ui/default-view.d.ts +8 -1
- package/dist/mods/editor-ui/default-view.d.ts.map +1 -1
- package/dist/mods/editor-ui/default-view.js +8 -5
- package/dist/mods/editor-ui/default-view.js.map +1 -1
- package/dist/mods/editor-ui/dir-view.js +0 -2
- package/dist/mods/editor-ui/dir-view.js.map +1 -1
- package/dist/mods/editor-ui/empty-placeholder.d.ts +5 -0
- package/dist/mods/editor-ui/empty-placeholder.d.ts.map +1 -0
- package/dist/mods/editor-ui/empty-placeholder.js +14 -0
- package/dist/mods/editor-ui/empty-placeholder.js.map +1 -0
- package/dist/mods/editor-ui/form-field.d.ts +17 -0
- package/dist/mods/editor-ui/form-field.d.ts.map +1 -0
- package/dist/mods/editor-ui/form-field.js +68 -0
- package/dist/mods/editor-ui/form-field.js.map +1 -0
- package/dist/mods/editor-ui/form-fields.d.ts +1 -2
- package/dist/mods/editor-ui/form-fields.d.ts.map +1 -1
- package/dist/mods/editor-ui/form-fields.js +56 -60
- package/dist/mods/editor-ui/form-fields.js.map +1 -1
- package/dist/mods/editor-ui/layout-view.js +3 -2
- package/dist/mods/editor-ui/layout-view.js.map +1 -1
- package/dist/mods/editor-ui/list-items.js +1 -1
- package/dist/mods/editor-ui/list-items.js.map +1 -1
- package/dist/mods/editor-ui/node-utils.d.ts +2 -2
- package/dist/mods/editor-ui/node-utils.d.ts.map +1 -1
- package/dist/mods/editor-ui/node-utils.js +4 -5
- package/dist/mods/editor-ui/node-utils.js.map +1 -1
- package/dist/mods/editor-ui/type-picker.d.ts +15 -0
- package/dist/mods/editor-ui/type-picker.d.ts.map +1 -0
- package/dist/mods/editor-ui/type-picker.js +69 -0
- package/dist/mods/editor-ui/type-picker.js.map +1 -0
- package/dist/mods/editor-ui/user-view.js +1 -1
- package/dist/mods/editor-ui/user-view.js.map +1 -1
- package/dist/mods/servers.d.ts +1 -0
- package/dist/mods/servers.d.ts.map +1 -0
- package/dist/mods/servers.js +4 -0
- package/dist/mods/servers.js.map +1 -0
- package/dist/mods/treenity/groups/index.js +1 -1
- package/dist/mods/treenity/groups/index.js.map +1 -1
- package/dist/mods/treenity/preview.js +1 -1
- package/dist/mods/treenity/preview.js.map +1 -1
- package/dist/mods/treenity/ref-view.js +1 -1
- package/dist/mods/treenity/ref-view.js.map +1 -1
- package/dist/mods/treenity/schema-form.js +1 -1
- package/dist/mods/treenity/schema-form.js.map +1 -1
- package/dist/mods/treenity/seed.js +1 -1
- package/dist/mods/treenity/seed.js.map +1 -1
- package/dist/mods/treenity/type-view.js +1 -1
- package/dist/mods/treenity/type-view.js.map +1 -1
- package/dist/schema-loader.d.ts +1 -1
- package/dist/schema-loader.d.ts.map +1 -1
- package/dist/schema-loader.js +1 -1
- package/dist/schema-loader.js.map +1 -1
- package/dist/symbols.d.ts +5 -0
- package/dist/symbols.d.ts.map +1 -0
- package/dist/symbols.js +16 -0
- package/dist/symbols.js.map +1 -0
- package/dist/trpc.d.ts +10 -3
- package/dist/trpc.d.ts.map +1 -1
- package/package.json +74 -8
- package/src/AclEditor.tsx +11 -18
- package/src/ActionCards.tsx +224 -0
- package/src/App.tsx +204 -385
- package/src/ComponentSection.tsx +113 -0
- package/src/ErrorBoundary.tsx +37 -0
- package/src/Inspector.css +54 -0
- package/src/Inspector.tsx +73 -793
- package/src/Login.tsx +97 -0
- package/src/NodeEditor.tsx +300 -0
- package/src/Tree.css +91 -0
- package/src/Tree.tsx +40 -43
- package/src/bind/bind.test.ts +1 -1
- package/src/bind/engine.ts +1 -1
- package/src/bind/eval.ts +1 -1
- package/src/bind/hook.ts +1 -1
- package/src/bind/pipes.ts +1 -1
- package/src/cache.ts +5 -1
- package/src/client-tree.test.ts +1 -1
- package/src/client-tree.ts +22 -16
- package/src/client.ts +2 -4
- package/src/components/ConfirmDialog.tsx +34 -0
- package/src/components/ConfirmPopover.tsx +41 -0
- package/src/components/PathBreadcrumb.tsx +36 -0
- package/src/components/lib/utils.ts +6 -0
- package/src/components/lib/utils.ts.bak +6 -0
- package/src/components/ui/accordion.tsx +1 -1
- package/src/components/ui/alert-dialog.tsx +189 -0
- package/src/components/ui/badge.tsx +1 -1
- package/src/components/ui/breadcrumb.tsx +108 -0
- package/src/components/ui/button.tsx +53 -31
- package/src/components/ui/card.tsx +91 -0
- package/src/components/ui/checkbox.tsx +1 -1
- package/src/components/ui/collapsible.tsx +31 -0
- package/src/components/ui/command.tsx +177 -0
- package/src/components/ui/dialog.tsx +1 -2
- package/src/components/ui/drawer.tsx +1 -1
- package/src/components/ui/dropdown-menu.tsx +256 -0
- package/src/components/ui/form-field.tsx +37 -0
- package/src/components/ui/input.tsx +1 -1
- package/src/components/ui/label.tsx +1 -1
- package/src/components/ui/pagination.tsx +127 -0
- package/src/components/ui/popover.tsx +2 -2
- package/src/components/ui/progress.tsx +1 -1
- package/src/components/ui/resizable.tsx +47 -0
- package/src/components/ui/scroll-area.tsx +55 -0
- package/src/components/ui/select.tsx +1 -1
- package/src/components/ui/separator.tsx +27 -0
- package/src/components/ui/sheet.tsx +140 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +1 -1
- package/src/components/ui/switch.tsx +1 -1
- package/src/components/ui/table.tsx +115 -0
- package/src/components/ui/tabs.tsx +88 -0
- package/src/components/ui/textarea.tsx +1 -1
- package/src/components/ui/toggle-group.tsx +82 -0
- package/src/components/ui/toggle.tsx +46 -0
- package/src/components/ui/tooltip.tsx +1 -1
- package/src/context/index.tsx +75 -42
- package/src/events.ts +81 -0
- package/src/fiber-tree.ts +112 -0
- package/src/hooks.ts +88 -9
- package/src/idb.ts +1 -1
- package/src/lib/to-plain.ts +21 -0
- package/src/main.tsx +3 -1
- package/src/mods/clients.ts +3 -0
- package/src/mods/editor-ui/FieldLabel.tsx +124 -0
- package/src/mods/editor-ui/client.ts +1 -1
- package/src/mods/editor-ui/default-edit.tsx +99 -0
- package/src/mods/editor-ui/default-view.tsx +13 -8
- package/src/mods/editor-ui/dir-view.tsx +2 -2
- package/src/mods/editor-ui/editor-ui.css +174 -0
- package/src/mods/editor-ui/empty-placeholder.tsx +39 -0
- package/src/mods/editor-ui/form-field.tsx +144 -0
- package/src/mods/editor-ui/form-fields.tsx +132 -113
- package/src/mods/editor-ui/layout-view.tsx +4 -2
- package/src/mods/editor-ui/list-items.tsx +2 -2
- package/src/mods/editor-ui/node-utils.ts +4 -5
- package/src/mods/editor-ui/type-picker.tsx +147 -0
- package/src/mods/editor-ui/user-view.tsx +1 -1
- package/src/mods/servers.ts +2 -0
- package/src/mods/treenity/groups/index.tsx +1 -1
- package/src/mods/treenity/preview.tsx +1 -1
- package/src/mods/treenity/ref-view.tsx +1 -1
- package/src/mods/treenity/schema-form.tsx +1 -1
- package/src/mods/treenity/seed.ts +1 -1
- package/src/mods/treenity/type-view.tsx +1 -1
- package/src/optimistic.test.ts +111 -0
- package/src/remote-tree.test.ts +1 -1
- package/src/remote-tree.ts +1 -1
- package/src/root.css +117 -0
- package/src/schema-loader.ts +1 -1
- package/src/symbols.ts +18 -0
- package/src/index.html +0 -14
- package/src/style.css +0 -1269
- package/src/vite-env.d.ts +0 -3
package/src/Inspector.tsx
CHANGED
|
@@ -1,26 +1,18 @@
|
|
|
1
1
|
// Inspector — view + edit panel for selected node (Unity-style inspector)
|
|
2
|
-
// Shell only:
|
|
3
|
-
|
|
2
|
+
// Shell only: header, rendered view, delegates editing to NodeEditor
|
|
3
|
+
|
|
4
|
+
import './Inspector.css';
|
|
5
|
+
import { ConfirmDialog } from '#components/ConfirmDialog';
|
|
6
|
+
import { PathBreadcrumb } from '#components/PathBreadcrumb';
|
|
7
|
+
import { Badge } from '#components/ui/badge';
|
|
8
|
+
import { Button } from '#components/ui/button';
|
|
9
|
+
import { ScrollArea } from '#components/ui/scroll-area';
|
|
4
10
|
import { Render, RenderContext } from '#context';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
getSchema,
|
|
11
|
-
getViewContexts,
|
|
12
|
-
pickDefaultContext,
|
|
13
|
-
} from '#mods/editor-ui/node-utils';
|
|
14
|
-
import { type ComponentData, type GroupPerm, type NodeData, resolve, resolveExact } from '@treenity/core/core';
|
|
15
|
-
import type { TypeSchema } from '@treenity/core/schema/types';
|
|
16
|
-
import { createElement, useEffect, useRef, useState } from 'react';
|
|
17
|
-
import { AclEditor } from './AclEditor';
|
|
18
|
-
import * as cache from './cache';
|
|
19
|
-
import { set, usePath } from './hooks';
|
|
20
|
-
import { useSchema } from './schema-loader';
|
|
21
|
-
import { trpc } from './trpc';
|
|
22
|
-
|
|
23
|
-
type AnyClass = { new(): Record<string, unknown> };
|
|
11
|
+
import { getViewContexts, pickDefaultContext } from '#mods/editor-ui/node-utils';
|
|
12
|
+
import { useState } from 'react';
|
|
13
|
+
import { ErrorBoundary } from './ErrorBoundary';
|
|
14
|
+
import { usePath } from './hooks';
|
|
15
|
+
import { NodeEditor } from './NodeEditor';
|
|
24
16
|
|
|
25
17
|
type Props = {
|
|
26
18
|
path: string | null;
|
|
@@ -32,553 +24,24 @@ type Props = {
|
|
|
32
24
|
toast: (msg: string) => void;
|
|
33
25
|
};
|
|
34
26
|
|
|
35
|
-
// Breadcrumb from path
|
|
36
|
-
function Breadcrumb({ path, onSelect }: { path: string; onSelect: (p: string) => void }) {
|
|
37
|
-
if (path === '/')
|
|
38
|
-
return (
|
|
39
|
-
<div className="editor-breadcrumb">
|
|
40
|
-
<span>/</span>
|
|
41
|
-
</div>
|
|
42
|
-
);
|
|
43
|
-
const parts = path.split('/').filter(Boolean);
|
|
44
|
-
const crumbs: { label: string; path: string }[] = [{ label: '/', path: '/' }];
|
|
45
|
-
let cur = '';
|
|
46
|
-
for (const p of parts) {
|
|
47
|
-
cur += '/' + p;
|
|
48
|
-
crumbs.push({ label: p, path: cur });
|
|
49
|
-
}
|
|
50
|
-
return (
|
|
51
|
-
<div className="editor-breadcrumb">
|
|
52
|
-
{crumbs.map((c, i) => (
|
|
53
|
-
<span key={c.path}>
|
|
54
|
-
{i > 0 && <span className="sep">/</span>}
|
|
55
|
-
<span onClick={() => onSelect(c.path)}>{c.label === '/' ? 'root' : c.label}</span>
|
|
56
|
-
</span>
|
|
57
|
-
))}
|
|
58
|
-
</div>
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Pretty-print action result value
|
|
63
|
-
function ResultView({ value }: { value: unknown }) {
|
|
64
|
-
if (value === undefined || value === null) return null;
|
|
65
|
-
if (typeof value !== 'object')
|
|
66
|
-
return <span className="font-mono text-[11px]">{String(value)}</span>;
|
|
67
|
-
|
|
68
|
-
// Object/array with typed $type → render via Render
|
|
69
|
-
if ('$type' in (value as any)) {
|
|
70
|
-
return <Render value={value as ComponentData} />;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Plain object — key/value pairs
|
|
74
|
-
const entries = Object.entries(value as Record<string, unknown>);
|
|
75
|
-
if (entries.length === 0) return <span className="text-muted-foreground text-[11px]">empty</span>;
|
|
76
|
-
|
|
77
|
-
return (
|
|
78
|
-
<div className="flex flex-col gap-0.5">
|
|
79
|
-
{entries.map(([k, v]) => (
|
|
80
|
-
<div key={k} className="flex gap-2 text-[11px]">
|
|
81
|
-
<span className="text-muted-foreground shrink-0">{k}</span>
|
|
82
|
-
<span className="font-mono text-foreground/80 truncate">
|
|
83
|
-
{typeof v === 'object' && v !== null ? JSON.stringify(v) : String(v ?? '')}
|
|
84
|
-
</span>
|
|
85
|
-
</div>
|
|
86
|
-
))}
|
|
87
|
-
</div>
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Action pills — compact action buttons that expand on click
|
|
92
|
-
function ActionCardList({
|
|
93
|
-
path,
|
|
94
|
-
componentName,
|
|
95
|
-
compType,
|
|
96
|
-
toast,
|
|
97
|
-
onActionComplete,
|
|
98
|
-
}: {
|
|
99
|
-
path: string;
|
|
100
|
-
componentName: string;
|
|
101
|
-
compType: string;
|
|
102
|
-
compData: Record<string, unknown>;
|
|
103
|
-
toast: (msg: string) => void;
|
|
104
|
-
onActionComplete?: () => void;
|
|
105
|
-
}) {
|
|
106
|
-
const schema = useSchema(compType);
|
|
107
|
-
const [expanded, setExpanded] = useState<string | null>(null);
|
|
108
|
-
const [paramsText, setParamsText] = useState<Record<string, string>>({});
|
|
109
|
-
const [schemaData, setSchemaData] = useState<Record<string, Record<string, unknown>>>({});
|
|
110
|
-
const [running, setRunning] = useState<string | null>(null);
|
|
111
|
-
const [results, setResults] = useState<Record<string, { ok: boolean; value: unknown }>>({});
|
|
112
|
-
const [resultMode, setResultMode] = useState<Record<string, 'pretty' | 'json'>>({});
|
|
113
|
-
|
|
114
|
-
if (schema === undefined) return null;
|
|
115
|
-
|
|
116
|
-
const actions = getActions(compType, schema);
|
|
117
|
-
if (actions.length === 0) return null;
|
|
118
|
-
|
|
119
|
-
async function run(a: string) {
|
|
120
|
-
setRunning(a);
|
|
121
|
-
try {
|
|
122
|
-
const actionSchema = getActionSchema(compType, a);
|
|
123
|
-
let data: unknown = {};
|
|
124
|
-
if (actionSchema) {
|
|
125
|
-
data = schemaData[a] ?? {};
|
|
126
|
-
} else {
|
|
127
|
-
const raw = (paramsText[a] ?? '').trim();
|
|
128
|
-
if (raw && raw !== '{}') {
|
|
129
|
-
try { data = JSON.parse(raw); }
|
|
130
|
-
catch { toast('Invalid JSON params'); setRunning(null); return; }
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
const result = await trpc.execute.mutate({ path, key: componentName, action: a, data });
|
|
134
|
-
const fresh = (await trpc.get.query({ path, watch: true })) as NodeData | undefined;
|
|
135
|
-
if (fresh) cache.put(fresh);
|
|
136
|
-
onActionComplete?.();
|
|
137
|
-
setResults((prev) => ({ ...prev, [a]: { ok: true, value: result } }));
|
|
138
|
-
setExpanded(a);
|
|
139
|
-
} catch (e) {
|
|
140
|
-
setResults((prev) => ({
|
|
141
|
-
...prev,
|
|
142
|
-
[a]: { ok: false, value: e instanceof Error ? e.message : String(e) },
|
|
143
|
-
}));
|
|
144
|
-
setExpanded(a);
|
|
145
|
-
} finally {
|
|
146
|
-
setRunning(null);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return (
|
|
151
|
-
<div className="action-pills">
|
|
152
|
-
<div className="flex flex-wrap gap-1.5">
|
|
153
|
-
{actions.map((a) => (
|
|
154
|
-
<button
|
|
155
|
-
key={a}
|
|
156
|
-
className={`action-pill${expanded === a ? ' active' : ''}${running === a ? ' running' : ''}`}
|
|
157
|
-
onClick={() => setExpanded(expanded === a ? null : a)}
|
|
158
|
-
>
|
|
159
|
-
{running === a ? '...' : a}
|
|
160
|
-
{results[a] && !results[a].ok && expanded !== a && (
|
|
161
|
-
<span className="ml-1 text-destructive">!</span>
|
|
162
|
-
)}
|
|
163
|
-
{results[a]?.ok && expanded !== a && (
|
|
164
|
-
<span className="ml-1 text-primary/60">✓</span>
|
|
165
|
-
)}
|
|
166
|
-
</button>
|
|
167
|
-
))}
|
|
168
|
-
</div>
|
|
169
|
-
|
|
170
|
-
{expanded && (() => {
|
|
171
|
-
const a = expanded;
|
|
172
|
-
const actionSchema = getActionSchema(compType, a);
|
|
173
|
-
const hasParams = actionSchema !== null && Object.keys(actionSchema.properties).length > 0;
|
|
174
|
-
const noParams = actionSchema !== null && Object.keys(actionSchema.properties).length === 0;
|
|
175
|
-
const result = results[a];
|
|
176
|
-
const mode = resultMode[a] ?? 'pretty';
|
|
177
|
-
|
|
178
|
-
return (
|
|
179
|
-
<div className="action-detail">
|
|
180
|
-
{/* Params section */}
|
|
181
|
-
{hasParams && (
|
|
182
|
-
<div className="flex flex-col gap-1.5 mb-2">
|
|
183
|
-
{Object.entries(actionSchema!.properties).map(([field, prop]) => {
|
|
184
|
-
const p = prop as { type: string; title?: string; format?: string };
|
|
185
|
-
const val = (schemaData[a] ?? {})[field];
|
|
186
|
-
const setField = (v: unknown) =>
|
|
187
|
-
setSchemaData((prev) => ({
|
|
188
|
-
...prev,
|
|
189
|
-
[a]: { ...(prev[a] ?? {}), [field]: v },
|
|
190
|
-
}));
|
|
191
|
-
return (
|
|
192
|
-
<div key={field} className="action-detail-field">
|
|
193
|
-
<label>{p.title ?? field}</label>
|
|
194
|
-
{p.type === 'number' || p.format === 'number' ? (
|
|
195
|
-
<input type="number" value={String(val ?? 0)}
|
|
196
|
-
onChange={(e) => setField(Number(e.target.value))} />
|
|
197
|
-
) : p.type === 'boolean' ? (
|
|
198
|
-
<label className="flex items-center gap-1.5 cursor-pointer">
|
|
199
|
-
<input type="checkbox" checked={!!val} className="w-auto"
|
|
200
|
-
onChange={(e) => setField(e.target.checked)} />
|
|
201
|
-
<span className="text-[11px]">{val ? 'true' : 'false'}</span>
|
|
202
|
-
</label>
|
|
203
|
-
) : (
|
|
204
|
-
<input value={String(val ?? '')}
|
|
205
|
-
onChange={(e) => setField(e.target.value)} />
|
|
206
|
-
)}
|
|
207
|
-
</div>
|
|
208
|
-
);
|
|
209
|
-
})}
|
|
210
|
-
</div>
|
|
211
|
-
)}
|
|
212
|
-
|
|
213
|
-
{/* Free-form JSON params for untyped actions */}
|
|
214
|
-
{!hasParams && !noParams && (
|
|
215
|
-
<textarea
|
|
216
|
-
className="action-params-input mb-2"
|
|
217
|
-
value={paramsText[a] ?? '{}'}
|
|
218
|
-
onChange={(e) => setParamsText((prev) => ({ ...prev, [a]: e.target.value }))}
|
|
219
|
-
spellCheck={false}
|
|
220
|
-
rows={2}
|
|
221
|
-
/>
|
|
222
|
-
)}
|
|
223
|
-
|
|
224
|
-
{/* Run button */}
|
|
225
|
-
<button
|
|
226
|
-
className="action-run-btn"
|
|
227
|
-
disabled={running !== null}
|
|
228
|
-
onClick={() => run(a)}
|
|
229
|
-
>
|
|
230
|
-
{running === a ? '...' : '▶'} {a}
|
|
231
|
-
</button>
|
|
232
|
-
|
|
233
|
-
{/* Result */}
|
|
234
|
-
{result && (
|
|
235
|
-
<div className={`action-result-box${result.ok ? '' : ' error'}`}>
|
|
236
|
-
{!result.ok ? (
|
|
237
|
-
<span className="text-destructive font-mono text-[11px]">{String(result.value)}</span>
|
|
238
|
-
) : result.value === undefined || result.value === null ? (
|
|
239
|
-
<span className="text-primary text-[11px]">✓ done</span>
|
|
240
|
-
) : (
|
|
241
|
-
<>
|
|
242
|
-
<div className="flex items-center justify-between mb-1">
|
|
243
|
-
<span className="text-[10px] text-muted-foreground uppercase tracking-wider">Result</span>
|
|
244
|
-
{typeof result.value === 'object' && (
|
|
245
|
-
<div className="flex gap-0.5">
|
|
246
|
-
<button
|
|
247
|
-
className={`action-mode-btn${mode === 'pretty' ? ' active' : ''}`}
|
|
248
|
-
onClick={() => setResultMode((p) => ({ ...p, [a]: 'pretty' }))}
|
|
249
|
-
>View</button>
|
|
250
|
-
<button
|
|
251
|
-
className={`action-mode-btn${mode === 'json' ? ' active' : ''}`}
|
|
252
|
-
onClick={() => setResultMode((p) => ({ ...p, [a]: 'json' }))}
|
|
253
|
-
>JSON</button>
|
|
254
|
-
</div>
|
|
255
|
-
)}
|
|
256
|
-
</div>
|
|
257
|
-
{mode === 'json' ? (
|
|
258
|
-
<pre className="text-[11px] font-mono text-foreground/60 whitespace-pre-wrap break-all leading-relaxed">
|
|
259
|
-
{JSON.stringify(result.value, null, 2)}
|
|
260
|
-
</pre>
|
|
261
|
-
) : (
|
|
262
|
-
<ResultView value={result.value} />
|
|
263
|
-
)}
|
|
264
|
-
</>
|
|
265
|
-
)}
|
|
266
|
-
</div>
|
|
267
|
-
)}
|
|
268
|
-
</div>
|
|
269
|
-
);
|
|
270
|
-
})()}
|
|
271
|
-
</div>
|
|
272
|
-
);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function renderField(
|
|
276
|
-
name: string,
|
|
277
|
-
fieldSchema: {
|
|
278
|
-
type: string;
|
|
279
|
-
label: string;
|
|
280
|
-
placeholder?: string;
|
|
281
|
-
readOnly?: boolean;
|
|
282
|
-
enum?: string[];
|
|
283
|
-
items?: { type?: string; properties?: Record<string, unknown> };
|
|
284
|
-
refType?: string;
|
|
285
|
-
},
|
|
286
|
-
plainData: Record<string, unknown>,
|
|
287
|
-
setPlainData: (fn: (prev: Record<string, unknown>) => Record<string, unknown>) => void,
|
|
288
|
-
) {
|
|
289
|
-
const ctx = fieldSchema.readOnly ? 'react' : 'react:form';
|
|
290
|
-
const handler = resolveExact(fieldSchema.type, ctx) ?? resolveExact('string', ctx);
|
|
291
|
-
if (!handler)
|
|
292
|
-
return (
|
|
293
|
-
<div key={name} className="text-[--danger] text-xs">
|
|
294
|
-
No form handler: {fieldSchema.type}
|
|
295
|
-
</div>
|
|
296
|
-
);
|
|
297
|
-
const fieldData: { $type: string; [k: string]: unknown } = {
|
|
298
|
-
$type: fieldSchema.type,
|
|
299
|
-
value: plainData[name],
|
|
300
|
-
label: fieldSchema.label,
|
|
301
|
-
placeholder: fieldSchema.placeholder,
|
|
302
|
-
};
|
|
303
|
-
if (fieldSchema.items) fieldData.items = fieldSchema.items;
|
|
304
|
-
if (fieldSchema.enum) fieldData.enum = fieldSchema.enum;
|
|
305
|
-
if (fieldSchema.refType) fieldData.refType = fieldSchema.refType;
|
|
306
|
-
const isComplex = fieldSchema.type === 'object' || fieldSchema.type === 'array';
|
|
307
|
-
return (
|
|
308
|
-
<div key={name} className={isComplex ? 'field stack' : 'field'}>
|
|
309
|
-
{fieldSchema.type !== 'boolean' && <label>{fieldSchema.label}</label>}
|
|
310
|
-
{createElement(handler as any, {
|
|
311
|
-
value: fieldData,
|
|
312
|
-
onChange: fieldSchema.readOnly
|
|
313
|
-
? undefined
|
|
314
|
-
: (next: { value: unknown }) => setPlainData((prev) => ({ ...prev, [name]: next.value })),
|
|
315
|
-
})}
|
|
316
|
-
</div>
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Inline string-array editor for raw component fields without schema
|
|
321
|
-
function StringArrayField({
|
|
322
|
-
value,
|
|
323
|
-
onChange,
|
|
324
|
-
}: {
|
|
325
|
-
value: unknown[];
|
|
326
|
-
onChange: (next: unknown[]) => void;
|
|
327
|
-
}) {
|
|
328
|
-
const [input, setInput] = useState('');
|
|
329
|
-
const isStrings = value.every((v) => typeof v === 'string');
|
|
330
|
-
|
|
331
|
-
if (!isStrings) {
|
|
332
|
-
return (
|
|
333
|
-
<textarea
|
|
334
|
-
value={JSON.stringify(value, null, 2)}
|
|
335
|
-
onChange={(e) => {
|
|
336
|
-
try {
|
|
337
|
-
onChange(JSON.parse(e.target.value));
|
|
338
|
-
} catch {
|
|
339
|
-
/* typing */
|
|
340
|
-
}
|
|
341
|
-
}}
|
|
342
|
-
/>
|
|
343
|
-
);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
const tags = value as string[];
|
|
347
|
-
return (
|
|
348
|
-
<div className="flex-1 space-y-1">
|
|
349
|
-
<div className="flex flex-wrap gap-1">
|
|
350
|
-
{tags.map((tag, i) => (
|
|
351
|
-
<span
|
|
352
|
-
key={i}
|
|
353
|
-
className="inline-flex items-center gap-0.5 text-[11px] font-mono bg-muted text-foreground/70 px-1.5 py-0.5 rounded"
|
|
354
|
-
>
|
|
355
|
-
{tag}
|
|
356
|
-
<button
|
|
357
|
-
type="button"
|
|
358
|
-
className="ml-0.5 border-0 bg-transparent p-0 text-muted-foreground/40 hover:text-foreground leading-none cursor-pointer"
|
|
359
|
-
onClick={() => onChange(tags.filter((_, j) => j !== i))}
|
|
360
|
-
>
|
|
361
|
-
×
|
|
362
|
-
</button>
|
|
363
|
-
</span>
|
|
364
|
-
))}
|
|
365
|
-
</div>
|
|
366
|
-
<input
|
|
367
|
-
className="text-xs w-full"
|
|
368
|
-
placeholder="Add item..."
|
|
369
|
-
value={input}
|
|
370
|
-
onChange={(e) => setInput(e.target.value)}
|
|
371
|
-
onKeyDown={(e) => {
|
|
372
|
-
if (e.key !== 'Enter') return;
|
|
373
|
-
e.preventDefault();
|
|
374
|
-
const t = input.trim();
|
|
375
|
-
if (t && !tags.includes(t)) onChange([...tags, t]);
|
|
376
|
-
setInput('');
|
|
377
|
-
}}
|
|
378
|
-
/>
|
|
379
|
-
</div>
|
|
380
|
-
);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
function NodeCard({
|
|
384
|
-
path,
|
|
385
|
-
type,
|
|
386
|
-
onChangeType,
|
|
387
|
-
}: {
|
|
388
|
-
path: string;
|
|
389
|
-
type: string;
|
|
390
|
-
onChangeType: (t: string) => void;
|
|
391
|
-
}) {
|
|
392
|
-
const [open, setOpen] = useState(false);
|
|
393
|
-
return (
|
|
394
|
-
<div className="card">
|
|
395
|
-
<div
|
|
396
|
-
className="card-header cursor-pointer select-none"
|
|
397
|
-
onClick={() => setOpen((v) => !v)}
|
|
398
|
-
>
|
|
399
|
-
<span>Node</span>
|
|
400
|
-
<span className="flex items-center gap-2 normal-case tracking-normal font-normal text-[11px] font-mono text-foreground/50">
|
|
401
|
-
{path}
|
|
402
|
-
<span className="text-primary">{type}</span>
|
|
403
|
-
{open ? (
|
|
404
|
-
<svg className="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="m6 9 6 6 6-6"/></svg>
|
|
405
|
-
) : (
|
|
406
|
-
<svg className="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="m9 18 6-6-6-6"/></svg>
|
|
407
|
-
)}
|
|
408
|
-
</span>
|
|
409
|
-
</div>
|
|
410
|
-
{open && (
|
|
411
|
-
<div className="card-body">
|
|
412
|
-
<div className="field">
|
|
413
|
-
<label>$path</label>
|
|
414
|
-
<input value={path} readOnly />
|
|
415
|
-
</div>
|
|
416
|
-
<div className="field">
|
|
417
|
-
<label>$type</label>
|
|
418
|
-
<input value={type} onChange={(e) => onChangeType(e.target.value)} />
|
|
419
|
-
</div>
|
|
420
|
-
</div>
|
|
421
|
-
)}
|
|
422
|
-
</div>
|
|
423
|
-
);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function ComponentBody({
|
|
427
|
-
ctype, cdata, setCD, path, componentName, toast, onActionComplete,
|
|
428
|
-
}: {
|
|
429
|
-
ctype: string;
|
|
430
|
-
cdata: Record<string, unknown>;
|
|
431
|
-
setCD: (fn: (prev: Record<string, unknown>) => Record<string, unknown>) => void;
|
|
432
|
-
path: string;
|
|
433
|
-
componentName: string;
|
|
434
|
-
toast: (msg: string) => void;
|
|
435
|
-
onActionComplete?: () => void;
|
|
436
|
-
}) {
|
|
437
|
-
const cschema = useSchema(ctype);
|
|
438
|
-
if (cschema === undefined) return null;
|
|
439
|
-
|
|
440
|
-
return (
|
|
441
|
-
<div className="card-body">
|
|
442
|
-
{cschema && Object.keys(cschema.properties).length > 0 ? (
|
|
443
|
-
Object.entries(cschema.properties).map(([field, prop]) => {
|
|
444
|
-
const p = prop as {
|
|
445
|
-
type: string; title: string; format?: string; description?: string;
|
|
446
|
-
readOnly?: boolean; enum?: string[]; items?: { type?: string; properties?: Record<string, unknown> };
|
|
447
|
-
refType?: string;
|
|
448
|
-
};
|
|
449
|
-
return renderField(field, {
|
|
450
|
-
type: p.format ?? p.type, label: p.title ?? field, placeholder: p.description,
|
|
451
|
-
readOnly: p.readOnly, enum: p.enum, items: p.items, refType: p.refType,
|
|
452
|
-
}, cdata, setCD);
|
|
453
|
-
})
|
|
454
|
-
) : Object.keys(cdata).length > 0 ? (
|
|
455
|
-
Object.entries(cdata).map(([k, v]) => (
|
|
456
|
-
<div key={k} className={`field${Array.isArray(v) || (typeof v === 'object' && v !== null) ? ' stack' : ''}`}>
|
|
457
|
-
<label>{k}</label>
|
|
458
|
-
{typeof v === 'boolean' ? (
|
|
459
|
-
<label className="flex items-center gap-2 cursor-pointer">
|
|
460
|
-
<input type="checkbox" checked={!!cdata[k]} className="w-auto"
|
|
461
|
-
onChange={(e) => setCD((prev) => ({ ...prev, [k]: e.target.checked }))} />
|
|
462
|
-
{cdata[k] ? 'true' : 'false'}
|
|
463
|
-
</label>
|
|
464
|
-
) : typeof v === 'number' ? (
|
|
465
|
-
<input type="number" value={String(cdata[k] ?? 0)}
|
|
466
|
-
onChange={(e) => setCD((prev) => ({ ...prev, [k]: Number(e.target.value) }))} />
|
|
467
|
-
) : Array.isArray(v) ? (
|
|
468
|
-
<StringArrayField value={cdata[k] as unknown[]}
|
|
469
|
-
onChange={(next) => setCD((prev) => ({ ...prev, [k]: next }))} />
|
|
470
|
-
) : typeof v === 'object' ? (
|
|
471
|
-
(() => {
|
|
472
|
-
const h = resolve('object', 'react:form');
|
|
473
|
-
return h
|
|
474
|
-
? createElement(h as any, {
|
|
475
|
-
value: { $type: 'object', value: cdata[k] },
|
|
476
|
-
onChange: (next: { value: unknown }) => setCD((prev) => ({ ...prev, [k]: next.value })),
|
|
477
|
-
})
|
|
478
|
-
: <pre className="text-[11px] font-mono text-foreground/60">{JSON.stringify(cdata[k], null, 2)}</pre>;
|
|
479
|
-
})()
|
|
480
|
-
) : (
|
|
481
|
-
<input value={String(cdata[k] ?? '')}
|
|
482
|
-
onChange={(e) => setCD((prev) => ({ ...prev, [k]: e.target.value }))} />
|
|
483
|
-
)}
|
|
484
|
-
</div>
|
|
485
|
-
))
|
|
486
|
-
) : (
|
|
487
|
-
<pre className="text-[11px] font-mono text-foreground/60 bg-muted/30 rounded p-2 whitespace-pre-wrap">
|
|
488
|
-
{JSON.stringify(cdata, null, 2)}
|
|
489
|
-
</pre>
|
|
490
|
-
)}
|
|
491
|
-
<ActionCardList path={path} componentName={componentName} compType={ctype} compData={cdata} toast={toast} onActionComplete={onActionComplete} />
|
|
492
|
-
</div>
|
|
493
|
-
);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
27
|
export function Inspector({ path, currentUserId, onDelete, onAddComponent, onSelect, onSetRoot, toast }: Props) {
|
|
497
28
|
const node = usePath(path);
|
|
498
|
-
|
|
499
|
-
const [context, setContext] = useState('react');
|
|
29
|
+
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
500
30
|
const [editing, setEditing] = useState(false);
|
|
501
|
-
const [
|
|
502
|
-
const [compTexts, setCompTexts] = useState<Record<string, string>>({});
|
|
503
|
-
const [compData, setCompData] = useState<Record<string, Record<string, unknown>>>({});
|
|
504
|
-
const [plainData, setPlainData] = useState<Record<string, unknown>>({});
|
|
505
|
-
const [tab, setTab] = useState<'properties' | 'json'>('properties');
|
|
506
|
-
const [jsonText, setJsonText] = useState('');
|
|
507
|
-
const [collapsed, setCollapsed] = useState<Set<string>>(new Set());
|
|
508
|
-
const [aclOwner, setAclOwner] = useState('');
|
|
509
|
-
const [aclRules, setAclRules] = useState<GroupPerm[]>([]);
|
|
510
|
-
const [dirty, setDirty] = useState(false);
|
|
511
|
-
const [stale, setStale] = useState(false);
|
|
512
|
-
const syncedPathRef = useRef<string | null>(null);
|
|
513
|
-
const syncedRevRef = useRef<unknown>(null);
|
|
514
|
-
|
|
515
|
-
function syncFromNode(n: NodeData) {
|
|
516
|
-
setNodeType(n.$type);
|
|
517
|
-
setAclOwner((n.$owner as string) ?? '');
|
|
518
|
-
setAclRules(n.$acl ? [...(n.$acl as GroupPerm[])] : []);
|
|
519
|
-
const texts: Record<string, string> = {};
|
|
520
|
-
const cdata: Record<string, Record<string, unknown>> = {};
|
|
521
|
-
for (const [name, comp] of getComponents(n)) {
|
|
522
|
-
texts[name] = JSON.stringify(comp, null, 2);
|
|
523
|
-
const d: Record<string, unknown> = {};
|
|
524
|
-
for (const [k, v] of Object.entries(comp)) {
|
|
525
|
-
if (!k.startsWith('$')) d[k] = v;
|
|
526
|
-
}
|
|
527
|
-
cdata[name] = d;
|
|
528
|
-
}
|
|
529
|
-
setCompTexts(texts);
|
|
530
|
-
setCompData(cdata);
|
|
531
|
-
setPlainData(getPlainFields(n));
|
|
532
|
-
setJsonText(JSON.stringify(n, null, 2));
|
|
533
|
-
setTab('properties');
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
useEffect(() => {
|
|
537
|
-
if (!node) return;
|
|
538
|
-
|
|
539
|
-
const pathChanged = node.$path !== syncedPathRef.current;
|
|
540
|
-
if (pathChanged) {
|
|
541
|
-
setContext(pickDefaultContext(node.$type));
|
|
542
|
-
syncFromNode(node);
|
|
543
|
-
syncedPathRef.current = node.$path;
|
|
544
|
-
syncedRevRef.current = node.$rev;
|
|
545
|
-
setDirty(false);
|
|
546
|
-
setStale(false);
|
|
547
|
-
return;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
if (node.$rev !== syncedRevRef.current) {
|
|
551
|
-
if (dirty) {
|
|
552
|
-
setStale(true);
|
|
553
|
-
} else {
|
|
554
|
-
syncFromNode(node);
|
|
555
|
-
syncedRevRef.current = node.$rev;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
}, [node?.$path, node?.$rev]);
|
|
559
|
-
|
|
560
|
-
// Dirty-tracking wrappers — mark form as edited on any user change
|
|
561
|
-
const dSetNodeType: typeof setNodeType = (v) => { setNodeType(v); setDirty(true); };
|
|
562
|
-
const dSetCompData: typeof setCompData = (v) => { setCompData(v); setDirty(true); };
|
|
563
|
-
const dSetPlainData: typeof setPlainData = (v) => { setPlainData(v); setDirty(true); };
|
|
564
|
-
const dSetJsonText: typeof setJsonText = (v) => { setJsonText(v); setDirty(true); };
|
|
565
|
-
const dSetAclOwner: typeof setAclOwner = (v) => { setAclOwner(v); setDirty(true); };
|
|
566
|
-
const dSetAclRules: typeof setAclRules = (v) => { setAclRules(v); setDirty(true); };
|
|
31
|
+
const [context, setContext] = useState('react');
|
|
567
32
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
setDirty(false);
|
|
574
|
-
setStale(false);
|
|
33
|
+
// Reset context when path changes
|
|
34
|
+
const [prevPath, setPrevPath] = useState(path);
|
|
35
|
+
if (path !== prevPath) {
|
|
36
|
+
setPrevPath(path);
|
|
37
|
+
if (node) setContext(pickDefaultContext(node.$type));
|
|
575
38
|
}
|
|
576
39
|
|
|
577
40
|
if (!node) {
|
|
578
41
|
return (
|
|
579
|
-
<div className="
|
|
580
|
-
<div className="
|
|
581
|
-
<div className="
|
|
42
|
+
<div className="flex flex-1 flex-col overflow-hidden bg-background">
|
|
43
|
+
<div className="flex flex-1 flex-col items-center justify-center gap-2 text-muted-foreground/40">
|
|
44
|
+
<div className="text-[32px] opacity-30">☍</div>
|
|
582
45
|
<p>Select a node to inspect</p>
|
|
583
46
|
</div>
|
|
584
47
|
</div>
|
|
@@ -586,272 +49,89 @@ export function Inspector({ path, currentUserId, onDelete, onAddComponent, onSel
|
|
|
586
49
|
}
|
|
587
50
|
|
|
588
51
|
const nodeName = node.$path === '/' ? '/' : node.$path.slice(node.$path.lastIndexOf('/') + 1);
|
|
589
|
-
const components = getComponents(node);
|
|
590
52
|
const viewContexts = getViewContexts(node.$type, node);
|
|
591
|
-
const schemaHandler = resolve(node.$type, 'schema');
|
|
592
|
-
const schema = schemaHandler ? (schemaHandler() as TypeSchema) : null;
|
|
593
|
-
|
|
594
|
-
// Main component: when the node IS the component (its $type has a registered class).
|
|
595
|
-
// Show the class's fields (with defaults as fallback for unset fields).
|
|
596
|
-
const mainCompCls = resolve(node.$type, 'class') as AnyClass | null;
|
|
597
|
-
const mainCompDefaults = mainCompCls ? new mainCompCls() : null;
|
|
598
|
-
|
|
599
|
-
async function handleSave() {
|
|
600
|
-
if (!node) return;
|
|
601
|
-
let toSave: NodeData;
|
|
602
|
-
if (tab === 'json') {
|
|
603
|
-
try {
|
|
604
|
-
toSave = JSON.parse(jsonText);
|
|
605
|
-
} catch {
|
|
606
|
-
toast('Invalid JSON');
|
|
607
|
-
return;
|
|
608
|
-
}
|
|
609
|
-
} else {
|
|
610
|
-
toSave = { $path: node.$path, $type: nodeType, ...plainData };
|
|
611
|
-
if (aclOwner) toSave.$owner = aclOwner;
|
|
612
|
-
if (aclRules.length > 0) toSave.$acl = aclRules;
|
|
613
|
-
for (const [name, comp] of components) {
|
|
614
|
-
const ctype = (comp as ComponentData).$type;
|
|
615
|
-
const cschema = getSchema(ctype);
|
|
616
|
-
const cd = compData[name];
|
|
617
|
-
if ((cschema || (cd && Object.keys(cd).length > 0)) && cd) {
|
|
618
|
-
toSave[name] = { $type: ctype, ...cd };
|
|
619
|
-
} else {
|
|
620
|
-
const text = compTexts[name];
|
|
621
|
-
if (text === undefined) continue;
|
|
622
|
-
try {
|
|
623
|
-
toSave[name] = JSON.parse(text);
|
|
624
|
-
} catch {
|
|
625
|
-
toast(`Invalid JSON in component: ${name}`);
|
|
626
|
-
return;
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
await set(toSave);
|
|
632
|
-
const fresh = cache.get(node.$path);
|
|
633
|
-
if (fresh) {
|
|
634
|
-
syncFromNode(fresh);
|
|
635
|
-
syncedRevRef.current = fresh.$rev;
|
|
636
|
-
}
|
|
637
|
-
setDirty(false);
|
|
638
|
-
setStale(false);
|
|
639
|
-
toast('Saved');
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
function handleAdd() {
|
|
643
|
-
if (!node) return;
|
|
644
|
-
onAddComponent(node.$path);
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
function handleRemoveComponent(name: string) {
|
|
648
|
-
if (!node) return;
|
|
649
|
-
const next = { ...node };
|
|
650
|
-
delete next[name];
|
|
651
|
-
set(next);
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
function toggleCollapse(name: string) {
|
|
655
|
-
setCollapsed((prev) => {
|
|
656
|
-
const next = new Set(prev);
|
|
657
|
-
if (next.has(name)) next.delete(name);
|
|
658
|
-
else next.add(name);
|
|
659
|
-
return next;
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
53
|
|
|
663
54
|
return (
|
|
664
55
|
<div className="editor">
|
|
665
56
|
{/* Header */}
|
|
666
|
-
<div className="
|
|
667
|
-
<
|
|
668
|
-
<div className="
|
|
57
|
+
<div className="px-6 pt-4 pb-3 border-b border-border bg-card shrink-0">
|
|
58
|
+
<PathBreadcrumb path={node.$path} onSelect={onSelect} />
|
|
59
|
+
<div className="flex items-center gap-2.5 flex-wrap">
|
|
669
60
|
<h2>{nodeName}</h2>
|
|
670
|
-
<
|
|
61
|
+
<Badge variant="outline" className="font-mono text-[10px]">{node.$type}</Badge>
|
|
671
62
|
<a
|
|
672
63
|
href={node.$path}
|
|
673
64
|
target="_blank"
|
|
674
65
|
rel="noopener"
|
|
675
|
-
className="text-[11px] text-
|
|
66
|
+
className="text-[11px] text-muted-foreground hover:text-primary no-underline"
|
|
676
67
|
>
|
|
677
68
|
View ↗
|
|
678
69
|
</a>
|
|
679
70
|
{onSetRoot && (
|
|
680
|
-
<
|
|
681
|
-
className="sm ghost text-[11px]"
|
|
682
|
-
onClick={() => onSetRoot(node.$path)}
|
|
683
|
-
title="Focus subtree"
|
|
684
|
-
>
|
|
71
|
+
<Button variant="ghost" size="sm" className="h-6 px-1.5 text-[11px]" onClick={() => onSetRoot(node.$path)} title="Focus subtree">
|
|
685
72
|
⌂
|
|
686
|
-
</
|
|
73
|
+
</Button>
|
|
687
74
|
)}
|
|
688
75
|
{viewContexts.length > 1 && (
|
|
689
|
-
<span className="
|
|
76
|
+
<span className="flex gap-0.5">
|
|
690
77
|
{viewContexts.map((c) => (
|
|
691
|
-
<
|
|
78
|
+
<Button
|
|
692
79
|
key={c}
|
|
693
|
-
|
|
80
|
+
variant={context === c ? 'default' : 'ghost'}
|
|
81
|
+
size="sm"
|
|
82
|
+
className="h-6 px-2 text-[11px]"
|
|
694
83
|
onClick={() => setContext(c)}
|
|
695
84
|
>
|
|
696
|
-
{c}
|
|
697
|
-
</
|
|
85
|
+
{c.replace('react:', '')}
|
|
86
|
+
</Button>
|
|
698
87
|
))}
|
|
699
88
|
</span>
|
|
700
89
|
)}
|
|
701
|
-
<span className="
|
|
702
|
-
<
|
|
90
|
+
<span className="flex-1" />
|
|
91
|
+
<Button variant={editing ? 'ghost' : 'default'} size="sm" className="h-7" onClick={() => setEditing(!editing)}>
|
|
703
92
|
{editing ? 'Close' : 'Edit'}
|
|
704
|
-
</
|
|
705
|
-
<
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
}
|
|
93
|
+
</Button>
|
|
94
|
+
<Button
|
|
95
|
+
variant="destructive"
|
|
96
|
+
size="sm"
|
|
97
|
+
className="h-7"
|
|
98
|
+
onClick={() => setConfirmDelete(true)}
|
|
710
99
|
>
|
|
711
100
|
Delete
|
|
712
|
-
</
|
|
101
|
+
</Button>
|
|
102
|
+
<ConfirmDialog
|
|
103
|
+
open={confirmDelete}
|
|
104
|
+
onOpenChange={setConfirmDelete}
|
|
105
|
+
title={`Delete ${node.$path}?`}
|
|
106
|
+
description="This action cannot be undone."
|
|
107
|
+
variant="destructive"
|
|
108
|
+
onConfirm={() => onDelete(node.$path)}
|
|
109
|
+
/>
|
|
713
110
|
</div>
|
|
714
111
|
</div>
|
|
715
112
|
|
|
716
113
|
{/* Rendered view */}
|
|
717
|
-
<
|
|
718
|
-
<
|
|
719
|
-
<
|
|
720
|
-
<
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
<div className={`edit-panel${editing ? ' open' : ''}`}>
|
|
727
|
-
<div className="edit-panel-header">
|
|
728
|
-
<span>Edit {nodeName}</span>
|
|
729
|
-
<button className="sm ghost" onClick={() => setEditing(false)}>
|
|
730
|
-
✕
|
|
731
|
-
</button>
|
|
732
|
-
</div>
|
|
733
|
-
|
|
734
|
-
<div className="edit-panel-tabs">
|
|
735
|
-
<button
|
|
736
|
-
className={`editor-tab${tab === 'properties' ? ' active' : ''}`}
|
|
737
|
-
onClick={() => setTab('properties')}
|
|
738
|
-
>
|
|
739
|
-
Properties
|
|
740
|
-
</button>
|
|
741
|
-
<button
|
|
742
|
-
className={`editor-tab${tab === 'json' ? ' active' : ''}`}
|
|
743
|
-
onClick={() => {
|
|
744
|
-
setTab('json');
|
|
745
|
-
setJsonText(JSON.stringify({ ...node, ...plainData }, null, 2));
|
|
746
|
-
}}
|
|
747
|
-
>
|
|
748
|
-
JSON
|
|
749
|
-
</button>
|
|
750
|
-
</div>
|
|
751
|
-
|
|
752
|
-
<div className="edit-panel-body">
|
|
753
|
-
{tab === 'properties' ? (
|
|
754
|
-
<>
|
|
755
|
-
<NodeCard path={node.$path} type={nodeType} onChangeType={dSetNodeType} />
|
|
756
|
-
|
|
757
|
-
<AclEditor
|
|
758
|
-
path={node.$path}
|
|
759
|
-
owner={aclOwner}
|
|
760
|
-
rules={aclRules}
|
|
761
|
-
currentUserId={currentUserId}
|
|
762
|
-
onChange={(o, r) => {
|
|
763
|
-
dSetAclOwner(o);
|
|
764
|
-
dSetAclRules(r);
|
|
765
|
-
}}
|
|
766
|
-
/>
|
|
767
|
-
|
|
768
|
-
{(mainCompCls || schema) && (
|
|
769
|
-
<div className="card">
|
|
770
|
-
<div className="card-header">{node.$type}</div>
|
|
771
|
-
<ComponentBody
|
|
772
|
-
ctype={node.$type}
|
|
773
|
-
cdata={plainData}
|
|
774
|
-
setCD={(fn) => dSetPlainData(fn)}
|
|
775
|
-
path={node.$path}
|
|
776
|
-
componentName=""
|
|
777
|
-
toast={toast}
|
|
778
|
-
onActionComplete={handleReset}
|
|
779
|
-
/>
|
|
780
|
-
</div>
|
|
781
|
-
)}
|
|
782
|
-
|
|
783
|
-
{components.map(([name, comp]) => (
|
|
784
|
-
<div key={name} className="card">
|
|
785
|
-
<div className="card-header cursor-pointer select-none" onClick={() => toggleCollapse(name)}>
|
|
786
|
-
<span className="font-mono text-[12px]">{name}</span>
|
|
787
|
-
<span className="flex items-center gap-2">
|
|
788
|
-
<span className="component-type">{(comp as ComponentData).$type}</span>
|
|
789
|
-
<button
|
|
790
|
-
className="sm danger"
|
|
791
|
-
onClick={(e) => { e.stopPropagation(); handleRemoveComponent(name); }}
|
|
792
|
-
>
|
|
793
|
-
Remove
|
|
794
|
-
</button>
|
|
795
|
-
</span>
|
|
796
|
-
</div>
|
|
797
|
-
{!collapsed.has(name) && (
|
|
798
|
-
<ComponentBody
|
|
799
|
-
ctype={(comp as ComponentData).$type}
|
|
800
|
-
cdata={compData[name] ?? {}}
|
|
801
|
-
setCD={(fn) => dSetCompData((prev) => ({ ...prev, [name]: fn(prev[name] ?? {}) }))}
|
|
802
|
-
path={node.$path}
|
|
803
|
-
componentName={name}
|
|
804
|
-
toast={toast}
|
|
805
|
-
onActionComplete={handleReset}
|
|
806
|
-
/>
|
|
807
|
-
)}
|
|
808
|
-
</div>
|
|
809
|
-
))}
|
|
810
|
-
|
|
811
|
-
{!schema && !mainCompDefaults && Object.keys(plainData).length > 0 && (
|
|
812
|
-
<div className="card">
|
|
813
|
-
<div className="card-header">Data</div>
|
|
814
|
-
<div className="card-body">
|
|
815
|
-
{Object.entries(plainData).map(([k, v]) => (
|
|
816
|
-
<div key={k} className="field">
|
|
817
|
-
<label>{k}</label>
|
|
818
|
-
<input
|
|
819
|
-
value={typeof v === 'string' ? v : JSON.stringify(v)}
|
|
820
|
-
onChange={(e) =>
|
|
821
|
-
dSetPlainData((prev) => ({ ...prev, [k]: e.target.value }))
|
|
822
|
-
}
|
|
823
|
-
/>
|
|
824
|
-
</div>
|
|
825
|
-
))}
|
|
826
|
-
</div>
|
|
827
|
-
</div>
|
|
828
|
-
)}
|
|
829
|
-
</>
|
|
830
|
-
) : (
|
|
831
|
-
<div className="json-view">
|
|
832
|
-
<textarea
|
|
833
|
-
value={jsonText}
|
|
834
|
-
onChange={(e) => dSetJsonText(e.target.value)}
|
|
835
|
-
spellCheck={false}
|
|
836
|
-
/>
|
|
837
|
-
</div>
|
|
838
|
-
)}
|
|
114
|
+
<ScrollArea className="flex-1">
|
|
115
|
+
<div className="p-4">
|
|
116
|
+
<ErrorBoundary>
|
|
117
|
+
<RenderContext name={context}>
|
|
118
|
+
<div className="node-view">
|
|
119
|
+
<Render value={node} />
|
|
120
|
+
</div>
|
|
121
|
+
</RenderContext>
|
|
122
|
+
</ErrorBoundary>
|
|
839
123
|
</div>
|
|
124
|
+
</ScrollArea>
|
|
840
125
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
{tab === 'properties' && (
|
|
851
|
-
<button onClick={handleAdd}>+ Component</button>
|
|
852
|
-
)}
|
|
853
|
-
</div>
|
|
854
|
-
</div>
|
|
126
|
+
{/* Slide-out edit panel */}
|
|
127
|
+
<NodeEditor
|
|
128
|
+
node={node}
|
|
129
|
+
open={editing}
|
|
130
|
+
onClose={() => setEditing(false)}
|
|
131
|
+
currentUserId={currentUserId}
|
|
132
|
+
toast={toast}
|
|
133
|
+
onAddComponent={onAddComponent}
|
|
134
|
+
/>
|
|
855
135
|
</div>
|
|
856
136
|
);
|
|
857
137
|
}
|