@object-ui/app-shell 11.3.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 (50) hide show
  1. package/CHANGELOG.md +522 -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 +1 -0
  11. package/dist/index.js +3 -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/resolveViewId.d.ts +23 -0
  17. package/dist/utils/resolveViewId.js +37 -0
  18. package/dist/utils/warnSuppressedListNav.d.ts +10 -0
  19. package/dist/utils/warnSuppressedListNav.js +40 -0
  20. package/dist/views/InterfaceListPage.js +6 -4
  21. package/dist/views/ObjectView.js +61 -10
  22. package/dist/views/RecordDetailView.js +131 -104
  23. package/dist/views/RecordFormPage.js +7 -1
  24. package/dist/views/RelatedRecordActionsBridge.d.ts +24 -0
  25. package/dist/views/RelatedRecordActionsBridge.js +114 -0
  26. package/dist/views/metadata-admin/PackagesPage.js +18 -7
  27. package/dist/views/metadata-admin/PermissionMatrixEditor.d.ts +18 -1
  28. package/dist/views/metadata-admin/PermissionMatrixEditor.js +73 -14
  29. package/dist/views/metadata-admin/i18n.d.ts +12 -21
  30. package/dist/views/metadata-admin/i18n.js +343 -2
  31. package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +25 -11
  32. package/dist/views/metadata-admin/permission-slice.d.ts +66 -0
  33. package/dist/views/metadata-admin/permission-slice.js +70 -0
  34. package/dist/views/metadata-admin/previews/AppNavCanvas.js +11 -7
  35. package/dist/views/metadata-admin/previews/FlowRunsPanel.d.ts +16 -7
  36. package/dist/views/metadata-admin/previews/FlowRunsPanel.js +18 -2
  37. package/dist/views/studio-design/BuilderLanding.d.ts +15 -0
  38. package/dist/views/studio-design/BuilderLanding.js +133 -0
  39. package/dist/views/studio-design/ObjectFormDesigner.d.ts +31 -0
  40. package/dist/views/studio-design/ObjectFormDesigner.js +226 -0
  41. package/dist/views/studio-design/ObjectSettingsPanel.d.ts +30 -0
  42. package/dist/views/studio-design/ObjectSettingsPanel.js +45 -0
  43. package/dist/views/studio-design/ObjectValidationsPanel.d.ts +30 -0
  44. package/dist/views/studio-design/ObjectValidationsPanel.js +78 -0
  45. package/dist/views/studio-design/StudioDesignSurface.js +793 -146
  46. package/dist/views/studio-design/metadataError.d.ts +23 -0
  47. package/dist/views/studio-design/metadataError.js +44 -0
  48. package/dist/views/studio-design/packages-io.d.ts +27 -0
  49. package/dist/views/studio-design/packages-io.js +61 -0
  50. package/package.json +42 -39
@@ -10,16 +10,20 @@
10
10
  * designer.
11
11
  */
12
12
  import * as React from 'react';
13
+ /** An error on a run/step. The engine sends the run-level `error` as a plain
14
+ * string (`ExecutionLog.error`) while a step-level error is a `{code,message}`
15
+ * object — the panel accepts either shape. */
16
+ type RunError = string | {
17
+ code?: string;
18
+ message?: string;
19
+ };
13
20
  /** Step entry of a run log (spec `ExecutionStepLogSchema`, fields we render). */
14
21
  interface RunStep {
15
22
  nodeId: string;
16
23
  nodeType?: string;
17
24
  status: 'success' | 'failure' | 'skipped' | string;
18
25
  durationMs?: number;
19
- error?: {
20
- code?: string;
21
- message?: string;
22
- };
26
+ error?: RunError;
23
27
  }
24
28
  /** Run log entry (spec `ExecutionLogSchema`, fields we render). */
25
29
  export interface FlowRun {
@@ -34,10 +38,15 @@ export interface FlowRun {
34
38
  object?: string;
35
39
  };
36
40
  steps?: RunStep[];
37
- error?: {
38
- message?: string;
39
- };
41
+ error?: RunError;
40
42
  }
