@treenity/react 3.0.0 → 3.0.2
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 +97 -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 +18 -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 +9 -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 +8 -7
- package/dist/components/ui/button.d.ts.map +1 -1
- package/dist/components/ui/button.js +25 -20
- 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 +2 -2
- 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 +12 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +123 -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 +14 -2
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +146 -11
- 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/minimd.d.ts.map +1 -1
- package/dist/lib/minimd.js +8 -1
- package/dist/lib/minimd.js.map +1 -1
- package/dist/lib/sanitize-href.d.ts +3 -0
- package/dist/lib/sanitize-href.d.ts.map +1 -0
- package/dist/lib/sanitize-href.js +14 -0
- package/dist/lib/sanitize-href.js.map +1 -0
- 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 +11 -4
- 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 +56 -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 +56 -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 +69 -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 +70 -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.d.ts.map +1 -1
- package/dist/mods/treenity/preview.js +3 -4
- package/dist/mods/treenity/preview.js.map +1 -1
- package/dist/mods/treenity/ref-view.js +3 -2
- 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 +3 -2
- 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 +22 -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 +76 -8
- package/src/AclEditor.tsx +11 -18
- package/src/ActionCards.tsx +224 -0
- package/src/App.tsx +232 -385
- package/src/ComponentSection.tsx +113 -0
- package/src/ErrorBoundary.tsx +40 -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/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 +12 -1
- package/src/client-tree.ts +18 -12
- 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 +51 -30
- 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 +122 -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 +2 -2
- 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 +121 -0
- package/src/fiber-tree.ts +112 -0
- package/src/hooks.ts +161 -13
- package/src/idb.ts +1 -1
- package/src/lib/minimd.ts +7 -1
- package/src/lib/sanitize-href.ts +13 -0
- package/src/lib/to-plain.ts +21 -0
- package/src/main.tsx +14 -4
- package/src/mods/clients.ts +3 -0
- package/src/mods/editor-ui/FieldLabel.tsx +125 -0
- package/src/mods/editor-ui/client.ts +1 -1
- package/src/mods/editor-ui/default-edit.tsx +101 -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 +146 -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 +148 -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 +7 -8
- package/src/mods/treenity/ref-view.tsx +12 -7
- package/src/mods/treenity/schema-form.tsx +1 -1
- package/src/mods/treenity/seed.ts +3 -2
- package/src/mods/treenity/type-view.tsx +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 +25 -0
- package/src/bind/bind.test.ts +0 -316
- package/src/cache.test.ts +0 -139
- package/src/client-tree.test.ts +0 -116
- package/src/index.html +0 -14
- package/src/remote-tree.test.ts +0 -142
- package/src/style.css +0 -1269
- package/src/vite-env.d.ts +0 -3
package/src/hooks.ts
CHANGED
|
@@ -5,10 +5,19 @@
|
|
|
5
5
|
// execute: action caller
|
|
6
6
|
// watch: universal async generator
|
|
7
7
|
|
|
8
|
-
import { type
|
|
9
|
-
import {
|
|
8
|
+
import { getComponent, type NodeData, normalizeType, resolve } from '@treenity/core';
|
|
9
|
+
import { type Class, type TypeProxy } from '@treenity/core/comp';
|
|
10
10
|
import { deriveURI, parseURI } from '@treenity/core/uri';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
createContext,
|
|
13
|
+
useCallback,
|
|
14
|
+
useContext,
|
|
15
|
+
useEffect,
|
|
16
|
+
useMemo,
|
|
17
|
+
useRef,
|
|
18
|
+
useState,
|
|
19
|
+
useSyncExternalStore,
|
|
20
|
+
} from 'react';
|
|
12
21
|
import * as cache from './cache';
|
|
13
22
|
import { tree } from './client';
|
|
14
23
|
import { trpc } from './trpc';
|
|
@@ -50,7 +59,7 @@ export function usePath<T extends object>(
|
|
|
50
59
|
const path = isTyped ? pathOrUri : (parsed?.path ?? null);
|
|
51
60
|
|
|
52
61
|
const node = useSyncExternalStore(
|
|
53
|
-
useCallback((cb: () => void) => (path ? cache.subscribePath(path, cb) : () => {}), [path]),
|
|
62
|
+
useCallback((cb: () => void) => (path ? cache.subscribePath(path, cb) : () => { }), [path]),
|
|
54
63
|
useCallback(() => (path ? cache.get(path) : undefined), [path]),
|
|
55
64
|
);
|
|
56
65
|
|
|
@@ -90,7 +99,10 @@ export function useChildren(parentPath: string, opts?: WatchOpts) {
|
|
|
90
99
|
|
|
91
100
|
trpc.getChildren
|
|
92
101
|
.query({ path: parentPath, limit: opts?.limit, watch: opts?.watch, watchNew: opts?.watchNew })
|
|
93
|
-
.then((result: any) =>
|
|
102
|
+
.then((result: any) => {
|
|
103
|
+
if (result.truncated) console.warn(`[tree] Children of ${parentPath} truncated — results may be incomplete`);
|
|
104
|
+
cache.putMany(result.items as NodeData[], parentPath);
|
|
105
|
+
});
|
|
94
106
|
}, [parentPath, gen, opts?.limit, opts?.watch, opts?.watchNew]);
|
|
95
107
|
|
|
96
108
|
return useSyncExternalStore(
|
|
@@ -102,21 +114,153 @@ export function useChildren(parentPath: string, opts?: WatchOpts) {
|
|
|
102
114
|
// ── set: optimistic update + server persist ──
|
|
103
115
|
|
|
104
116
|
export async function set(next: NodeData) {
|
|
117
|
+
const prev = cache.get(next.$path);
|
|
105
118
|
cache.put(next);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
119
|
+
try {
|
|
120
|
+
await tree.set(next);
|
|
121
|
+
const fresh = await tree.get(next.$path);
|
|
122
|
+
if (fresh) cache.put(fresh);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
// F15: rollback optimistic cache on server reject (validation, ACL, OCC)
|
|
125
|
+
if (prev) cache.put(prev); else cache.remove(next.$path);
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
109
128
|
}
|
|
110
129
|
|
|
111
130
|
// ── execute: action caller ──
|
|
112
131
|
|
|
113
132
|
export const execute = (
|
|
114
|
-
|
|
115
|
-
) =>
|
|
133
|
+
pathOrUri: string, action: string, data?: unknown, type?: string, key?: string,
|
|
134
|
+
) => {
|
|
135
|
+
let path = pathOrUri;
|
|
136
|
+
if (!key && pathOrUri.includes('#')) {
|
|
137
|
+
const parsed = parseURI(pathOrUri);
|
|
138
|
+
path = parsed.path;
|
|
139
|
+
key = parsed.key;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Optimistic: resolve class from cache + registry, predict locally
|
|
143
|
+
const cached = cache.get(path);
|
|
144
|
+
if (cached) {
|
|
145
|
+
const compType = type ?? cached.$type;
|
|
146
|
+
const cls = resolve(compType, 'class');
|
|
147
|
+
if (cls) {
|
|
148
|
+
const fn = cls.prototype?.[action];
|
|
149
|
+
if (fn) predictOptimistic(path, cls, key, fn, data);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return trpc.execute.mutate({ path, type, key, action, data });
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// ── useCanWrite: ACL-based write permission check ──
|
|
157
|
+
|
|
158
|
+
const W = 2;
|
|
159
|
+
const permCache = new Map<string, { perm: number; ts: number }>();
|
|
160
|
+
const PERM_TTL = 30_000; // 30s cache
|
|
161
|
+
|
|
162
|
+
export function useCanWrite(path: string | null): boolean {
|
|
163
|
+
const [perm, setPerm] = useState<number>(0);
|
|
164
|
+
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
if (!path) return;
|
|
167
|
+
const cached = permCache.get(path);
|
|
168
|
+
if (cached && Date.now() - cached.ts < PERM_TTL) {
|
|
169
|
+
setPerm(cached.perm);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
trpc.getPerm.query({ path }).then((p) => {
|
|
173
|
+
permCache.set(path, { perm: p, ts: Date.now() });
|
|
174
|
+
setPerm(p);
|
|
175
|
+
}).catch(() => setPerm(0));
|
|
176
|
+
}, [path]);
|
|
177
|
+
|
|
178
|
+
return (perm & W) !== 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── useAutoSave: throttled auto-persist for editable views ──
|
|
182
|
+
// First save fires after 500ms (responsive), subsequent saves throttled to 2s.
|
|
183
|
+
// Returns [localData, setField, dirty] — local updates are instant, server writes are batched.
|
|
184
|
+
|
|
185
|
+
// TODO: check why unused
|
|
186
|
+
export function useAutoSave(node: NodeData) {
|
|
187
|
+
const [local, setLocal] = useState<Record<string, unknown>>({});
|
|
188
|
+
const dirtyRef = useRef(false);
|
|
189
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
190
|
+
const savingRef = useRef(false);
|
|
191
|
+
const pendingRef = useRef(false);
|
|
192
|
+
const lastSaveRef = useRef(0);
|
|
193
|
+
const nodeRef = useRef(node);
|
|
194
|
+
nodeRef.current = node;
|
|
195
|
+
|
|
196
|
+
const flush = useCallback(async () => {
|
|
197
|
+
if (savingRef.current) { pendingRef.current = true; return; }
|
|
198
|
+
if (!dirtyRef.current) return;
|
|
199
|
+
savingRef.current = true;
|
|
200
|
+
try {
|
|
201
|
+
const merged = { ...nodeRef.current, ...local };
|
|
202
|
+
delete merged.$rev; // skip OCC — force-write
|
|
203
|
+
await set(merged);
|
|
204
|
+
dirtyRef.current = false;
|
|
205
|
+
lastSaveRef.current = Date.now();
|
|
206
|
+
} catch (e) {
|
|
207
|
+
console.error('[autoSave] failed:', e);
|
|
208
|
+
} finally {
|
|
209
|
+
savingRef.current = false;
|
|
210
|
+
if (pendingRef.current) {
|
|
211
|
+
pendingRef.current = false;
|
|
212
|
+
flush();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}, [local]);
|
|
216
|
+
|
|
217
|
+
const setField = useCallback((field: string, value: unknown) => {
|
|
218
|
+
setLocal(prev => ({ ...prev, [field]: value }));
|
|
219
|
+
dirtyRef.current = true;
|
|
220
|
+
|
|
221
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
222
|
+
const elapsed = Date.now() - lastSaveRef.current;
|
|
223
|
+
const delay = elapsed > 2000 ? 500 : 2000; // first batch fast, then throttle
|
|
224
|
+
timerRef.current = setTimeout(() => { timerRef.current = null; flush(); }, delay);
|
|
225
|
+
}, [flush]);
|
|
226
|
+
|
|
227
|
+
// Flush on unmount
|
|
228
|
+
useEffect(() => () => {
|
|
229
|
+
if (timerRef.current) { clearTimeout(timerRef.current); flush(); }
|
|
230
|
+
}, [flush]);
|
|
231
|
+
|
|
232
|
+
// Reset local on node path change
|
|
233
|
+
useEffect(() => { setLocal({}); dirtyRef.current = false; }, [node.$path]);
|
|
234
|
+
|
|
235
|
+
const merged = useMemo(() => ({ ...node, ...local }), [node, local]);
|
|
236
|
+
|
|
237
|
+
return [merged, setField, dirtyRef.current] as const;
|
|
238
|
+
}
|
|
116
239
|
|
|
117
240
|
// ── Internals ──
|
|
118
241
|
|
|
119
|
-
const AsyncGenFn = Object.getPrototypeOf(async function* () {}).constructor;
|
|
242
|
+
const AsyncGenFn = Object.getPrototypeOf(async function* () { }).constructor;
|
|
243
|
+
const AsyncFn = Object.getPrototypeOf(async function () { }).constructor;
|
|
244
|
+
|
|
245
|
+
/** Optimistic prediction: run a sync method locally on a cloned cached node */
|
|
246
|
+
export function predictOptimistic<T extends object>(
|
|
247
|
+
path: string, cls: Class<T>, key: string | undefined,
|
|
248
|
+
fn: Function, data: unknown,
|
|
249
|
+
): void {
|
|
250
|
+
if (fn instanceof AsyncFn) return;
|
|
251
|
+
|
|
252
|
+
const cached = cache.get(path);
|
|
253
|
+
if (!cached) return;
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const draft = structuredClone(cached);
|
|
257
|
+
const target = getComponent(draft, cls, key);
|
|
258
|
+
if (!target) return;
|
|
259
|
+
|
|
260
|
+
fn.call(target, data);
|
|
261
|
+
cache.put(draft);
|
|
262
|
+
} catch { /* prediction failed — server-only */ }
|
|
263
|
+
}
|
|
120
264
|
|
|
121
265
|
function streamToAsyncIterable<T>(
|
|
122
266
|
input: { path: string; type?: string; key?: string; action: string; data?: unknown },
|
|
@@ -157,7 +301,7 @@ function makeProxy<T extends object>(
|
|
|
157
301
|
): TypeProxy<T> {
|
|
158
302
|
const type = normalizeType(cls);
|
|
159
303
|
const comp = node
|
|
160
|
-
? (key ? getComponent(node, key) :
|
|
304
|
+
? (key ? getComponent(node, key) : getComponent(node, cls))
|
|
161
305
|
: undefined;
|
|
162
306
|
|
|
163
307
|
return new Proxy(comp ?? {}, {
|
|
@@ -166,7 +310,11 @@ function makeProxy<T extends object>(
|
|
|
166
310
|
if (typeof fn === 'function') {
|
|
167
311
|
if (fn instanceof AsyncGenFn)
|
|
168
312
|
return (data?: unknown) => streamToAsyncIterable({ path, type, key, action: prop, data });
|
|
169
|
-
|
|
313
|
+
|
|
314
|
+
return (data?: unknown) => {
|
|
315
|
+
predictOptimistic(path, cls, key, fn, data);
|
|
316
|
+
return trpc.execute.mutate({ path, type, key, action: prop, data });
|
|
317
|
+
};
|
|
170
318
|
}
|
|
171
319
|
return (comp as any)?.[prop];
|
|
172
320
|
},
|
package/src/idb.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Raw IDB API, no dependencies. Fire-and-forget friendly.
|
|
3
3
|
// Degrades silently if IDB unavailable (private browsing, SSR).
|
|
4
4
|
|
|
5
|
-
import type { NodeData } from '@treenity/core
|
|
5
|
+
import type { NodeData } from '@treenity/core';
|
|
6
6
|
|
|
7
7
|
const DB_NAME = 'treenity';
|
|
8
8
|
const DB_VERSION = 1;
|
package/src/lib/minimd.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Minimal markdown → HTML for chat bubbles (~50 lines)
|
|
2
2
|
// Covers: headers, bold, italic, inline code, code blocks, links, lists, blockquotes, hr
|
|
3
3
|
import './minimd.css';
|
|
4
|
+
import { sanitizeHref } from './sanitize-href';
|
|
4
5
|
|
|
5
6
|
const esc = (s: string) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
6
7
|
|
|
@@ -11,7 +12,12 @@ function inline(s: string): string {
|
|
|
11
12
|
.replace(/__(.+?)__/g, '<strong>$1</strong>')
|
|
12
13
|
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
13
14
|
.replace(/_(.+?)_/g, '<em>$1</em>')
|
|
14
|
-
.replace(/\[([^\]]+)\]\(([^)]+)\)/g,
|
|
15
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text, url) => {
|
|
16
|
+
const safe = sanitizeHref(url);
|
|
17
|
+
if (!safe) return text;
|
|
18
|
+
const safeUrl = safe.replace(/"/g, '"');
|
|
19
|
+
return `<a href="${safeUrl}" target="_blank" rel="noopener">${text}</a>`;
|
|
20
|
+
});
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
export function minimd(src: string): string {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Allowlist-based href sanitizer — blocks javascript:, data:, vbscript:, etc.
|
|
2
|
+
// Strips control characters before protocol check to defeat browser bypass vectors
|
|
3
|
+
// (e.g. "java\tscript:" → browsers collapse to "javascript:")
|
|
4
|
+
|
|
5
|
+
const SAFE_PROTOCOL = /^(https?:|mailto:|tel:|\/|#)/i;
|
|
6
|
+
|
|
7
|
+
/** Returns sanitized URL string, or null if the protocol is unsafe. */
|
|
8
|
+
export function sanitizeHref(url: string): string | null {
|
|
9
|
+
const trimmed = url.replace(/[\x00-\x20]+/g, '');
|
|
10
|
+
if (!trimmed) return null;
|
|
11
|
+
if (SAFE_PROTOCOL.test(trimmed)) return url.trim();
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Deep-clone any value into a plain JSON-safe object.
|
|
2
|
+
// Strips valtio proxies, preserves Dates, drops functions/symbols.
|
|
3
|
+
|
|
4
|
+
export function toPlain<T>(value: T): T {
|
|
5
|
+
if (value === null || value === undefined) return value;
|
|
6
|
+
|
|
7
|
+
if (value instanceof Date) return new Date(value.getTime()) as T;
|
|
8
|
+
|
|
9
|
+
if (Array.isArray(value)) return value.map(toPlain) as T;
|
|
10
|
+
|
|
11
|
+
if (typeof value === 'object') {
|
|
12
|
+
const out: Record<string, unknown> = {};
|
|
13
|
+
for (const [k, v] of Object.entries(value)) {
|
|
14
|
+
if (typeof v === 'function' || typeof v === 'symbol') continue;
|
|
15
|
+
out[k] = toPlain(v);
|
|
16
|
+
}
|
|
17
|
+
return out as T;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return value;
|
|
21
|
+
}
|
package/src/main.tsx
CHANGED
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
2
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
3
3
|
import { enablePatches } from 'immer';
|
|
4
|
-
import { StrictMode } from 'react';
|
|
4
|
+
import { StrictMode, type ReactNode } from 'react';
|
|
5
5
|
import { createRoot } from 'react-dom/client';
|
|
6
6
|
import { App } from './App';
|
|
7
7
|
import './load-client';
|
|
8
|
-
import './
|
|
8
|
+
import { Toaster } from './components/ui/sonner';
|
|
9
|
+
import './root.css';
|
|
9
10
|
|
|
10
11
|
enablePatches();
|
|
11
12
|
|
|
12
13
|
const queryClient = new QueryClient();
|
|
13
14
|
|
|
15
|
+
// StrictMode off: FlowGram inversify container breaks on double-mount
|
|
16
|
+
// https://github.com/bytedance/flowgram.ai/issues/402
|
|
17
|
+
// TODO: re-enable once FlowGram fixes React 19 StrictMode support
|
|
18
|
+
// const Strict = import.meta.env.VITE_STRICT_MODE !== 'false'
|
|
19
|
+
// ? StrictMode
|
|
20
|
+
// : ({ children }: { children: ReactNode }) => children;
|
|
21
|
+
const Strict = ({ children }: { children: ReactNode }) => children;
|
|
22
|
+
|
|
14
23
|
const root = document.getElementById('root');
|
|
15
24
|
if (!root) throw new Error('No #root element');
|
|
16
25
|
createRoot(root).render(
|
|
17
|
-
<
|
|
26
|
+
<Strict>
|
|
18
27
|
<QueryClientProvider client={queryClient}>
|
|
19
28
|
<App />
|
|
29
|
+
<Toaster />
|
|
20
30
|
</QueryClientProvider>
|
|
21
|
-
</
|
|
31
|
+
</Strict>,
|
|
22
32
|
);
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// FieldLabel — interactive label for Inspector fields
|
|
2
|
+
// Click → dropdown menu (value/$ref/$map + copy/clear), drop target for tree nodes
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuItem,
|
|
8
|
+
DropdownMenuSeparator,
|
|
9
|
+
DropdownMenuTrigger,
|
|
10
|
+
} from '#components/ui/dropdown-menu';
|
|
11
|
+
import { Input } from '#components/ui/input';
|
|
12
|
+
import { isRef } from '@treenity/core';
|
|
13
|
+
import { useState } from 'react';
|
|
14
|
+
|
|
15
|
+
type FieldMode = 'value' | 'ref' | 'map';
|
|
16
|
+
|
|
17
|
+
function getFieldMode(v: unknown): FieldMode {
|
|
18
|
+
if (v && typeof v === 'object' && isRef(v)) {
|
|
19
|
+
return (v as { $map?: string }).$map !== undefined ? 'map' : 'ref';
|
|
20
|
+
}
|
|
21
|
+
return 'value';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const MODE_LABELS: Record<FieldMode, string> = { value: 'val', ref: '$ref', map: '$map' };
|
|
25
|
+
|
|
26
|
+
/** Interactive field label — click for mode menu, drop target for tree nodes */
|
|
27
|
+
export function FieldLabel({ label, value, onChange }: {
|
|
28
|
+
label: string;
|
|
29
|
+
value: unknown;
|
|
30
|
+
onChange?: (next: unknown) => void;
|
|
31
|
+
}) {
|
|
32
|
+
const [dragOver, setDragOver] = useState(false);
|
|
33
|
+
const mode = getFieldMode(value);
|
|
34
|
+
|
|
35
|
+
function switchMode(next: FieldMode) {
|
|
36
|
+
if (!onChange || next === mode) return;
|
|
37
|
+
if (next === 'value') {
|
|
38
|
+
onChange(0);
|
|
39
|
+
} else if (next === 'ref') {
|
|
40
|
+
onChange({ $ref: '.' });
|
|
41
|
+
} else {
|
|
42
|
+
const r = isRef(value) ? (value as { $ref: string }).$ref : '.';
|
|
43
|
+
onChange({ $ref: r, $map: '' });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!onChange) {
|
|
48
|
+
return <label>{label}</label>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<label
|
|
53
|
+
className={dragOver ? 'text-primary cursor-pointer' : 'cursor-pointer'}
|
|
54
|
+
onDragOver={(e) => {
|
|
55
|
+
if (e.dataTransfer.types.includes('application/treenity-path')) {
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
setDragOver(true);
|
|
58
|
+
}
|
|
59
|
+
}}
|
|
60
|
+
onDragLeave={() => setDragOver(false)}
|
|
61
|
+
onDrop={(e) => {
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
setDragOver(false);
|
|
64
|
+
const path = e.dataTransfer.getData('application/treenity-path');
|
|
65
|
+
if (path && onChange) {
|
|
66
|
+
const existing = isRef(value) ? (value as { $map?: string }).$map : undefined;
|
|
67
|
+
onChange(existing !== undefined ? { $ref: path, $map: existing } : { $ref: path });
|
|
68
|
+
}
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
<DropdownMenu>
|
|
72
|
+
<DropdownMenuTrigger asChild>
|
|
73
|
+
<span className="block overflow-hidden text-ellipsis">{label}</span>
|
|
74
|
+
</DropdownMenuTrigger>
|
|
75
|
+
<DropdownMenuContent align="start" className="min-w-[100px]">
|
|
76
|
+
{(['value', 'ref', 'map'] as FieldMode[]).map((m) => (
|
|
77
|
+
<DropdownMenuItem key={m} onClick={() => switchMode(m)}>
|
|
78
|
+
{mode === m ? '\u25CF ' : '\u00A0\u00A0'}{MODE_LABELS[m]}
|
|
79
|
+
</DropdownMenuItem>
|
|
80
|
+
))}
|
|
81
|
+
<DropdownMenuSeparator />
|
|
82
|
+
<DropdownMenuItem onClick={() => navigator.clipboard.writeText(JSON.stringify(value))}>
|
|
83
|
+
Copy
|
|
84
|
+
</DropdownMenuItem>
|
|
85
|
+
<DropdownMenuItem onClick={() => onChange(undefined)}>
|
|
86
|
+
Clear
|
|
87
|
+
</DropdownMenuItem>
|
|
88
|
+
</DropdownMenuContent>
|
|
89
|
+
</DropdownMenu>
|
|
90
|
+
</label>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Inline ref/map editor — $ref + optional $map, compact single/double row */
|
|
95
|
+
export function RefEditor({ value, onChange }: {
|
|
96
|
+
value: { $ref: string; $map?: string };
|
|
97
|
+
onChange: (next: unknown) => void;
|
|
98
|
+
}) {
|
|
99
|
+
const hasMap = value.$map !== undefined;
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div className="flex flex-col gap-1 flex-1 min-w-0">
|
|
103
|
+
<div className="flex items-center gap-1">
|
|
104
|
+
<span className="text-[9px] text-muted-foreground shrink-0 w-5">$ref</span>
|
|
105
|
+
<Input
|
|
106
|
+
className="h-7 text-xs flex-1 min-w-0"
|
|
107
|
+
value={value.$ref}
|
|
108
|
+
onChange={(e) => onChange(hasMap ? { $ref: e.target.value, $map: value.$map } : { $ref: e.target.value })}
|
|
109
|
+
placeholder="path"
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
{hasMap && (
|
|
113
|
+
<div className="flex items-center gap-1">
|
|
114
|
+
<span className="text-[9px] text-muted-foreground shrink-0 w-5">$map</span>
|
|
115
|
+
<Input
|
|
116
|
+
className="h-7 text-xs flex-1 min-w-0"
|
|
117
|
+
value={value.$map ?? ''}
|
|
118
|
+
onChange={(e) => onChange({ $ref: value.$ref, $map: e.target.value })}
|
|
119
|
+
placeholder="field"
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Checkbox } from '#components/ui/checkbox';
|
|
2
|
+
import { Input } from '#components/ui/input';
|
|
3
|
+
import { useSchema } from '#schema-loader';
|
|
4
|
+
import { type ComponentData, isRef, register, resolve } from '@treenity/core';
|
|
5
|
+
import { createElement } from 'react';
|
|
6
|
+
import { FieldLabel, RefEditor } from './FieldLabel';
|
|
7
|
+
import { renderField, StringArrayField } from './form-field';
|
|
8
|
+
|
|
9
|
+
function DefaultEditForm({ value, onChange }: { value: ComponentData; onChange?: (next: ComponentData) => void }) {
|
|
10
|
+
const schema = useSchema(value.$type);
|
|
11
|
+
if (schema === undefined) return null;
|
|
12
|
+
|
|
13
|
+
const data: Record<string, unknown> = {};
|
|
14
|
+
for (const [k, v] of Object.entries(value)) {
|
|
15
|
+
if (!k.startsWith('$')) data[k] = v;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const setData = (fn: (prev: Record<string, unknown>) => Record<string, unknown>) => {
|
|
19
|
+
if (!onChange) return;
|
|
20
|
+
const next = fn(data);
|
|
21
|
+
onChange({ ...value, ...next } as ComponentData);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Schema-driven form
|
|
25
|
+
if (schema && Object.keys(schema.properties).length > 0) {
|
|
26
|
+
return (
|
|
27
|
+
<div className="py-0.5 pb-2.5">
|
|
28
|
+
{Object.entries(schema.properties).map(([field, prop]) => {
|
|
29
|
+
const p = prop as {
|
|
30
|
+
type: string; title: string; format?: string; description?: string;
|
|
31
|
+
readOnly?: boolean; enum?: string[]; items?: { type?: string; properties?: Record<string, unknown> };
|
|
32
|
+
refType?: string;
|
|
33
|
+
};
|
|
34
|
+
return renderField(field, {
|
|
35
|
+
type: p.format ?? p.type, label: p.title ?? field, placeholder: p.description,
|
|
36
|
+
readOnly: p.readOnly || !onChange, enum: p.enum, items: p.items, refType: p.refType,
|
|
37
|
+
}, data, setData);
|
|
38
|
+
})}
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Fallback: raw field rendering
|
|
44
|
+
if (Object.keys(data).length > 0) {
|
|
45
|
+
return (
|
|
46
|
+
<div className="py-0.5 pb-2.5">
|
|
47
|
+
{Object.entries(data).map(([k, v]) => {
|
|
48
|
+
const onCh = (next: unknown) => setData((prev) => ({ ...prev, [k]: next }));
|
|
49
|
+
if (v && typeof v === 'object' && isRef(v)) {
|
|
50
|
+
return (
|
|
51
|
+
<div key={k} className="field">
|
|
52
|
+
<FieldLabel label={k} value={v} onChange={onCh} />
|
|
53
|
+
<RefEditor value={v as { $ref: string; $map?: string }} onChange={onCh} />
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return (
|
|
58
|
+
<div key={k} className={`field${Array.isArray(v) || (typeof v === 'object' && v !== null) ? ' stack' : ''}`}>
|
|
59
|
+
<FieldLabel label={k} value={v} onChange={onCh} />
|
|
60
|
+
{typeof v === 'boolean' ? (
|
|
61
|
+
<label className="flex items-center gap-2 cursor-pointer">
|
|
62
|
+
<Checkbox checked={!!data[k]}
|
|
63
|
+
onChange={(e) => setData((prev) => ({ ...prev, [k]: (e.target as HTMLInputElement).checked }))} />
|
|
64
|
+
{data[k] ? 'true' : 'false'}
|
|
65
|
+
</label>
|
|
66
|
+
) : typeof v === 'number' ? (
|
|
67
|
+
<Input type="number" className="h-7 text-xs" value={String(data[k] ?? 0)}
|
|
68
|
+
onChange={(e) => setData((prev) => ({ ...prev, [k]: Number(e.target.value) }))} />
|
|
69
|
+
) : Array.isArray(v) ? (
|
|
70
|
+
<StringArrayField value={data[k] as unknown[]}
|
|
71
|
+
onChange={(next) => setData((prev) => ({ ...prev, [k]: next }))} />
|
|
72
|
+
) : typeof v === 'object' ? (
|
|
73
|
+
(() => {
|
|
74
|
+
const h = resolve('object', 'react:form');
|
|
75
|
+
return h
|
|
76
|
+
? createElement(h as any, {
|
|
77
|
+
value: { $type: 'object', value: data[k] },
|
|
78
|
+
onChange: (next: { value: unknown }) => setData((prev) => ({ ...prev, [k]: next.value })),
|
|
79
|
+
})
|
|
80
|
+
: <pre className="text-[11px] font-mono text-foreground/60">{JSON.stringify(data[k], null, 2)}</pre>;
|
|
81
|
+
})()
|
|
82
|
+
) : (
|
|
83
|
+
<Input className="h-7 text-xs" value={String(data[k] ?? '')}
|
|
84
|
+
onChange={(e) => setData((prev) => ({ ...prev, [k]: e.target.value }))} />
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
})}
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Empty
|
|
94
|
+
return (
|
|
95
|
+
<pre className="text-[11px] font-mono text-foreground/60 bg-muted/30 rounded p-2 whitespace-pre-wrap">
|
|
96
|
+
{JSON.stringify(data, null, 2)}
|
|
97
|
+
</pre>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
register('default', 'react:edit', DefaultEditForm as any);
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import './editor-ui.css';
|
|
2
|
+
import { Button } from '#components/ui/button';
|
|
1
3
|
import { Render, RenderContext } from '#context';
|
|
2
4
|
import { useChildren } from '#hooks';
|
|
3
5
|
import { trpc } from '#trpc';
|
|
4
|
-
import { type ComponentData, type NodeData, register } from '@treenity/core
|
|
6
|
+
import { type ComponentData, type NodeData, register } from '@treenity/core';
|
|
5
7
|
import { useCallback, useState } from 'react';
|
|
8
|
+
import { EmptyNodePlaceholder } from './empty-placeholder';
|
|
6
9
|
import { getComponents, getPlainFields, getSchema } from './node-utils';
|
|
7
10
|
|
|
8
11
|
/** Fallback for components without their own react handler */
|
|
@@ -45,7 +48,7 @@ function ComponentFieldsView({ value }: { value: ComponentData }) {
|
|
|
45
48
|
);
|
|
46
49
|
}
|
|
47
50
|
|
|
48
|
-
function GenerateViewButton({ type, sample }: { type: string; sample: NodeData }) {
|
|
51
|
+
export function GenerateViewButton({ type, sample, context, label }: { type: string; sample: NodeData; context?: string; label?: string }) {
|
|
49
52
|
const [status, setStatus] = useState<'idle' | 'generating' | 'done' | 'error'>('idle');
|
|
50
53
|
const [error, setError] = useState('');
|
|
51
54
|
|
|
@@ -58,7 +61,7 @@ function GenerateViewButton({ type, sample }: { type: string; sample: NodeData }
|
|
|
58
61
|
await trpc.execute.mutate({
|
|
59
62
|
path: '/metatron',
|
|
60
63
|
action: 'task',
|
|
61
|
-
data: { prompt: `Generate a React view for type "${type}". Sample data:\n${JSON.stringify(clean, null, 2)}` },
|
|
64
|
+
data: { prompt: `Generate a React view for type "${type}"${context ? ` in context "${context}"` : ''}. Sample data:\n${JSON.stringify(clean, null, 2)}` },
|
|
62
65
|
});
|
|
63
66
|
setStatus('done');
|
|
64
67
|
} catch (err: any) {
|
|
@@ -78,12 +81,14 @@ function GenerateViewButton({ type, sample }: { type: string; sample: NodeData }
|
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
return (
|
|
81
|
-
<
|
|
84
|
+
<Button
|
|
85
|
+
variant="outline"
|
|
86
|
+
size="sm"
|
|
82
87
|
onClick={generate}
|
|
83
|
-
className="text-
|
|
88
|
+
className="text-blue-400 border-blue-400/30 hover:text-blue-300 my-2"
|
|
84
89
|
>
|
|
85
|
-
Generate AI View
|
|
86
|
-
</
|
|
90
|
+
{label ?? 'Generate AI View'}
|
|
91
|
+
</Button>
|
|
87
92
|
);
|
|
88
93
|
}
|
|
89
94
|
|
|
@@ -134,7 +139,7 @@ function DefaultNodeView({ value }: { value: NodeData }) {
|
|
|
134
139
|
</RenderContext>
|
|
135
140
|
)}
|
|
136
141
|
|
|
137
|
-
{children.length === 0 && !hasInfo && <
|
|
142
|
+
{children.length === 0 && !hasInfo && <EmptyNodePlaceholder value={value} />}
|
|
138
143
|
</div>
|
|
139
144
|
);
|
|
140
145
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Render, RenderContext } from '#context';
|
|
2
2
|
import { useChildren } from '#hooks';
|
|
3
|
-
import { type NodeData
|
|
3
|
+
import { type NodeData } from '@treenity/core';
|
|
4
4
|
|
|
5
5
|
const STATUS_COLORS: Record<string, [string, string]> = {
|
|
6
6
|
draft: ['var(--accent-subtle, #1a2a3a)', 'var(--accent)'],
|
|
@@ -88,4 +88,4 @@ function FolderView({ value }: { value: NodeData }) {
|
|
|
88
88
|
);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
register('dir', 'react', FolderView as any);
|
|
91
|
+
// register('dir', 'react', FolderView as any);
|