@object-ui/app-shell 11.2.0 → 11.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +562 -0
  2. package/README.md +23 -0
  3. package/dist/console/ConsoleShell.js +17 -2
  4. package/dist/console/home/CloudOnboardingNext.d.ts +9 -0
  5. package/dist/console/home/CloudOnboardingNext.js +14 -4
  6. package/dist/console/home/HomePage.js +34 -7
  7. package/dist/console/organizations/CreateWorkspaceDialog.js +33 -3
  8. package/dist/console/organizations/OrganizationsPage.js +16 -7
  9. package/dist/hooks/useConsoleActionRuntime.js +32 -3
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.js +6 -0
  12. package/dist/preview/DraftChangesPanel.d.ts +3 -1
  13. package/dist/preview/DraftChangesPanel.js +6 -5
  14. package/dist/utils/deriveRelatedLists.d.ts +20 -5
  15. package/dist/utils/deriveRelatedLists.js +31 -13
  16. package/dist/utils/index.d.ts +2 -24
  17. package/dist/utils/index.js +14 -101
  18. package/dist/utils/resolveViewId.d.ts +23 -0
  19. package/dist/utils/resolveViewId.js +37 -0
  20. package/dist/utils/warnSuppressedListNav.d.ts +10 -0
  21. package/dist/utils/warnSuppressedListNav.js +40 -0
  22. package/dist/views/DashboardView.js +2 -3
  23. package/dist/views/InterfaceListPage.js +10 -5
  24. package/dist/views/ObjectView.js +65 -12
  25. package/dist/views/PageView.js +2 -3
  26. package/dist/views/RecordDetailView.js +131 -104
  27. package/dist/views/RecordFormPage.js +7 -1
  28. package/dist/views/RelatedRecordActionsBridge.d.ts +24 -0
  29. package/dist/views/RelatedRecordActionsBridge.js +114 -0
  30. package/dist/views/ReportView.js +2 -3
  31. package/dist/views/metadata-admin/PackagesPage.js +18 -7
  32. package/dist/views/metadata-admin/PermissionMatrixEditor.d.ts +18 -1
  33. package/dist/views/metadata-admin/PermissionMatrixEditor.js +73 -14
  34. package/dist/views/metadata-admin/clientValidation.js +8 -2
  35. package/dist/views/metadata-admin/color-variant-field.d.ts +1 -12
  36. package/dist/views/metadata-admin/color-variant-field.js +11 -0
  37. package/dist/views/metadata-admin/i18n.d.ts +12 -21
  38. package/dist/views/metadata-admin/i18n.js +343 -2
  39. package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +25 -11
  40. package/dist/views/metadata-admin/permission-slice.d.ts +66 -0
  41. package/dist/views/metadata-admin/permission-slice.js +70 -0
  42. package/dist/views/metadata-admin/previews/AppNavCanvas.js +11 -7
  43. package/dist/views/metadata-admin/previews/FlowRunsPanel.d.ts +16 -7
  44. package/dist/views/metadata-admin/previews/FlowRunsPanel.js +18 -2
  45. package/dist/views/metadata-admin/previews/OutlineStrip.d.ts +1 -13
  46. package/dist/views/metadata-admin/previews/OutlineStrip.js +12 -0
  47. package/dist/views/metadata-admin/previews/PagePreview.js +9 -0
  48. package/dist/views/metadata-admin/previews/SourcePageEditor.d.ts +28 -0
  49. package/dist/views/metadata-admin/previews/SourcePageEditor.js +83 -0
  50. package/dist/views/studio-design/BuilderLanding.d.ts +15 -0
  51. package/dist/views/studio-design/BuilderLanding.js +133 -0
  52. package/dist/views/studio-design/ObjectFormDesigner.d.ts +31 -0
  53. package/dist/views/studio-design/ObjectFormDesigner.js +226 -0
  54. package/dist/views/studio-design/ObjectSettingsPanel.d.ts +30 -0
  55. package/dist/views/studio-design/ObjectSettingsPanel.js +45 -0
  56. package/dist/views/studio-design/ObjectValidationsPanel.d.ts +30 -0
  57. package/dist/views/studio-design/ObjectValidationsPanel.js +78 -0
  58. package/dist/views/studio-design/StudioDesignSurface.d.ts +20 -0
  59. package/dist/views/studio-design/StudioDesignSurface.js +1306 -0
  60. package/dist/views/studio-design/metadataError.d.ts +23 -0
  61. package/dist/views/studio-design/metadataError.js +44 -0
  62. package/dist/views/studio-design/packages-io.d.ts +27 -0
  63. package/dist/views/studio-design/packages-io.js +61 -0
  64. package/package.json +46 -43