43
+ /**
44
+ * Normalize a run/step error to its human-readable message. The engine emits a
45
+ * run-level `error` as a plain string but a step-level error as `{code,message}`
46
+ * — reading `.message` off the string case silently dropped the run failure
47
+ * reason (the whole point of the Runs panel for a failed run), so accept both.
48
+ */
49
+ export declare function errorText(e: RunError | undefined | null): string | undefined;
41
50
  /** Fetch a flow's run history. Exposed for tests. */
42
51
  export declare function fetchFlowRuns(flowName: string, signal?: AbortSignal): Promise<FlowRun[] | null>;
43
52
  export declare function FlowRunsPanel({ flowName }: {
@@ -15,6 +15,20 @@ import * as React from 'react';
15
15
  import { AlertCircle, CheckCircle2, ChevronDown, ChevronRight, Clock, Loader2, PauseCircle, RefreshCw, SkipForward } from 'lucide-react';
16
16
  import { cn } from '@object-ui/components';
17
17
  import { apiBase } from './useFlowNodePalette';
18
+ /**
19
+ * Normalize a run/step error to its human-readable message. The engine emits a
20
+ * run-level `error` as a plain string but a step-level error as `{code,message}`
21
+ * — reading `.message` off the string case silently dropped the run failure
22
+ * reason (the whole point of the Runs panel for a failed run), so accept both.
23
+ */
24
+ export function errorText(e) {
25
+ if (!e)
26
+ return undefined;
27
+ if (typeof e === 'string')
28
+ return e || undefined;
29
+ const m = e.message;
30
+ return typeof m === 'string' && m ? m : undefined;
31
+ }
18
32
  /** Fetch a flow's run history. Exposed for tests. */
19
33
  export async function fetchFlowRuns(flowName, signal) {
20
34
  try {
@@ -60,14 +74,16 @@ function StepRow({ step }) {
60
74
  : step.status === 'failure'
61
75
  ? 'text-rose-600 dark:text-rose-400'
62
76
  : 'text-muted-foreground';
63
- return (_jsxs("li", { className: "flex items-baseline gap-1.5 py-0.5", children: [_jsx("span", { className: cn('shrink-0 text-[9px] font-semibold uppercase', cls), children: step.status }), _jsx("span", { className: "truncate font-mono text-[10px]", title: step.nodeId, children: step.nodeId }), step.nodeType && _jsx("span", { className: "shrink-0 text-[9px] uppercase text-muted-foreground", children: step.nodeType }), fmtDuration(step.durationMs) && (_jsx("span", { className: "ml-auto shrink-0 text-[9px] text-muted-foreground", children: fmtDuration(step.durationMs) })), step.error?.message && (_jsx("span", { className: "min-w-0 truncate text-[9px] text-rose-600", title: step.error.message, children: step.error.message }))] }));
77
+ const stepErr = errorText(step.error);
78
+ return (_jsxs("li", { className: "flex items-baseline gap-1.5 py-0.5", children: [_jsx("span", { className: cn('shrink-0 text-[9px] font-semibold uppercase', cls), children: step.status }), _jsx("span", { className: "truncate font-mono text-[10px]", title: step.nodeId, children: step.nodeId }), step.nodeType && _jsx("span", { className: "shrink-0 text-[9px] uppercase text-muted-foreground", children: step.nodeType }), fmtDuration(step.durationMs) && (_jsx("span", { className: "ml-auto shrink-0 text-[9px] text-muted-foreground", children: fmtDuration(step.durationMs) })), stepErr && (_jsx("span", { className: "min-w-0 truncate text-[9px] text-rose-600", title: stepErr, children: stepErr }))] }));
64
79
  }
65
80
  function RunRow({ run }) {
66
81
  const [open, setOpen] = React.useState(false);
67
82
  const meta = statusMeta(run.status);
68
83
  const Icon = meta.icon;
69
84
  const steps = Array.isArray(run.steps) ? run.steps : [];
70
- return (_jsxs("li", { className: "rounded border bg-background", children: [_jsxs("button", { type: "button", onClick: () => setOpen((v) => !v), className: "flex w-full items-center gap-1.5 p-1.5 text-left", "aria-expanded": open, children: [open ? (_jsx(ChevronDown, { className: "h-3 w-3 shrink-0 text-muted-foreground" })) : (_jsx(ChevronRight, { className: "h-3 w-3 shrink-0 text-muted-foreground" })), _jsx(Icon, { className: cn('h-3.5 w-3.5 shrink-0', meta.cls, run.status === 'running' && 'animate-spin') }), _jsx("span", { className: cn('shrink-0 text-[10px] font-semibold', meta.cls), children: meta.label }), _jsx("span", { className: "min-w-0 truncate text-[10px] text-muted-foreground", title: run.id, children: fmtTime(run.startedAt) }), fmtDuration(run.durationMs) && (_jsx("span", { className: "ml-auto shrink-0 text-[10px] text-muted-foreground", children: fmtDuration(run.durationMs) }))] }), open && (_jsxs("div", { className: "border-t px-2 py-1.5", children: [_jsxs("div", { className: "pb-1 font-mono text-[9px] text-muted-foreground", title: run.id, children: ["run ", run.id, run.trigger?.type && ` · trigger ${run.trigger.type}`] }), run.error?.message && (_jsx("div", { className: "pb-1 text-[10px] text-rose-600", children: run.error.message })), steps.length === 0 ? (_jsx("div", { className: "text-[10px] italic text-muted-foreground", children: "No step log recorded." })) : (_jsx("ul", { className: "divide-y divide-border/50", children: steps.map((s, i) => (_jsx(StepRow, { step: s }, `${s.nodeId}#${i}`))) }))] }))] }));
85
+ const runErr = errorText(run.error);
86
+ return (_jsxs("li", { className: "rounded border bg-background", children: [_jsxs("button", { type: "button", onClick: () => setOpen((v) => !v), className: "flex w-full items-center gap-1.5 p-1.5 text-left", "aria-expanded": open, children: [open ? (_jsx(ChevronDown, { className: "h-3 w-3 shrink-0 text-muted-foreground" })) : (_jsx(ChevronRight, { className: "h-3 w-3 shrink-0 text-muted-foreground" })), _jsx(Icon, { className: cn('h-3.5 w-3.5 shrink-0', meta.cls, run.status === 'running' && 'animate-spin') }), _jsx("span", { className: cn('shrink-0 text-[10px] font-semibold', meta.cls), children: meta.label }), _jsx("span", { className: "min-w-0 truncate text-[10px] text-muted-foreground", title: run.id, children: fmtTime(run.startedAt) }), fmtDuration(run.durationMs) && (_jsx("span", { className: "ml-auto shrink-0 text-[10px] text-muted-foreground", children: fmtDuration(run.durationMs) }))] }), open && (_jsxs("div", { className: "border-t px-2 py-1.5", children: [_jsxs("div", { className: "pb-1 font-mono text-[9px] text-muted-foreground", title: run.id, children: ["run ", run.id, run.trigger?.type && ` · trigger ${run.trigger.type}`] }), runErr && (_jsx("div", { className: "pb-1 text-[10px] text-rose-600", children: runErr })), steps.length === 0 ? (_jsx("div", { className: "text-[10px] italic text-muted-foreground", children: "No step log recorded." })) : (_jsx("ul", { className: "divide-y divide-border/50", children: steps.map((s, i) => (_jsx(StepRow, { step: s }, `${s.nodeId}#${i}`))) }))] }))] }));
71
87
  }
72
88
  export function FlowRunsPanel({ flowName }) {
73
89
  const [runs, setRuns] = React.useState([]);
@@ -0,0 +1,15 @@
1
+ /**
2
+ * BuilderLanding — the application builder's front door.
3
+ *
4
+ * The journey from login: Home → Studio app → 「应用构建」 (this page, embedded
5
+ * in the app chrome via the `studio:builder` component ref) → pick or create a
6
+ * writable base package → the full-screen pillar builder
7
+ * (`/studio/:packageId/:tab`). Also served standalone at bare `/studio` so the
8
+ * builder is bookmarkable.
9
+ *
10
+ * Writable bases (where authoring happens) lead; read-only code packages are
11
+ * listed secondary for browsing. Writability is the shared display heuristic
12
+ * from packages-io — the ADR-0070 D4 gate stays the server-side authority.
13
+ */
14
+ import * as React from 'react';
15
+ export declare function BuilderLanding(): React.ReactElement;
@@ -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;