@@ -0,0 +1,133 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
3
+ /**
4
+ * BuilderLanding — the application builder's front door.
5
+ *
6
+ * The journey from login: Home → Studio app → 「应用构建」 (this page, embedded
7
+ * in the app chrome via the `studio:builder` component ref) → pick or create a
8
+ * writable base package → the full-screen pillar builder
9
+ * (`/studio/:packageId/:tab`). Also served standalone at bare `/studio` so the
10
+ * builder is bookmarkable.
11
+ *
12
+ * Writable bases (where authoring happens) lead; read-only code packages are
13
+ * listed secondary for browsing. Writability is the shared display heuristic
14
+ * from packages-io — the ADR-0070 D4 gate stays the server-side authority.
15
+ */
16
+ import * as React from 'react';
17
+ import { useNavigate } from 'react-router-dom';
18
+ import { Boxes, Hammer, Lock, Plus, Loader2, Copy } from 'lucide-react';
19
+ import { toast } from 'sonner';
20
+ import { toFieldNameLoose } from '../metadata-admin/previews/object-fields-io';
21
+ import { fetchPackages, createBasePackage, duplicatePackage, PACKAGE_ID_RE } from './packages-io';
22
+ export function BuilderLanding() {
23
+ const navigate = useNavigate();
24
+ const [pkgs, setPkgs] = React.useState(null);
25
+ const [error, setError] = React.useState(null);
26
+ const [creating, setCreating] = React.useState(false);
27
+ const [newName, setNewName] = React.useState('');
28
+ const [newId, setNewId] = React.useState('');
29
+ const [idTouched, setIdTouched] = React.useState(false);
30
+ const [busy, setBusy] = React.useState(false);
31
+ React.useEffect(() => {
32
+ let cancelled = false;
33
+ fetchPackages()
34
+ .then((list) => {
35
+ if (!cancelled)
36
+ setPkgs(list);
37
+ })
38
+ .catch((e) => {
39
+ if (!cancelled)
40
+ setError(e instanceof Error ? e.message : String(e));
41
+ });
42
+ return () => {
43
+ cancelled = true;
44
+ };
45
+ }, []);
46
+ const open = (id) => navigate(`/studio/${encodeURIComponent(id)}/data`);
47
+ const doCreate = async () => {
48
+ const name = newName.trim();
49
+ const id = newId.trim();
50
+ if (!name || !PACKAGE_ID_RE.test(id))
51
+ return;
52
+ setBusy(true);
53
+ setError(null);
54
+ try {
55
+ await createBasePackage(id, name);
56
+ open(id);
57
+ }
58
+ catch (e) {
59
+ setError(e instanceof Error ? e.message : String(e));
60
+ }
61
+ finally {
62
+ setBusy(false);
63
+ }
64
+ };
65
+ const writable = pkgs?.filter((p) => p.writable) ?? [];
66
+ const readonly = pkgs?.filter((p) => !p.writable) ?? [];
67
+ // 复制为可写副本 (ADR-0070 D4): a read-only code package is a STARTING POINT,
68
+ // not a dead end — duplicate re-namespaces it into a new writable base and
69
+ // drops the user straight into its builder. This is also the real substance
70
+ // behind Home's "Start with a template".
71
+ const [dupFor, setDupFor] = React.useState(null);
72
+ const [dupName, setDupName] = React.useState('');
73
+ const [dupId, setDupId] = React.useState('');
74
+ const [dupBusy, setDupBusy] = React.useState(false);
75
+ const [dupErr, setDupErr] = React.useState(null);
76
+ const startDup = (p) => {
77
+ setDupFor(p.id);
78
+ setDupName(`${p.name} 副本`);
79
+ setDupId(`${p.id}-copy`);
80
+ setDupErr(null);
81
+ };
82
+ const doDup = async () => {
83
+ if (!dupFor)
84
+ return;
85
+ const id = dupId.trim();
86
+ const name = dupName.trim();
87
+ if (!PACKAGE_ID_RE.test(id) || !name)
88
+ return;
89
+ setDupBusy(true);
90
+ setDupErr(null);
91
+ try {
92
+ await duplicatePackage(dupFor, id, name);
93
+ toast.success(`已复制为可写软件包「${name}」`);
94
+ open(id);
95
+ }
96
+ catch (e) {
97
+ setDupErr(e instanceof Error ? e.message : String(e));
98
+ }
99
+ finally {
100
+ setDupBusy(false);
101
+ }
102
+ };
103
+ return (_jsxs("div", { className: "mx-auto w-full max-w-3xl p-6", children: [_jsxs("div", { className: "mb-1 flex items-center gap-2", children: [_jsx(Hammer, { className: "h-5 w-5 text-primary" }), _jsx("h1", { className: "text-lg font-semibold", children: "\u5E94\u7528\u6784\u5EFA" })] }), _jsxs("p", { className: "mb-5 text-xs leading-5 text-muted-foreground", children: ["\u5728\u4E00\u4E2A", _jsx("b", { children: "\u53EF\u5199\u8F6F\u4EF6\u5305" }), "\u91CC\u8BBE\u8BA1\u5BF9\u8C61\u3001\u8868\u5355\u3001\u81EA\u52A8\u5316\u4E0E\u754C\u9762;\u7F16\u8F91\u5B58\u4E3A\u8349\u7A3F,\u6574\u5305\u4E00\u6B21\u53D1\u5E03\u3002 \u6E90\u7801\u52A0\u8F7D\u7684\u8F6F\u4EF6\u5305\u4E3A\u53EA\u8BFB(\u4EC5\u53EF\u6D4F\u89C8)\u3002"] }), error && (_jsx("div", { className: "mb-3 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-1.5 text-[11px] text-destructive", children: error })), _jsx("h2", { className: "mb-2 text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: "\u6211\u7684\u8F6F\u4EF6\u5305(\u53EF\u5199)" }), _jsxs("div", { className: "mb-6 grid grid-cols-1 gap-2 sm:grid-cols-2", children: [pkgs === null && _jsx("p", { className: "text-[11px] text-muted-foreground", children: "\u52A0\u8F7D\u4E2D\u2026" }), pkgs !== null && writable.length === 0 && (_jsx("p", { className: "text-[11px] text-muted-foreground", children: "\u8FD8\u6CA1\u6709\u53EF\u5199\u8F6F\u4EF6\u5305 \u2014 \u4ECE\u53F3\u4FA7\u65B0\u5EFA\u4E00\u4E2A\u5F00\u59CB\u3002" })), writable.map((p) => (_jsxs("div", { className: "rounded-lg border bg-background", children: [_jsxs("div", { className: "flex items-center gap-2.5 px-3 py-2.5", children: [_jsxs("button", { type: "button", onClick: () => open(p.id), className: "flex min-w-0 flex-1 items-center gap-2.5 text-left", children: [_jsx(Boxes, { className: "h-4 w-4 shrink-0 text-primary" }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate text-[13px] font-medium", children: p.name }), _jsx("span", { className: "block truncate font-mono text-[10px] text-muted-foreground", children: p.id })] })] }), _jsx("span", { className: "rounded bg-emerald-400/15 px-1.5 py-0.5 text-[10px] text-emerald-600 dark:text-emerald-300", children: "\u53EF\u5199" }), _jsxs("button", { type: "button", onClick: () => (dupFor === p.id ? setDupFor(null) : startDup(p)), title: "\u590D\u5236\u8FD9\u4E2A\u8F6F\u4EF6\u5305\u4E3A\u65B0\u7684\u53EF\u5199\u526F\u672C(Airtable \u7684 duplicate base)", className: "inline-flex items-center gap-1 rounded-md border px-2 py-1 text-[10px] text-muted-foreground hover:bg-muted hover:text-foreground", children: [_jsx(Copy, { className: "h-3 w-3" }), " \u590D\u5236"] })] }), dupFor === p.id && (_jsxs("div", { className: "flex flex-col gap-1.5 border-t px-3 py-2.5", children: [_jsx("input", { autoFocus: true, value: dupName, onChange: (e) => setDupName(e.target.value), onKeyDown: (e) => {
104
+ if (e.key === 'Enter')
105
+ void doDup();
106
+ if (e.key === 'Escape')
107
+ setDupFor(null);
108
+ }, placeholder: "\u526F\u672C\u540D\u79F0", className: "h-7 w-full rounded-md border bg-background px-2 text-[11px] outline-none focus:ring-1 focus:ring-primary" }), _jsx("input", { value: dupId, onChange: (e) => setDupId(e.target.value.toLowerCase().replace(/[^a-z0-9_.-]/g, '')), onKeyDown: (e) => {
109
+ if (e.key === 'Enter')
110
+ void doDup();
111
+ if (e.key === 'Escape')
112
+ setDupFor(null);
113
+ }, placeholder: "\u526F\u672C\u5305 ID", className: "h-7 w-full rounded-md border bg-background px-2 font-mono text-[11px] outline-none focus:ring-1 focus:ring-primary" }), dupErr && _jsx("p", { className: "text-[10px] text-destructive", children: dupErr }), _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs("button", { type: "button", onClick: () => void doDup(), disabled: dupBusy || !dupName.trim() || !PACKAGE_ID_RE.test(dupId.trim()), className: "inline-flex flex-1 items-center justify-center gap-1 rounded-md bg-primary px-2 py-1 text-[11px] font-medium text-primary-foreground disabled:opacity-50", children: [dupBusy ? _jsx(Loader2, { className: "h-3 w-3 animate-spin" }) : _jsx(Copy, { className: "h-3 w-3" }), "\u590D\u5236\u5E76\u8FDB\u5165\u6784\u5EFA\u5668"] }), _jsx("button", { type: "button", onClick: () => setDupFor(null), className: "rounded-md border px-2 py-1 text-[11px] text-muted-foreground hover:bg-muted", children: "\u53D6\u6D88" })] })] }))] }, p.id))), creating ? (_jsxs("div", { className: "flex flex-col gap-1.5 rounded-lg border border-dashed bg-muted/20 px-3 py-2.5", children: [_jsx("input", { autoFocus: true, value: newName, onChange: (e) => {
114
+ setNewName(e.target.value);
115
+ if (!idTouched) {
116
+ const slug = toFieldNameLoose(e.target.value).replace(/_/g, '-');
117
+ setNewId(slug ? `com.example.${slug}` : '');
118
+ }
119
+ }, onKeyDown: (e) => {
120
+ if (e.key === 'Enter')
121
+ void doCreate();
122
+ if (e.key === 'Escape')
123
+ setCreating(false);
124
+ }, placeholder: "\u540D\u79F0(\u5982:\u7EF4\u4FEE\u4E2D\u5FC3)", className: "h-7 w-full rounded-md border bg-background px-2 text-[11px] outline-none focus:ring-1 focus:ring-primary" }), _jsx("input", { value: newId, onChange: (e) => {
125
+ setIdTouched(true);
126
+ setNewId(e.target.value.toLowerCase().replace(/[^a-z0-9_.-]/g, ''));
127
+ }, onKeyDown: (e) => {
128
+ if (e.key === 'Enter')
129
+ void doCreate();
130
+ if (e.key === 'Escape')
131
+ setCreating(false);
132
+ }, placeholder: "\u5305 ID(\u5982:com.example.repairs)", className: "h-7 w-full rounded-md border bg-background px-2 font-mono text-[11px] outline-none focus:ring-1 focus:ring-primary" }), _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs("button", { type: "button", onClick: () => void doCreate(), disabled: busy || !newName.trim() || !PACKAGE_ID_RE.test(newId.trim()), className: "inline-flex flex-1 items-center justify-center gap-1 rounded-md bg-primary px-2 py-1 text-[11px] font-medium text-primary-foreground disabled:opacity-50", children: [busy ? _jsx(Loader2, { className: "h-3 w-3 animate-spin" }) : _jsx(Plus, { className: "h-3 w-3" }), "\u521B\u5EFA\u5E76\u5F00\u59CB\u6784\u5EFA"] }), _jsx("button", { type: "button", onClick: () => setCreating(false), className: "rounded-md border px-2 py-1 text-[11px] text-muted-foreground hover:bg-muted", children: "\u53D6\u6D88" })] })] })) : (_jsxs("button", { type: "button", onClick: () => setCreating(true), className: "flex items-center justify-center gap-1.5 rounded-lg border border-dashed px-3 py-2.5 text-xs text-muted-foreground hover:border-primary/50 hover:text-foreground", children: [_jsx(Plus, { className: "h-4 w-4" }), " \u65B0\u5EFA\u8F6F\u4EF6\u5305"] }))] }), readonly.length > 0 && (_jsxs(_Fragment, { children: [_jsx("h2", { className: "mb-2 text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: "\u5DF2\u5B89\u88C5(\u53EA\u8BFB \u00B7 \u53EF\u6D4F\u89C8)" }), _jsx("div", { className: "grid grid-cols-1 gap-2 sm:grid-cols-2", children: readonly.map((p) => (_jsxs("button", { type: "button", onClick: () => open(p.id), className: "flex items-center gap-2.5 rounded-lg border bg-muted/20 px-3 py-2.5 text-left hover:bg-muted/40", children: [_jsx(Boxes, { className: "h-4 w-4 shrink-0 text-muted-foreground" }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate text-[13px]", children: p.name }), _jsx("span", { className: "block truncate font-mono text-[10px] text-muted-foreground", children: p.id })] }), _jsxs("span", { className: "inline-flex items-center gap-0.5 rounded bg-amber-400/15 px-1.5 py-0.5 text-[10px] text-amber-600 dark:text-amber-300", children: [_jsx(Lock, { className: "h-2.5 w-2.5" }), " \u53EA\u8BFB"] })] }, p.id))) })] }))] }));
133
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * ObjectFormDesigner — the WYSIWYG form-layout designer for the Studio Data
3
+ * pillar's Form view. An admin arranges the object's default form exactly as
4
+ * end users will see it: fields grouped into **sections** (the object's
5
+ * `fieldGroups`), drag-reordered within a section and dragged **across**
6
+ * sections, with per-field selection opening the same protocol field inspector
7
+ * the grid uses.
8
+ *
9
+ * Build boundary: this component is only the drag/section CHROME. The data
10
+ * model + all mutations are the existing, tested `object-fields-io` helpers
11
+ * (readFields/writeFields · readGroups/addGroup/renameGroup/removeGroup/
12
+ * moveGroup · clearFieldGroup · groupEntries), and section membership +
13
+ * in-group order persist to the object draft (`fields[].group` + `fieldGroups`)
14
+ * via the pillar's existing draft → publish. No new metadata shape.
15
+ */
16
+ import * as React from 'react';
17
+ export interface ObjectFormDesignerProps {
18
+ /** Object metadata draft (reads `fields` + `fieldGroups`). */
19
+ draft: Record<string, unknown>;
20
+ /** Field names to hide from the layout (system/audit) but preserve on write. */
21
+ systemFieldNames: Set<string>;
22
+ /** Persist a partial object-draft patch (fields / fieldGroups) + mark dirty. */
23
+ onChange: (patch: Record<string, unknown>) => void;
24
+ /** Currently selected field name (highlighted). */
25
+ selectedField?: string | null;
26
+ /** Select a field → opens the shared field inspector. */
27
+ onSelectField: (name: string) => void;
28
+ /** Append a new field (reuses the pillar's add-field). */
29
+ onAddField: () => void;
30
+ }
31
+ export declare function ObjectFormDesigner({ draft, systemFieldNames, onChange, selectedField, onSelectField, onAddField, }: ObjectFormDesignerProps): React.ReactElement;
@@ -0,0 +1,226 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
3
+ /**
4
+ * ObjectFormDesigner — the WYSIWYG form-layout designer for the Studio Data
5
+ * pillar's Form view. An admin arranges the object's default form exactly as
6
+ * end users will see it: fields grouped into **sections** (the object's
7
+ * `fieldGroups`), drag-reordered within a section and dragged **across**
8
+ * sections, with per-field selection opening the same protocol field inspector
9
+ * the grid uses.
10
+ *
11
+ * Build boundary: this component is only the drag/section CHROME. The data
12
+ * model + all mutations are the existing, tested `object-fields-io` helpers
13
+ * (readFields/writeFields · readGroups/addGroup/renameGroup/removeGroup/
14
+ * moveGroup · clearFieldGroup · groupEntries), and section membership +
15
+ * in-group order persist to the object draft (`fields[].group` + `fieldGroups`)
16
+ * via the pillar's existing draft → publish. No new metadata shape.
17
+ */
18
+ import * as React from 'react';
19
+ import { DndContext, DragOverlay, PointerSensor, KeyboardSensor, useSensor, useSensors, useDroppable, pointerWithin, } from '@dnd-kit/core';
20
+ import { SortableContext, verticalListSortingStrategy, useSortable, arrayMove, sortableKeyboardCoordinates, } from '@dnd-kit/sortable';
21
+ import { CSS } from '@dnd-kit/utilities';
22
+ import { GripVertical, Plus, Trash2, ChevronUp, ChevronDown, Rows3 } from 'lucide-react';
23
+ import { readFields, writeFields, readGroups, addGroup, renameGroup, removeGroup, moveGroup, clearFieldGroup, } from '../metadata-admin/previews/object-fields-io';
24
+ const UNGROUPED = '__ungrouped__';
25
+ const cid = (key) => `g:${key}`; // container (section) droppable id
26
+ const fid = (name) => `f:${name}`; // sortable field id
27
+ const unCid = (id) => id.slice(2);
28
+ const unFid = (id) => id.slice(2);
29
+ /** A faithful, non-interactive preview of a field's control (by type). */
30
+ function FieldControlPreview({ type }) {
31
+ const box = 'mt-1 flex items-center rounded-md border bg-muted/30 px-2 text-[11px] text-muted-foreground';
32
+ switch (type) {
33
+ case 'select':
34
+ case 'radio':
35
+ case 'lookup':
36
+ case 'reference':
37
+ case 'user':
38
+ case 'multiselect':
39
+ return (_jsxs("div", { className: `${box} h-7 justify-between`, children: [_jsx("span", { children: type === 'lookup' || type === 'reference' || type === 'user' ? '搜索…' : '请选择…' }), _jsx("span", { children: "\u25BE" })] }));
40
+ case 'textarea':
41
+ case 'html':
42
+ case 'markdown':
43
+ case 'json':
44
+ case 'code':
45
+ return _jsx("div", { className: `${box} h-14 items-start pt-1.5`, children: "\u2026" });
46
+ case 'boolean':
47
+ case 'checkbox':
48
+ case 'switch':
49
+ return (_jsx("div", { className: "mt-1 flex items-center", children: _jsx("span", { className: "h-4 w-7 rounded-full bg-muted" }) }));
50
+ case 'number':
51
+ case 'currency':
52
+ case 'percent':
53
+ return _jsx("div", { className: `${box} h-7`, children: "0.00" });
54
+ case 'date':
55
+ case 'datetime':
56
+ case 'time':
57
+ return _jsx("div", { className: `${box} h-7`, children: "\u9009\u62E9\u65E5\u671F\u2026" });
58
+ default:
59
+ return _jsx("div", { className: `${box} h-7`, children: "\u00A0" });
60
+ }
61
+ }
62
+ /** One draggable field card inside a section. */
63
+ function SortableField({ entry, selected, onSelect, }) {
64
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: fid(entry.name) });
65
+ const type = String(entry.def.type ?? 'text');
66
+ const label = String(entry.def.label ?? entry.name);
67
+ const required = !!entry.def.required;
68
+ return (_jsxs("div", { ref: setNodeRef, style: { transform: CSS.Transform.toString(transform), transition }, onClick: onSelect, ...attributes, ...listeners, "aria-label": `${label} — 点选改属性,拖动排序`, className: 'group relative flex cursor-grab touch-none select-none items-start gap-1.5 rounded-md border bg-background px-2 py-2 active:cursor-grabbing ' +
69
+ (selected ? 'ring-2 ring-primary' : 'hover:border-foreground/25') +
70
+ (isDragging ? ' opacity-40' : ''), children: [_jsx("span", { className: "mt-0.5 text-muted-foreground opacity-0 group-hover:opacity-100", children: _jsx(GripVertical, { className: "h-3.5 w-3.5" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex items-center gap-1 text-xs font-medium", children: [_jsx("span", { className: "truncate", children: label }), required && _jsx("span", { className: "text-destructive", children: "*" }), _jsx("span", { className: "ml-1 rounded bg-muted px-1 py-px text-[9px] uppercase text-muted-foreground", children: type })] }), _jsx(FieldControlPreview, { type: type })] })] }));
71
+ }
72
+ /** A section (declared group or the implicit ungrouped bucket) = a drop zone. */
73
+ function Section({ containerId, title, fieldIds, isDeclared, canMoveUp, canMoveDown, entryByName, selectedField, onSelectField, onRename, onDelete, onMove, }) {
74
+ const { setNodeRef, isOver } = useDroppable({ id: containerId });
75
+ return (_jsxs("div", { className: 'rounded-lg border ' + (isOver ? 'border-primary bg-primary/5' : 'bg-muted/20'), children: [_jsxs("div", { className: "flex items-center gap-1 border-b px-3 py-1.5", children: [isDeclared ? (_jsx("input", { defaultValue: title, onBlur: (e) => e.target.value.trim() && e.target.value !== title && onRename(e.target.value), onKeyDown: (e) => {
76
+ if (e.key === 'Enter')
77
+ e.target.blur();
78
+ }, className: "min-w-0 flex-1 rounded bg-transparent px-1 py-0.5 text-[13px] font-medium outline-none hover:bg-muted focus:bg-background focus:ring-1 focus:ring-primary" })) : (_jsx("span", { className: "flex-1 px-1 text-[13px] font-medium text-muted-foreground", children: title })), isDeclared && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", disabled: !canMoveUp, onClick: () => onMove(-1), "aria-label": "\u4E0A\u79FB\u5206\u7EC4", className: "rounded p-0.5 text-muted-foreground hover:bg-muted disabled:opacity-30", children: _jsx(ChevronUp, { className: "h-3.5 w-3.5" }) }), _jsx("button", { type: "button", disabled: !canMoveDown, onClick: () => onMove(1), "aria-label": "\u4E0B\u79FB\u5206\u7EC4", className: "rounded p-0.5 text-muted-foreground hover:bg-muted disabled:opacity-30", children: _jsx(ChevronDown, { className: "h-3.5 w-3.5" }) }), _jsx("button", { type: "button", onClick: onDelete, "aria-label": "\u5220\u9664\u5206\u7EC4", title: "\u5220\u9664\u5206\u7EC4(\u5B57\u6BB5\u5F52\u4E3A\u672A\u5206\u7EC4)", className: "rounded p-0.5 text-muted-foreground hover:bg-destructive/10 hover:text-destructive", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) })] }))] }), _jsx(SortableContext, { items: fieldIds, strategy: verticalListSortingStrategy, children: _jsxs("div", { ref: setNodeRef, className: "grid min-h-[52px] grid-cols-2 gap-2 p-2.5", children: [fieldIds.length === 0 && (_jsx("div", { className: "col-span-2 flex items-center justify-center rounded-md border border-dashed py-3 text-[11px] text-muted-foreground", children: "\u62D6\u5B57\u6BB5\u5230\u8FD9\u91CC" })), fieldIds.map((id) => {
79
+ const name = unFid(id);
80
+ const entry = entryByName.get(name);
81
+ if (!entry)
82
+ return null;
83
+ return (_jsx(SortableField, { entry: entry, selected: selectedField === name, onSelect: () => onSelectField(name) }, id));
84
+ })] }) })] }));
85
+ }
86
+ export function ObjectFormDesigner({ draft, systemFieldNames, onChange, selectedField, onSelectField, onAddField, }) {
87
+ const view = React.useMemo(() => readFields(draft.fields), [draft.fields]);
88
+ const groups = React.useMemo(() => readGroups(draft.fieldGroups), [draft.fieldGroups]);
89
+ const entryByName = React.useMemo(() => new Map(view.entries.map((e) => [e.name, e])), [view]);
90
+ // Container order: declared groups (in order) then the ungrouped bucket.
91
+ const containerOrder = React.useMemo(() => [...groups.map((g) => cid(g.key)), cid(UNGROUPED)], [groups]);
92
+ const labelOf = React.useMemo(() => {
93
+ const m = new Map();
94
+ for (const g of groups)
95
+ m.set(cid(g.key), g.label || g.key);
96
+ m.set(cid(UNGROUPED), '未分组');
97
+ return m;
98
+ }, [groups]);
99
+ // Derive container → ordered field ids from the draft (editable fields only;
100
+ // system/audit fields are preserved on write but never shown in the layout).
101
+ const derived = React.useMemo(() => {
102
+ const map = {};
103
+ for (const c of containerOrder)
104
+ map[c] = [];
105
+ for (const e of view.entries) {
106
+ if (systemFieldNames.has(e.name))
107
+ continue;
108
+ const g = typeof e.def.group === 'string' ? e.def.group : '';
109
+ const target = g && map[cid(g)] ? cid(g) : cid(UNGROUPED);
110
+ map[target].push(fid(e.name));
111
+ }
112
+ return map;
113
+ }, [view.entries, containerOrder, systemFieldNames]);
114
+ const [items, setItems] = React.useState(derived);
115
+ const [activeId, setActiveId] = React.useState(null);
116
+ // Re-sync from the draft whenever it changes and we are not mid-drag.
117
+ React.useEffect(() => {
118
+ if (!activeId)
119
+ setItems(derived);
120
+ }, [derived, activeId]);
121
+ // Latest items snapshot for drag-end math — robust whether or not onDragOver
122
+ // fired (e.g. synthetic/automated drags that skip intermediate move events).
123
+ const itemsRef = React.useRef(items);
124
+ React.useEffect(() => {
125
+ itemsRef.current = items;
126
+ }, [items]);
127
+ const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 4 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
128
+ const findContainer = React.useCallback((id) => {
129
+ if (id.startsWith('g:'))
130
+ return id;
131
+ return Object.keys(items).find((k) => items[k].includes(id));
132
+ }, [items]);
133
+ /** Flatten the container map back to `fields` (stamping group + order) and persist. */
134
+ const commit = React.useCallback((next) => {
135
+ const editable = [];
136
+ for (const c of containerOrder) {
137
+ const groupKey = unCid(c);
138
+ for (const id of next[c] ?? []) {
139
+ const e = entryByName.get(unFid(id));
140
+ if (!e)
141
+ continue;
142
+ const def = { ...e.def };
143
+ if (groupKey === UNGROUPED)
144
+ delete def.group;
145
+ else
146
+ def.group = groupKey;
147
+ editable.push({ name: e.name, def });
148
+ }
149
+ }
150
+ const system = view.entries.filter((e) => systemFieldNames.has(e.name));
151
+ const finalView = { shape: view.shape, entries: [...system, ...editable] };
152
+ onChange({ fields: writeFields(finalView) });
153
+ }, [containerOrder, entryByName, view, systemFieldNames, onChange]);
154
+ const onDragStart = (e) => setActiveId(String(e.active.id));
155
+ const onDragOver = (e) => {
156
+ const activeIdStr = String(e.active.id);
157
+ const overId = e.over ? String(e.over.id) : null;
158
+ if (!overId)
159
+ return;
160
+ const from = findContainer(activeIdStr);
161
+ const to = findContainer(overId);
162
+ if (!from || !to || from === to)
163
+ return;
164
+ setItems((prev) => {
165
+ const fromItems = prev[from] ?? [];
166
+ const toItems = prev[to] ?? [];
167
+ const overIndex = overId.startsWith('g:') ? toItems.length : toItems.indexOf(overId);
168
+ const insertAt = overIndex < 0 ? toItems.length : overIndex;
169
+ return {
170
+ ...prev,
171
+ [from]: fromItems.filter((i) => i !== activeIdStr),
172
+ [to]: [...toItems.slice(0, insertAt), activeIdStr, ...toItems.slice(insertAt)],
173
+ };
174
+ });
175
+ };
176
+ const onDragEnd = (e) => {
177
+ setActiveId(null);
178
+ const activeIdStr = String(e.active.id);
179
+ const overId = e.over ? String(e.over.id) : null;
180
+ if (!overId)
181
+ return;
182
+ const prev = itemsRef.current;
183
+ const inContainer = (id) => id.startsWith('g:') && id in prev ? id : Object.keys(prev).find((k) => prev[k].includes(id));
184
+ const from = inContainer(activeIdStr);
185
+ const to = inContainer(overId);
186
+ if (!from || !to)
187
+ return;
188
+ let next;
189
+ if (from === to) {
190
+ const list = prev[from];
191
+ const oldIndex = list.indexOf(activeIdStr);
192
+ const newIndex = overId.startsWith('g:') ? list.length - 1 : list.indexOf(overId);
193
+ if (oldIndex < 0 || newIndex < 0 || oldIndex === newIndex) {
194
+ commit(prev);
195
+ return;
196
+ }
197
+ next = { ...prev, [from]: arrayMove(list, oldIndex, newIndex) };
198
+ }
199
+ else {
200
+ const fromItems = prev[from].filter((i) => i !== activeIdStr);
201
+ const toItems = prev[to];
202
+ const overIndex = overId.startsWith('g:') ? toItems.length : toItems.indexOf(overId);
203
+ const insertAt = overIndex < 0 ? toItems.length : overIndex;
204
+ next = {
205
+ ...prev,
206
+ [from]: fromItems,
207
+ [to]: [...toItems.slice(0, insertAt), activeIdStr, ...toItems.slice(insertAt)],
208
+ };
209
+ }
210
+ setItems(next);
211
+ commit(next);
212
+ };
213
+ const addSection = () => onChange({ fieldGroups: addGroup(groups, '新分组') });
214
+ const renameSection = (key, label) => onChange({ fieldGroups: renameGroup(groups, key, label) });
215
+ const moveSection = (key, dir) => onChange({ fieldGroups: moveGroup(groups, key, dir) });
216
+ const deleteSection = (key) => onChange({ fieldGroups: removeGroup(groups, key), fields: writeFields(clearFieldGroup(view, key)) });
217
+ const activeEntry = activeId && !activeId.startsWith('g:') ? entryByName.get(unFid(activeId)) : null;
218
+ return (_jsxs("div", { className: "min-h-0 flex-1 overflow-auto rounded-lg border bg-background p-4", children: [_jsxs("div", { className: "mb-3 flex items-center gap-2", children: [_jsxs("span", { className: "inline-flex items-center gap-1 text-[11px] text-muted-foreground", children: [_jsx(Rows3, { className: "h-3.5 w-3.5" }), " \u62D6\u52A8\u5B57\u6BB5\u6392\u5E8F / \u62D6\u5230\u5176\u5B83\u5206\u7EC4 \u00B7 \u70B9\u9009\u5B57\u6BB5\u6539\u5C5E\u6027"] }), _jsxs("button", { type: "button", onClick: addSection, className: "ml-auto inline-flex items-center gap-1 rounded-md border px-2 py-1 text-[11px] text-muted-foreground hover:bg-muted hover:text-foreground", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), " \u6DFB\u52A0\u5206\u7EC4"] }), _jsxs("button", { type: "button", onClick: onAddField, className: "inline-flex items-center gap-1 rounded-md border px-2 py-1 text-[11px] text-muted-foreground hover:bg-muted hover:text-foreground", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), " \u6DFB\u52A0\u5B57\u6BB5"] })] }), _jsxs(DndContext, { sensors: sensors, collisionDetection: pointerWithin, onDragStart: onDragStart, onDragOver: onDragOver, onDragEnd: onDragEnd, onDragCancel: () => setActiveId(null), children: [_jsx("div", { className: "mx-auto flex max-w-3xl flex-col gap-3", children: containerOrder.map((c) => {
219
+ const isUngrouped = c === cid(UNGROUPED);
220
+ // Hide the ungrouped bucket only when it is empty AND groups exist.
221
+ if (isUngrouped && (items[c]?.length ?? 0) === 0 && groups.length > 0)
222
+ return null;
223
+ const declaredIdx = groups.findIndex((g) => cid(g.key) === c);
224
+ return (_jsx(Section, { containerId: c, title: labelOf.get(c) ?? '未分组', fieldIds: items[c] ?? [], isDeclared: !isUngrouped, canMoveUp: declaredIdx > 0, canMoveDown: declaredIdx >= 0 && declaredIdx < groups.length - 1, entryByName: entryByName, selectedField: selectedField, onSelectField: onSelectField, onRename: (label) => renameSection(unCid(c), label), onDelete: () => deleteSection(unCid(c)), onMove: (dir) => moveSection(unCid(c), dir) }, c));
225
+ }) }), _jsx(DragOverlay, { children: activeEntry ? (_jsxs("div", { className: "flex items-center gap-1.5 rounded-md border bg-background px-2 py-2 shadow-lg", children: [_jsx(GripVertical, { className: "h-3.5 w-3.5 text-muted-foreground" }), _jsx("span", { className: "text-xs font-medium", children: String(activeEntry.def.label ?? activeEntry.name) })] })) : null })] })] }));
226
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * Data pillar — object Settings view (builder-ui Phase B).
10
+ *
11
+ * Two stacked cards:
12
+ * 1. Basics — hosts the SAME default inspector metadata-admin uses
13
+ * (`getMetadataDefaultInspector('object')` → ObjectDefaultInspector):
14
+ * label / pluralLabel / icon / description, one implementation for both
15
+ * surfaces.
16
+ * 2. Semantic roles (ADR-0085) — the cross-surface presentation roles:
17
+ * `nameField`, `stageField` (string | false | unset), `highlightFields`.
18
+ * These are the ONLY presentation knobs the protocol carries, so the
19
+ * builder must make them directly editable — otherwise designers fall
20
+ * back to guessing which heuristic picked their title/stepper/columns.
21
+ */
22
+ import React from 'react';
23
+ import type { SupportedLocale } from '../metadata-admin/i18n';
24
+ export declare function ObjectSettingsPanel({ name, draft, onPatch, disabled, locale, }: {
25
+ name: string;
26
+ draft: Record<string, unknown>;
27
+ onPatch: (patch: Record<string, unknown>) => void;
28
+ disabled?: boolean;
29
+ locale: SupportedLocale;
30
+ }): React.JSX.Element;
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ObjectUI
4
+ * Copyright (c) 2024-present ObjectStack Inc.
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ */
9
+ /**
10
+ * Data pillar — object Settings view (builder-ui Phase B).
11
+ *
12
+ * Two stacked cards:
13
+ * 1. Basics — hosts the SAME default inspector metadata-admin uses
14
+ * (`getMetadataDefaultInspector('object')` → ObjectDefaultInspector):
15
+ * label / pluralLabel / icon / description, one implementation for both
16
+ * surfaces.
17
+ * 2. Semantic roles (ADR-0085) — the cross-surface presentation roles:
18
+ * `nameField`, `stageField` (string | false | unset), `highlightFields`.
19
+ * These are the ONLY presentation knobs the protocol carries, so the
20
+ * builder must make them directly editable — otherwise designers fall
21
+ * back to guessing which heuristic picked their title/stepper/columns.
22
+ */
23
+ import React from 'react';
24
+ import { Settings2, Sparkles, X } from 'lucide-react';
25
+ import { getMetadataDefaultInspector } from '../metadata-admin/default-inspector-registry';
26
+ import { readFields } from '../metadata-admin/previews/object-fields-io';
27
+ export function ObjectSettingsPanel({ name, draft, onPatch, disabled, locale, }) {
28
+ const DefaultInspector = getMetadataDefaultInspector('object');
29
+ const fields = React.useMemo(() => readFields(draft.fields).entries, [draft.fields]);
30
+ const selectFields = fields.filter((e) => (e.def.type ?? 'text') === 'select');
31
+ const nameField = typeof draft.nameField === 'string' ? draft.nameField : '';
32
+ const stageField = draft.stageField;
33
+ const highlightFields = Array.isArray(draft.highlightFields)
34
+ ? draft.highlightFields.filter((f) => typeof f === 'string')
35
+ : [];
36
+ const highlightCandidates = fields.filter((e) => e.def.hidden !== true && !highlightFields.includes(e.name));
37
+ return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-4 overflow-auto", children: [_jsxs("section", { className: "rounded-lg border", children: [_jsxs("header", { className: "flex items-center gap-2 border-b px-3 py-2", children: [_jsx(Settings2, { className: "h-3.5 w-3.5" }), _jsx("span", { className: "text-[13px] font-medium", children: "\u57FA\u7840\u4FE1\u606F" })] }), _jsx("div", { className: "max-w-xl p-3", children: DefaultInspector ? (_jsx(DefaultInspector, { type: "object", name: name, draft: draft, onPatch: onPatch, readOnly: !!disabled, locale: locale })) : (_jsx("p", { className: "text-[12px] text-muted-foreground", children: "\u672A\u6CE8\u518C\u5BF9\u8C61\u9ED8\u8BA4\u68C0\u67E5\u5668\u3002" })) })] }), _jsxs("section", { className: "rounded-lg border", children: [_jsxs("header", { className: "flex items-center gap-2 border-b px-3 py-2", children: [_jsx(Sparkles, { className: "h-3.5 w-3.5" }), _jsx("span", { className: "text-[13px] font-medium", children: "\u8BED\u4E49\u89D2\u8272" }), _jsx("span", { className: "text-[11px] text-muted-foreground", children: "\u8DE8\u8868\u5355 / \u5217\u8868 / \u8BE6\u60C5\u7EDF\u4E00\u751F\u6548(ADR-0085)" })] }), _jsxs("div", { className: "grid max-w-xl gap-4 p-3", children: [_jsxs("label", { className: "block", children: [_jsx("span", { className: "mb-1 block text-[11px] text-muted-foreground", children: "\u8BB0\u5F55\u540D\u79F0\u5B57\u6BB5(nameField)\u2014\u2014 \u6807\u9898\u3001\u94FE\u63A5\u3001\u5F15\u7528\u5904\u663E\u793A\u7684\u5B57\u6BB5" }), _jsxs("select", { value: nameField, disabled: disabled, onChange: (e) => onPatch(e.target.value ? { nameField: e.target.value } : { nameField: undefined }), className: "w-full rounded border bg-background px-2 py-1 text-[12px]", children: [_jsx("option", { value: "", children: "(\u81EA\u52A8\u63A8\u5BFC)" }), fields.map((e) => (_jsx("option", { value: e.name, children: typeof e.def.label === 'string' ? `${e.def.label} (${e.name})` : e.name }, e.name)))] })] }), _jsxs("label", { className: "block", children: [_jsx("span", { className: "mb-1 block text-[11px] text-muted-foreground", children: "\u751F\u547D\u5468\u671F\u5B57\u6BB5(stageField)\u2014\u2014 \u8BE6\u60C5\u9875\u9876\u90E8\u8FDB\u5EA6\u6761\u6309\u5B83\u7684\u9009\u9879\u6E32\u67D3" }), _jsxs("select", { value: stageField === false ? '__none__' : (stageField ?? ''), disabled: disabled, onChange: (e) => {
38
+ const v = e.target.value;
39
+ onPatch({ stageField: v === '__none__' ? false : v === '' ? undefined : v });
40
+ }, className: "w-full rounded border bg-background px-2 py-1 text-[12px]", children: [_jsx("option", { value: "", children: "(\u81EA\u52A8\u63A2\u6D4B status/stage \u7B49\u5B57\u6BB5\u540D)" }), _jsx("option", { value: "__none__", children: "\u65E0 \u2014\u2014 \u8FD9\u4E2A\u5BF9\u8C61\u6CA1\u6709\u7EBF\u6027\u6D41\u7A0B,\u4E0D\u663E\u793A\u8FDB\u5EA6\u6761" }), selectFields.map((e) => (_jsx("option", { value: e.name, children: typeof e.def.label === 'string' ? `${e.def.label} (${e.name})` : e.name }, e.name)))] })] }), _jsxs("div", { children: [_jsx("span", { className: "mb-1 block text-[11px] text-muted-foreground", children: "\u91CD\u70B9\u5B57\u6BB5(highlightFields)\u2014\u2014 \u9ED8\u8BA4\u5217\u8868\u5217\u3001\u5361\u7247\u3001\u8BE6\u60C5\u9876\u680F\u53D6\u524D 4;\u987A\u5E8F\u5373\u5C55\u793A\u987A\u5E8F" }), _jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [highlightFields.map((f) => (_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-primary/10 px-2 py-0.5 text-[11px] text-primary", children: [f, !disabled && (_jsx("button", { type: "button", "aria-label": `移除 ${f}`, onClick: () => onPatch({ highlightFields: highlightFields.filter((x) => x !== f) }), className: "rounded-full hover:bg-primary/20", children: _jsx(X, { className: "h-3 w-3" }) }))] }, f))), !disabled && highlightCandidates.length > 0 && (_jsxs("select", { value: "", onChange: (e) => {
41
+ if (!e.target.value)
42
+ return;
43
+ onPatch({ highlightFields: [...highlightFields, e.target.value] });
44
+ }, className: "rounded border bg-background px-1.5 py-0.5 text-[11px] text-muted-foreground", children: [_jsx("option", { value: "", children: "+ \u6DFB\u52A0\u5B57\u6BB5\u2026" }), highlightCandidates.map((e) => (_jsx("option", { value: e.name, children: typeof e.def.label === 'string' ? `${e.def.label} (${e.name})` : e.name }, e.name)))] })), highlightFields.length === 0 && (_jsx("span", { className: "text-[11px] text-muted-foreground", children: "(\u672A\u58F0\u660E \u2014\u2014 \u5404\u5904\u6309\u542F\u53D1\u5F0F\u81EA\u52A8\u6311\u9009)" }))] })] })] })] })] }));
45
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * Data pillar — Validations view (builder-ui Phase B).
10
+ *
11
+ * Edits `ObjectSchema.validations` (spec `ValidationRuleSchema`, a
12
+ * discriminated union on `type`). The no-code surface targets the `script`
13
+ * rule — `{ type: 'script', name, message, condition }` where the CEL
14
+ * `condition` is a FAIL predicate (TRUE ⇒ the write is rejected with
15
+ * `message`). The condition editor reuses the metadata-admin
16
+ * `ConditionBuilder`, fed with the DRAFT field list so unpublished fields
17
+ * are pickable.
18
+ *
19
+ * Non-`script` rule types (state_machine / format / cross_field / json /
20
+ * conditional) are surfaced read-only with their type badge — they carry
21
+ * structures a row editor can't honestly express; authoring them stays in
22
+ * code for now. Showing them (rather than hiding) keeps the list a truthful
23
+ * inventory of everything that will run on save.
24
+ */
25
+ import React from 'react';
26
+ export declare function ObjectValidationsPanel({ draft, onPatch, disabled, }: {
27
+ draft: Record<string, unknown>;
28
+ onPatch: (patch: Record<string, unknown>) => void;
29
+ disabled?: boolean;
30
+ }): React.JSX.Element;
@@ -0,0 +1,78 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ObjectUI
4
+ * Copyright (c) 2024-present ObjectStack Inc.
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ */
9
+ /**
10
+ * Data pillar — Validations view (builder-ui Phase B).
11
+ *
12
+ * Edits `ObjectSchema.validations` (spec `ValidationRuleSchema`, a
13
+ * discriminated union on `type`). The no-code surface targets the `script`
14
+ * rule — `{ type: 'script', name, message, condition }` where the CEL
15
+ * `condition` is a FAIL predicate (TRUE ⇒ the write is rejected with
16
+ * `message`). The condition editor reuses the metadata-admin
17
+ * `ConditionBuilder`, fed with the DRAFT field list so unpublished fields
18
+ * are pickable.
19
+ *
20
+ * Non-`script` rule types (state_machine / format / cross_field / json /
21
+ * conditional) are surfaced read-only with their type badge — they carry
22
+ * structures a row editor can't honestly express; authoring them stays in
23
+ * code for now. Showing them (rather than hiding) keeps the list a truthful
24
+ * inventory of everything that will run on save.
25
+ */
26
+ import React from 'react';
27
+ import { Plus, Trash2, ShieldAlert } from 'lucide-react';
28
+ import { ConditionBuilder } from '../metadata-admin/inspectors/ConditionBuilder';
29
+ import { readFields } from '../metadata-admin/previews/object-fields-io';
30
+ function readRules(input) {
31
+ if (!Array.isArray(input))
32
+ return [];
33
+ return input.filter((r) => !!r && typeof r === 'object');
34
+ }
35
+ function nextRuleName(existing) {
36
+ let i = existing.length + 1;
37
+ let name = `validation_${i}`;
38
+ const taken = new Set(existing);
39
+ while (taken.has(name))
40
+ name = `validation_${++i}`;
41
+ return name;
42
+ }
43
+ export function ObjectValidationsPanel({ draft, onPatch, disabled, }) {
44
+ const rules = readRules(draft.validations);
45
+ const [selected, setSelected] = React.useState(null);
46
+ const fields = React.useMemo(() => readFields(draft.fields).entries.map((e) => ({
47
+ name: e.name,
48
+ label: typeof e.def.label === 'string' ? e.def.label : undefined,
49
+ hidden: e.def.hidden === true,
50
+ })), [draft.fields]);
51
+ const commit = (next) => onPatch({ validations: next });
52
+ const patchRule = (name, patch) => commit(rules.map((r) => (r.name === name ? { ...r, ...patch } : r)));
53
+ const addRule = () => {
54
+ const name = nextRuleName(rules.map((r) => r.name ?? ''));
55
+ // `condition: 'false'` — a VALID never-failing CEL placeholder. An empty
56
+ // condition is rejected by the spec's ExpressionInputSchema, which would
57
+ // 422 the whole draft save and dead-end the create flow (the same
58
+ // required-field-blocks-authoring class as dashboard `layout` / page
59
+ // `regions`). A rule that never fires is safe to save mid-authoring.
60
+ commit([...rules, { type: 'script', name, message: '', condition: 'false', severity: 'error' }]);
61
+ setSelected(name);
62
+ };
63
+ const removeRule = (name) => {
64
+ commit(rules.filter((r) => r.name !== name));
65
+ if (selected === name)
66
+ setSelected(null);
67
+ };
68
+ const sel = rules.find((r) => r.name === selected) ?? null;
69
+ return (_jsxs("div", { className: "flex min-h-0 flex-1 gap-4", children: [_jsxs("div", { className: "flex w-72 shrink-0 flex-col rounded-lg border", children: [_jsxs("header", { className: "flex items-center gap-2 border-b px-3 py-2", children: [_jsx(ShieldAlert, { className: "h-3.5 w-3.5" }), _jsx("span", { className: "text-[13px] font-medium", children: "\u9A8C\u8BC1\u89C4\u5219" }), _jsxs("span", { className: "text-[11px] text-muted-foreground", children: ["(", rules.length, ")"] }), !disabled && (_jsxs("button", { type: "button", onClick: addRule, className: "ml-auto inline-flex items-center gap-1 rounded border px-1.5 py-0.5 text-[11px] hover:bg-muted", children: [_jsx(Plus, { className: "h-3 w-3" }), " \u65B0\u589E"] }))] }), _jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: rules.length === 0 ? (_jsxs("p", { className: "px-3 py-6 text-center text-[11px] leading-5 text-muted-foreground", children: ["\u8FD8\u6CA1\u6709\u9A8C\u8BC1\u89C4\u5219\u3002", _jsx("br", {}), "\u89C4\u5219\u5728\u4FDD\u5B58\u8BB0\u5F55\u65F6\u6267\u884C:\u6761\u4EF6\u4E3A\u771F \u21D2 \u62D2\u7EDD\u4FDD\u5B58\u5E76\u63D0\u793A\u6D88\u606F\u3002"] })) : (rules.map((r) => (_jsxs("button", { type: "button", onClick: () => setSelected(r.name ?? null), className: 'flex w-full items-start gap-2 border-b px-3 py-2 text-left text-[12px] ' +
70
+ (selected === r.name ? 'bg-muted' : 'hover:bg-muted/50'), children: [_jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate font-medium", children: r.label || r.name }), _jsx("span", { className: "block truncate text-[11px] text-muted-foreground", children: r.message || '(无消息)' })] }), _jsx("span", { className: 'shrink-0 rounded-full px-1.5 py-0.5 text-[10px] ' +
71
+ (r.type === 'script'
72
+ ? 'bg-primary/10 text-primary'
73
+ : 'bg-muted text-muted-foreground'), children: r.type ?? 'script' })] }, r.name)))) })] }), _jsx("div", { className: "flex min-w-0 flex-1 flex-col rounded-lg border", children: !sel ? (_jsx("div", { className: "flex flex-1 items-center justify-center p-6 text-center text-[12px] text-muted-foreground", children: "\u9009\u62E9\u5DE6\u4FA7\u7684\u89C4\u5219\u8FDB\u884C\u7F16\u8F91,\u6216\u70B9\u300C\u65B0\u589E\u300D\u521B\u5EFA\u4E00\u6761\u3002" })) : sel.type !== 'script' ? (_jsxs("div", { className: "flex flex-1 flex-col gap-2 p-4", children: [_jsxs("p", { className: "text-[13px] font-medium", children: [sel.label || sel.name, _jsx("span", { className: "ml-2 rounded-full bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground", children: sel.type })] }), _jsxs("p", { className: "text-[12px] leading-5 text-muted-foreground", children: ["\u300C", sel.type, "\u300D\u7C7B\u578B\u7684\u89C4\u5219\u5E26\u6709\u7ED3\u6784\u5316\u914D\u7F6E(\u72B6\u6001\u673A\u8F6C\u79FB\u8868 / \u683C\u5F0F\u7EA6\u675F\u7B49),\u6682\u4E0D\u652F\u6301\u5728\u6B64\u7F16\u8F91 \u2014\u2014 \u8BF7\u5728\u4EE3\u7801\u5305\u4E2D\u7EF4\u62A4\u3002\u6D88\u606F:", sel.message || '(无)'] }), !disabled && (_jsxs("button", { type: "button", onClick: () => removeRule(sel.name), className: "mt-auto inline-flex w-fit items-center gap-1 rounded border border-destructive/40 px-2 py-1 text-[11px] text-destructive hover:bg-destructive/10", children: [_jsx(Trash2, { className: "h-3 w-3" }), " \u5220\u9664\u89C4\u5219"] }))] })) : (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-3 overflow-auto p-4", children: [_jsxs("label", { className: "block", children: [_jsx("span", { className: "mb-1 block text-[11px] text-muted-foreground", children: "\u89C4\u5219\u540D(snake_case,\u552F\u4E00)" }), _jsx("input", { value: sel.name ?? '', disabled: disabled, onChange: (e) => {
74
+ const name = e.target.value;
75
+ patchRule(sel.name, { name });
76
+ setSelected(name);
77
+ }, className: "w-full rounded border bg-background px-2 py-1 text-[12px]" })] }), _jsxs("label", { className: "block", children: [_jsx("span", { className: "mb-1 block text-[11px] text-muted-foreground", children: "\u9519\u8BEF\u6D88\u606F(\u6821\u9A8C\u5931\u8D25\u65F6\u5C55\u793A\u7ED9\u7528\u6237)" }), _jsx("input", { value: sel.message ?? '', disabled: disabled, onChange: (e) => patchRule(sel.name, { message: e.target.value }), placeholder: "\u4F8B\u5982:\u5B8C\u6210\u65E5\u671F\u5728\u72B6\u6001\u4E3A\u5DF2\u5B8C\u6210\u65F6\u5FC5\u586B", className: "w-full rounded border bg-background px-2 py-1 text-[12px]" })] }), _jsxs("div", { children: [_jsxs("span", { className: "mb-1 block text-[11px] text-muted-foreground", children: ["\u5931\u8D25\u6761\u4EF6(CEL)\u2014\u2014 \u6761\u4EF6\u4E3A ", _jsx("b", { children: "\u771F" }), " \u65F6,\u62D2\u7EDD\u4FDD\u5B58\u5E76\u663E\u793A\u4E0A\u9762\u7684\u6D88\u606F;\u65B0\u89C4\u5219\u9ED8\u8BA4", ' ', _jsx("code", { className: "rounded bg-muted px-1", children: "false" }), "(\u6C38\u4E0D\u89E6\u53D1),\u8BF7\u6539\u4E3A\u771F\u5B9E\u6761\u4EF6"] }), _jsx(ConditionBuilder, { value: typeof sel.condition === 'string' ? sel.condition : '', onCommit: (cel) => patchRule(sel.name, { condition: cel }), fields: fields, disabled: disabled })] }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsxs("label", { className: "flex items-center gap-1.5 text-[12px]", children: [_jsx("span", { className: "text-muted-foreground", children: "\u4E25\u91CD\u5EA6" }), _jsxs("select", { value: sel.severity ?? 'error', disabled: disabled, onChange: (e) => patchRule(sel.name, { severity: e.target.value }), className: "rounded border bg-background px-1.5 py-0.5 text-[12px]", children: [_jsx("option", { value: "error", children: "error(\u62D2\u7EDD\u4FDD\u5B58)" }), _jsx("option", { value: "warning", children: "warning" }), _jsx("option", { value: "info", children: "info" })] })] }), _jsxs("label", { className: "flex items-center gap-1.5 text-[12px]", children: [_jsx("input", { type: "checkbox", checked: sel.active !== false, disabled: disabled, onChange: (e) => patchRule(sel.name, { active: e.target.checked }) }), "\u542F\u7528"] }), !disabled && (_jsxs("button", { type: "button", onClick: () => removeRule(sel.name), className: "ml-auto inline-flex items-center gap-1 rounded border border-destructive/40 px-2 py-1 text-[11px] text-destructive hover:bg-destructive/10", children: [_jsx(Trash2, { className: "h-3 w-3" }), " \u5220\u9664"] }))] })] })) })] }));
78
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * StudioDesignSurface — the open-source WYSIWYG design surface (ADR-0080).
3
+ *
4
+ * Routed as /studio/:packageId/{data|automations|interfaces} — three pillars,
5
+ * each composed AROUND existing renderers (no new editor code):
6
+ * - Interfaces: the real App navigation tree → live canvas (getMetadataPreview)
7
+ * + inspector (getMetadataInspector), edits persisting via draft → publish.
8
+ * - Data: the package's objects → fields + record grid.
9
+ * - Automations: flows → FlowPreview (default OFF / review-then-enable).
10
+ *
11
+ * Open-core boundary: the left AI copilot is NOT part of the open-source
12
+ * surface — it is an injected slot (`aiSlot`) the cloud edition fills.
13
+ */
14
+ import * as React from 'react';
15
+ export interface StudioDesignSurfaceProps {
16
+ /** Open-core slot — the cloud edition injects its AI copilot panel here. */
17
+ aiSlot?: React.ReactNode;
18
+ }
19
+ export declare function StudioDesignSurface({ aiSlot }: StudioDesignSurfaceProps): React.ReactElement;
20
+ export default StudioDesignSurface;