@object-ui/app-shell 7.0.0 → 7.2.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.
- package/CHANGELOG.md +560 -0
- package/dist/console/AppContent.js +23 -17
- package/dist/console/ConsoleShell.d.ts +16 -0
- package/dist/console/ConsoleShell.js +43 -2
- package/dist/console/ai/AiChatPage.js +47 -16
- package/dist/console/ai/LiveCanvas.d.ts +8 -2
- package/dist/console/ai/LiveCanvas.js +6 -4
- package/dist/console/home/HomeLayout.js +5 -7
- package/dist/console/home/HomePage.js +1 -9
- package/dist/console/organizations/CreateWorkspaceDialog.js +15 -1
- package/dist/console/organizations/OrganizationsPage.js +22 -3
- package/dist/console/organizations/provisionEnvironment.d.ts +53 -0
- package/dist/console/organizations/provisionEnvironment.js +64 -0
- package/dist/environment/EnvironmentEntitlementDialog.d.ts +34 -0
- package/dist/environment/EnvironmentEntitlementDialog.js +37 -0
- package/dist/environment/EnvironmentListToolbar.d.ts +33 -0
- package/dist/environment/EnvironmentListToolbar.js +59 -0
- package/dist/environment/entitlements.d.ts +90 -0
- package/dist/environment/entitlements.js +91 -0
- package/dist/environment/useEnvironmentEntitlements.d.ts +32 -0
- package/dist/environment/useEnvironmentEntitlements.js +108 -0
- package/dist/hooks/useActionModal.js +15 -1
- package/dist/hooks/useAiSurface.d.ts +59 -0
- package/dist/hooks/useAiSurface.js +78 -0
- package/dist/hooks/useChatConversation.d.ts +30 -0
- package/dist/hooks/useChatConversation.js +63 -0
- package/dist/hooks/useConsoleActionRuntime.d.ts +3 -0
- package/dist/hooks/useConsoleActionRuntime.js +42 -10
- package/dist/index.d.ts +5 -2
- package/dist/index.js +10 -2
- package/dist/layout/AppHeader.js +28 -4
- package/dist/layout/ConsoleFloatingChatbot.d.ts +6 -4
- package/dist/layout/ConsoleFloatingChatbot.js +41 -10
- package/dist/layout/ConsoleLayout.js +5 -6
- package/dist/layout/ContextSelectors.js +59 -35
- package/dist/layout/agentPicker.d.ts +56 -0
- package/dist/layout/agentPicker.js +40 -0
- package/dist/preview/CommitTimeline.d.ts +15 -0
- package/dist/preview/CommitTimeline.js +82 -0
- package/dist/preview/DraftPreviewBar.js +20 -7
- package/dist/preview/UnpublishedAppBar.js +11 -7
- package/dist/preview/commitHistory.d.ts +28 -0
- package/dist/preview/commitHistory.js +48 -0
- package/dist/providers/ExpressionProvider.js +9 -3
- package/dist/providers/MetadataProvider.js +9 -0
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.js +1 -1
- package/dist/utils/recordFormNavigation.d.ts +60 -0
- package/dist/utils/recordFormNavigation.js +35 -0
- package/dist/utils/resolvePageVarTokens.d.ts +31 -0
- package/dist/utils/resolvePageVarTokens.js +72 -0
- package/dist/views/CreateViewDialog.js +14 -1
- package/dist/views/FlowRunner.d.ts +2 -30
- package/dist/views/FlowRunner.js +18 -50
- package/dist/views/ObjectView.js +26 -12
- package/dist/views/ScreenView.d.ts +70 -0
- package/dist/views/ScreenView.js +73 -0
- package/dist/views/metadata-admin/AssignedUsersSection.d.ts +28 -0
- package/dist/views/metadata-admin/AssignedUsersSection.js +151 -0
- package/dist/views/metadata-admin/DirectoryPage.js +2 -14
- package/dist/views/metadata-admin/JsonSourceEditor.d.ts +3 -1
- package/dist/views/metadata-admin/JsonSourceEditor.js +21 -3
- package/dist/views/metadata-admin/PackagesPage.d.ts +5 -0
- package/dist/views/metadata-admin/PackagesPage.js +58 -5
- package/dist/views/metadata-admin/PermissionMatrixEditor.js +2 -1
- package/dist/views/metadata-admin/ResourceEditPage.js +83 -24
- package/dist/views/metadata-admin/ResourceListPage.js +28 -19
- package/dist/views/metadata-admin/StudioHomePage.js +6 -14
- package/dist/views/metadata-admin/anchors.js +20 -2
- package/dist/views/metadata-admin/createBody.d.ts +26 -0
- package/dist/views/metadata-admin/createBody.js +30 -0
- package/dist/views/metadata-admin/i18n.js +108 -2
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +10 -2
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +136 -8
- package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +99 -4
- package/dist/views/metadata-admin/inspectors/FlowExprIssue.d.ts +21 -0
- package/dist/views/metadata-admin/inspectors/FlowExprIssue.js +13 -0
- package/dist/views/metadata-admin/inspectors/FlowKeyValueField.d.ts +20 -2
- package/dist/views/metadata-admin/inspectors/FlowKeyValueField.js +71 -28
- package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.d.ts +4 -1
- package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.js +24 -9
- package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +81 -4
- package/dist/views/metadata-admin/inspectors/FlowObjectListField.d.ts +4 -1
- package/dist/views/metadata-admin/inspectors/FlowObjectListField.js +8 -3
- package/dist/views/metadata-admin/inspectors/ObjectDefaultInspector.js +5 -4
- package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +47 -12
- package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.d.ts +1 -1
- package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.js +60 -2
- package/dist/views/metadata-admin/inspectors/VariableTextInput.d.ts +47 -0
- package/dist/views/metadata-admin/inspectors/VariableTextInput.js +95 -0
- package/dist/views/metadata-admin/inspectors/_shared.d.ts +5 -1
- package/dist/views/metadata-admin/inspectors/_shared.js +2 -2
- package/dist/views/metadata-admin/inspectors/datasetFilterCondition.d.ts +24 -0
- package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +102 -0
- package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +16 -1
- package/dist/views/metadata-admin/inspectors/flow-node-config.js +67 -11
- package/dist/views/metadata-admin/inspectors/flow-ref-check.d.ts +39 -0
- package/dist/views/metadata-admin/inspectors/flow-ref-check.js +114 -0
- package/dist/views/metadata-admin/inspectors/flow-scope.d.ts +109 -0
- package/dist/views/metadata-admin/inspectors/flow-scope.js +199 -0
- package/dist/views/metadata-admin/inspectors/useDatasetFields.d.ts +14 -3
- package/dist/views/metadata-admin/inspectors/useDatasetFields.js +0 -0
- package/dist/views/metadata-admin/inspectors/useFlowScope.d.ts +23 -0
- package/dist/views/metadata-admin/inspectors/useFlowScope.js +45 -0
- package/dist/views/metadata-admin/issuePath.d.ts +22 -0
- package/dist/views/metadata-admin/issuePath.js +65 -0
- package/dist/views/metadata-admin/package-scope.d.ts +41 -0
- package/dist/views/metadata-admin/package-scope.js +59 -0
- package/dist/views/metadata-admin/preview-registry.d.ts +12 -0
- package/dist/views/metadata-admin/previews/DatasetPreview.js +21 -5
- package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +26 -1
- package/dist/views/metadata-admin/previews/FlowCanvas.js +143 -16
- package/dist/views/metadata-admin/previews/FlowPreview.d.ts +1 -1
- package/dist/views/metadata-admin/previews/FlowPreview.js +47 -7
- package/dist/views/metadata-admin/previews/FlowSimulatorPanel.js +37 -3
- package/dist/views/metadata-admin/previews/ObjectFormCanvas.js +9 -4
- package/dist/views/metadata-admin/previews/PagePreview.js +112 -3
- package/dist/views/metadata-admin/previews/ProblemsPanel.d.ts +18 -0
- package/dist/views/metadata-admin/previews/ProblemsPanel.js +27 -0
- package/dist/views/metadata-admin/previews/ReportPreview.d.ts +9 -8
- package/dist/views/metadata-admin/previews/ReportPreview.js +33 -16
- package/dist/views/metadata-admin/previews/ScreenPreview.d.ts +38 -0
- package/dist/views/metadata-admin/previews/ScreenPreview.js +61 -0
- package/dist/views/metadata-admin/previews/flow-canvas-layout.d.ts +14 -0
- package/dist/views/metadata-admin/previews/flow-canvas-layout.js +37 -0
- package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +17 -1
- package/dist/views/metadata-admin/previews/flow-canvas-parts.js +23 -6
- package/dist/views/metadata-admin/previews/flow-expr-problems.d.ts +19 -0
- package/dist/views/metadata-admin/previews/flow-expr-problems.js +97 -0
- package/dist/views/metadata-admin/previews/flow-problems.d.ts +84 -0
- package/dist/views/metadata-admin/previews/flow-problems.js +209 -0
- package/dist/views/metadata-admin/previews/object-fields-io.d.ts +21 -0
- package/dist/views/metadata-admin/previews/object-fields-io.js +37 -2
- package/dist/views/metadata-admin/previews/screen-spec.d.ts +43 -0
- package/dist/views/metadata-admin/previews/screen-spec.js +108 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +20 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.d.ts +7 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +76 -2
- package/dist/views/metadata-admin/previews/simulator/flow-simulator.d.ts +32 -3
- package/dist/views/metadata-admin/previews/simulator/flow-simulator.js +119 -9
- package/package.json +38 -38
|
@@ -55,6 +55,66 @@ export declare function resolveRecordFormTarget(opts: {
|
|
|
55
55
|
_id?: string | number;
|
|
56
56
|
} | null | undefined;
|
|
57
57
|
}): RecordFormTarget;
|
|
58
|
+
/**
|
|
59
|
+
* Subset of the default form-view shape consumed by
|
|
60
|
+
* {@link resolveFormViewLayout}. This is the flattened `config` body of the
|
|
61
|
+
* object's default `viewKind: 'form'` ViewItem (ADR-0017), merged onto the
|
|
62
|
+
* object by `MetadataProvider` as `objectDef.form` (and mirrored under
|
|
63
|
+
* `objectDef.formViews.default` for the legacy aggregated-container shape).
|
|
64
|
+
*/
|
|
65
|
+
export interface FormViewDefinition {
|
|
66
|
+
/**
|
|
67
|
+
* Layout family declared by the form view (`simple` | `tabbed` | `wizard`
|
|
68
|
+
* | `split`). Only `tabbed` changes the *modal's* internal layout; the
|
|
69
|
+
* others still render their curated sections, just stacked.
|
|
70
|
+
*/
|
|
71
|
+
type?: string;
|
|
72
|
+
/** Curated field sections — the selection, order, and grouping to render. */
|
|
73
|
+
sections?: any[];
|
|
74
|
+
/** Inline child collections (master-detail). */
|
|
75
|
+
subforms?: any[];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Object metadata subset carrying the default form view, as merged onto the
|
|
79
|
+
* runtime objects list (`useMetadata().objects`).
|
|
80
|
+
*/
|
|
81
|
+
export interface ObjectDefinitionForFormView {
|
|
82
|
+
form?: FormViewDefinition | null;
|
|
83
|
+
formViews?: {
|
|
84
|
+
default?: FormViewDefinition | null;
|
|
85
|
+
} | null;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Layout props derived from an object's default form view, ready to spread
|
|
89
|
+
* into a `<ModalForm>` schema.
|
|
90
|
+
*/
|
|
91
|
+
export interface FormViewModalLayout {
|
|
92
|
+
/** Curated sections to render (omitted when the form view declares none). */
|
|
93
|
+
sections?: any[];
|
|
94
|
+
/** `'tabbed'` when the form view is tabbed; omitted otherwise (stacked). */
|
|
95
|
+
contentLayout?: 'tabbed';
|
|
96
|
+
/** Inline child collections for a master-detail modal. */
|
|
97
|
+
subforms?: any[];
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Resolve a `<ModalForm>`'s layout props from an object's DEFAULT FORM VIEW
|
|
101
|
+
* (curated sections + field selection/order, plus master-detail subforms).
|
|
102
|
+
*
|
|
103
|
+
* The create / edit record modal otherwise falls back to the raw object
|
|
104
|
+
* schema — rendering every field in schema order and ignoring the curated
|
|
105
|
+
* form view entirely. This resolver lets the New/Edit modal honor the same
|
|
106
|
+
* view-driven layout the full-screen record page (`RecordFormPage`) does.
|
|
107
|
+
*
|
|
108
|
+
* Resolution mirrors `RecordFormPage`: prefer `objectDef.form` (the default
|
|
109
|
+
* ViewItem) and fall back to `objectDef.formViews.default` (legacy container).
|
|
110
|
+
*
|
|
111
|
+
* Returns an EMPTY object when the object has no form view, or a form view
|
|
112
|
+
* that declares no sections — the caller then keeps its existing behavior
|
|
113
|
+
* (a flat field list, i.e. the raw object schema). Empty `sections` /
|
|
114
|
+
* `subforms` arrays are treated as absent so an empty curation never blanks
|
|
115
|
+
* out the form.
|
|
116
|
+
*/
|
|
117
|
+
export declare function resolveFormViewLayout(objectDef: ObjectDefinitionForFormView | null | undefined): FormViewModalLayout;
|
|
58
118
|
/**
|
|
59
119
|
* Action descriptor accepted by the navigate-create / navigate-edit
|
|
60
120
|
* handlers. Loose-typed because the same shape is constructed dynamically
|
|
@@ -41,6 +41,41 @@ export function resolveRecordFormTarget(opts) {
|
|
|
41
41
|
}
|
|
42
42
|
return { kind: 'page', url: `${baseUrl}/${objectDef.name}/new` };
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Resolve a `<ModalForm>`'s layout props from an object's DEFAULT FORM VIEW
|
|
46
|
+
* (curated sections + field selection/order, plus master-detail subforms).
|
|
47
|
+
*
|
|
48
|
+
* The create / edit record modal otherwise falls back to the raw object
|
|
49
|
+
* schema — rendering every field in schema order and ignoring the curated
|
|
50
|
+
* form view entirely. This resolver lets the New/Edit modal honor the same
|
|
51
|
+
* view-driven layout the full-screen record page (`RecordFormPage`) does.
|
|
52
|
+
*
|
|
53
|
+
* Resolution mirrors `RecordFormPage`: prefer `objectDef.form` (the default
|
|
54
|
+
* ViewItem) and fall back to `objectDef.formViews.default` (legacy container).
|
|
55
|
+
*
|
|
56
|
+
* Returns an EMPTY object when the object has no form view, or a form view
|
|
57
|
+
* that declares no sections — the caller then keeps its existing behavior
|
|
58
|
+
* (a flat field list, i.e. the raw object schema). Empty `sections` /
|
|
59
|
+
* `subforms` arrays are treated as absent so an empty curation never blanks
|
|
60
|
+
* out the form.
|
|
61
|
+
*/
|
|
62
|
+
export function resolveFormViewLayout(objectDef) {
|
|
63
|
+
const formView = objectDef?.form ?? objectDef?.formViews?.default;
|
|
64
|
+
if (!formView)
|
|
65
|
+
return {};
|
|
66
|
+
const layout = {};
|
|
67
|
+
if (Array.isArray(formView.sections) && formView.sections.length > 0) {
|
|
68
|
+
layout.sections = formView.sections;
|
|
69
|
+
// Only 'tabbed' maps to a modal content layout; 'wizard'/'split' have no
|
|
70
|
+
// modal equivalent and degrade to a stacked section list.
|
|
71
|
+
if (formView.type === 'tabbed')
|
|
72
|
+
layout.contentLayout = 'tabbed';
|
|
73
|
+
}
|
|
74
|
+
if (Array.isArray(formView.subforms) && formView.subforms.length > 0) {
|
|
75
|
+
layout.subforms = formView.subforms;
|
|
76
|
+
}
|
|
77
|
+
return layout;
|
|
78
|
+
}
|
|
44
79
|
/**
|
|
45
80
|
* Resolve the URL for a `navigate_create` action.
|
|
46
81
|
*
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
* resolvePageVarTokens — resolve `{{page.<path>}}` tokens against a page-variable
|
|
9
|
+
* snapshot. The data-entry bridge for SDUI forms: an interactive input
|
|
10
|
+
* (`element:text_input`, `element:record_picker`) writes a page variable; a
|
|
11
|
+
* submit action references it in its params/body as `{{page.<var>}}`; this
|
|
12
|
+
* resolves those tokens against the live snapshot — published into the action
|
|
13
|
+
* context by `PageVariableActionBridge` — just before the request body is built.
|
|
14
|
+
*
|
|
15
|
+
* - A WHOLE-VALUE token (`"{{page.amount}}"`) is replaced by the variable's RAW
|
|
16
|
+
* value, preserving its type — a number stays a number, an object stays an
|
|
17
|
+
* object — so numeric/boolean/array form fields submit with the right JSON
|
|
18
|
+
* type rather than being stringified.
|
|
19
|
+
* - An EMBEDDED token (`"/orgs/{{page.slug}}/setup"`) is string-interpolated.
|
|
20
|
+
* - Resolution walks nested objects/arrays. A whole-value miss resolves to ''
|
|
21
|
+
* (kept as a present-but-empty field); an embedded miss drops to ''.
|
|
22
|
+
*
|
|
23
|
+
* Distinct from the single-brace `{field}` row-record interpolation used in API
|
|
24
|
+
* target URLs (different brace count, different source) so the two never collide.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Deep-resolve every `{{page.<var>}}` token in `value` against `pageVariables`.
|
|
28
|
+
* Returns `value` unchanged when there is no snapshot. Non-string leaves pass
|
|
29
|
+
* through untouched; the input is never mutated (objects/arrays are copied).
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolvePageVarTokens<T>(value: T, pageVariables?: Record<string, any> | null): T;
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
* resolvePageVarTokens — resolve `{{page.<path>}}` tokens against a page-variable
|
|
9
|
+
* snapshot. The data-entry bridge for SDUI forms: an interactive input
|
|
10
|
+
* (`element:text_input`, `element:record_picker`) writes a page variable; a
|
|
11
|
+
* submit action references it in its params/body as `{{page.<var>}}`; this
|
|
12
|
+
* resolves those tokens against the live snapshot — published into the action
|
|
13
|
+
* context by `PageVariableActionBridge` — just before the request body is built.
|
|
14
|
+
*
|
|
15
|
+
* - A WHOLE-VALUE token (`"{{page.amount}}"`) is replaced by the variable's RAW
|
|
16
|
+
* value, preserving its type — a number stays a number, an object stays an
|
|
17
|
+
* object — so numeric/boolean/array form fields submit with the right JSON
|
|
18
|
+
* type rather than being stringified.
|
|
19
|
+
* - An EMBEDDED token (`"/orgs/{{page.slug}}/setup"`) is string-interpolated.
|
|
20
|
+
* - Resolution walks nested objects/arrays. A whole-value miss resolves to ''
|
|
21
|
+
* (kept as a present-but-empty field); an embedded miss drops to ''.
|
|
22
|
+
*
|
|
23
|
+
* Distinct from the single-brace `{field}` row-record interpolation used in API
|
|
24
|
+
* target URLs (different brace count, different source) so the two never collide.
|
|
25
|
+
*/
|
|
26
|
+
const WHOLE_RE = /^\s*\{\{\s*page\.([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\s*\}\}\s*$/;
|
|
27
|
+
function lookup(path, vars) {
|
|
28
|
+
let node = vars;
|
|
29
|
+
for (const seg of path.split('.')) {
|
|
30
|
+
if (node == null)
|
|
31
|
+
return undefined;
|
|
32
|
+
node = node[seg];
|
|
33
|
+
}
|
|
34
|
+
return node;
|
|
35
|
+
}
|
|
36
|
+
function resolveString(str, vars) {
|
|
37
|
+
if (!str.includes('{{'))
|
|
38
|
+
return str;
|
|
39
|
+
const whole = str.match(WHOLE_RE);
|
|
40
|
+
if (whole) {
|
|
41
|
+
const v = lookup(whole[1], vars);
|
|
42
|
+
return v === undefined ? '' : v;
|
|
43
|
+
}
|
|
44
|
+
// Fresh global regex per call — avoids shared `lastIndex` state.
|
|
45
|
+
return str.replace(/\{\{\s*page\.([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\s*\}\}/g, (_m, path) => {
|
|
46
|
+
const v = lookup(path, vars);
|
|
47
|
+
return v == null ? '' : String(v);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function walk(value, vars) {
|
|
51
|
+
if (typeof value === 'string')
|
|
52
|
+
return resolveString(value, vars);
|
|
53
|
+
if (Array.isArray(value))
|
|
54
|
+
return value.map((v) => walk(v, vars));
|
|
55
|
+
if (value && typeof value === 'object') {
|
|
56
|
+
const out = {};
|
|
57
|
+
for (const [k, v] of Object.entries(value))
|
|
58
|
+
out[k] = walk(v, vars);
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Deep-resolve every `{{page.<var>}}` token in `value` against `pageVariables`.
|
|
65
|
+
* Returns `value` unchanged when there is no snapshot. Non-string leaves pass
|
|
66
|
+
* through untouched; the input is never mutated (objects/arrays are copied).
|
|
67
|
+
*/
|
|
68
|
+
export function resolvePageVarTokens(value, pageVariables) {
|
|
69
|
+
if (!pageVariables)
|
|
70
|
+
return value;
|
|
71
|
+
return walk(value, pageVariables);
|
|
72
|
+
}
|
|
@@ -18,7 +18,7 @@ import { useEffect, useMemo, useState } from 'react';
|
|
|
18
18
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, Button, cn, } from '@object-ui/components';
|
|
19
19
|
import { useObjectTranslation } from '@object-ui/i18n';
|
|
20
20
|
import { deriveFieldOptions, isImageLikeField, isGeoLikeField, pickPreferredField, KANBAN_GROUP_PREFERRED, PRIMARY_DATE_PREFERRED, END_DATE_PREFERRED, } from '@object-ui/plugin-view';
|
|
21
|
-
import { LayoutGrid, KanbanSquare, Calendar as CalendarIcon, Image as ImageIcon, GanttChartSquare, Clock, Map as MapIcon, BarChart3, AlertCircle, } from 'lucide-react';
|
|
21
|
+
import { LayoutGrid, KanbanSquare, Calendar as CalendarIcon, Image as ImageIcon, GanttChartSquare, Clock, Map as MapIcon, BarChart3, ListTree, AlertCircle, } from 'lucide-react';
|
|
22
22
|
function buildViewTypeMeta(t) {
|
|
23
23
|
return [
|
|
24
24
|
{ type: 'grid', icon: LayoutGrid, label: t('console.objectView.viewTypeGrid'), description: t('console.objectView.viewTypeGridDesc') },
|
|
@@ -29,6 +29,7 @@ function buildViewTypeMeta(t) {
|
|
|
29
29
|
{ type: 'gantt', icon: GanttChartSquare, label: t('console.objectView.viewTypeGantt'), description: t('console.objectView.viewTypeGanttDesc') },
|
|
30
30
|
{ type: 'map', icon: MapIcon, label: t('console.objectView.viewTypeMap'), description: t('console.objectView.viewTypeMapDesc') },
|
|
31
31
|
{ type: 'chart', icon: BarChart3, label: t('console.objectView.viewTypeChart'), description: t('console.objectView.viewTypeChartDesc') },
|
|
32
|
+
{ type: 'tree', icon: ListTree, label: t('console.objectView.viewTypeTree'), description: t('console.objectView.viewTypeTreeDesc') },
|
|
32
33
|
];
|
|
33
34
|
}
|
|
34
35
|
/** Suggest a non-colliding default name like "Grid 1", "Grid 2", … */
|
|
@@ -154,6 +155,18 @@ const REQUIRED_FIELDS_BY_TYPE = {
|
|
|
154
155
|
filter: (f) => f.type === 'number',
|
|
155
156
|
},
|
|
156
157
|
],
|
|
158
|
+
tree: [
|
|
159
|
+
{
|
|
160
|
+
key: 'parentField',
|
|
161
|
+
i18nKey: 'console.objectView.parentField',
|
|
162
|
+
helpI18nKey: 'console.objectView.parentFieldHelp',
|
|
163
|
+
// Self-referencing pointer: a `tree` field, or a lookup/master_detail
|
|
164
|
+
// back to the same object. `rawType` carries the unnormalized field type.
|
|
165
|
+
filter: (f) => f.rawType === 'tree' ||
|
|
166
|
+
f.rawType === 'lookup' ||
|
|
167
|
+
f.rawType === 'master_detail',
|
|
168
|
+
},
|
|
169
|
+
],
|
|
157
170
|
// grid has no strictly required fields at create time
|
|
158
171
|
};
|
|
159
172
|
export function CreateViewDialog({ open, onOpenChange, onCreate, existingLabels, availableTypes, objectDef, }) {
|
|
@@ -1,33 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
label?: string;
|
|
4
|
-
type?: string;
|
|
5
|
-
required?: boolean;
|
|
6
|
-
options?: Array<{
|
|
7
|
-
value: unknown;
|
|
8
|
-
label: string;
|
|
9
|
-
}>;
|
|
10
|
-
defaultValue?: unknown;
|
|
11
|
-
placeholder?: string;
|
|
12
|
-
}
|
|
13
|
-
export interface ScreenSpec {
|
|
14
|
-
nodeId: string;
|
|
15
|
-
title?: string;
|
|
16
|
-
description?: string;
|
|
17
|
-
fields: ScreenFieldSpec[];
|
|
18
|
-
/**
|
|
19
|
-
* `'object-form'` renders the named object's FULL create/edit form — incl.
|
|
20
|
-
* inline master-detail child grids — as a wizard step (vs. the flat `fields`
|
|
21
|
-
* list). The form persists the record (and its children, atomically) itself,
|
|
22
|
-
* then resumes the run with the saved id bound to `idVariable`.
|
|
23
|
-
*/
|
|
24
|
-
kind?: 'fields' | 'object-form';
|
|
25
|
-
objectName?: string;
|
|
26
|
-
mode?: 'create' | 'edit';
|
|
27
|
-
recordId?: string;
|
|
28
|
-
defaults?: Record<string, unknown>;
|
|
29
|
-
idVariable?: string;
|
|
30
|
-
}
|
|
1
|
+
import { type ScreenSpec } from './ScreenView';
|
|
2
|
+
export type { ScreenSpec, ScreenFieldSpec } from './ScreenView';
|
|
31
3
|
export interface ScreenFlowState {
|
|
32
4
|
flowName: string;
|
|
33
5
|
runId: string;
|
package/dist/views/FlowRunner.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* FlowRunner — renders the interactive `screen` of a paused screen-flow run
|
|
4
4
|
* (framework screen-flow runtime, ADR-0019) and resumes it with the collected
|
|
@@ -10,18 +10,15 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
10
10
|
* On submit it POSTs `/api/v1/automation/{flow}/runs/{runId}/resume` with the
|
|
11
11
|
* field values as `inputs`; a `paused` response renders the next screen
|
|
12
12
|
* (multi-screen wizards), a terminal response closes and refreshes the view.
|
|
13
|
+
*
|
|
14
|
+
* The screen BODY (flat fields / object-form) is rendered by the shared
|
|
15
|
+
* {@link ScreenView} — the same renderer the Studio design preview reuses, so
|
|
16
|
+
* the two can never drift (cf. #1927).
|
|
13
17
|
*/
|
|
14
18
|
import { useEffect, useState } from 'react';
|
|
15
|
-
import { Dialog, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription, Button,
|
|
16
|
-
import { ObjectForm } from '@object-ui/plugin-form';
|
|
19
|
+
import { Dialog, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription, Button, } from '@object-ui/components';
|
|
17
20
|
import { toast } from 'sonner';
|
|
18
|
-
|
|
19
|
-
const v = {};
|
|
20
|
-
for (const f of screen.fields)
|
|
21
|
-
if (f.defaultValue !== undefined)
|
|
22
|
-
v[f.name] = f.defaultValue;
|
|
23
|
-
return v;
|
|
24
|
-
}
|
|
21
|
+
import { ScreenView, isObjectFormScreen, initialScreenValues } from './ScreenView';
|
|
25
22
|
export function FlowRunner({ state, authFetch, baseUrl, onClose, onComplete, dataSource, objects }) {
|
|
26
23
|
const [screen, setScreen] = useState(null);
|
|
27
24
|
const [runId, setRunId] = useState('');
|
|
@@ -33,7 +30,7 @@ export function FlowRunner({ state, authFetch, baseUrl, onClose, onComplete, dat
|
|
|
33
30
|
setScreen(state.screen);
|
|
34
31
|
setRunId(state.runId);
|
|
35
32
|
setFlowName(state.flowName);
|
|
36
|
-
setValues(
|
|
33
|
+
setValues(initialScreenValues(state.screen));
|
|
37
34
|
}
|
|
38
35
|
}, [state]);
|
|
39
36
|
if (!state || !screen)
|
|
@@ -67,7 +64,7 @@ export function FlowRunner({ state, authFetch, baseUrl, onClose, onComplete, dat
|
|
|
67
64
|
if (data.status === 'paused' && data.screen) {
|
|
68
65
|
setScreen(data.screen);
|
|
69
66
|
setRunId(data.runId || runId);
|
|
70
|
-
setValues(
|
|
67
|
+
setValues(initialScreenValues(data.screen));
|
|
71
68
|
toast.success('Saved — next step');
|
|
72
69
|
}
|
|
73
70
|
else {
|
|
@@ -111,43 +108,14 @@ export function FlowRunner({ state, authFetch, baseUrl, onClose, onComplete, dat
|
|
|
111
108
|
setSubmitting(false);
|
|
112
109
|
}
|
|
113
110
|
};
|
|
114
|
-
const isObjectForm = screen
|
|
115
|
-
const objectDef = isObjectForm && Array.isArray(objects)
|
|
116
|
-
? objects.find((o) => o?.name === screen.objectName)
|
|
117
|
-
: undefined;
|
|
118
|
-
const subforms = objectDef
|
|
119
|
-
? (objectDef.form?.subforms ?? objectDef.formViews?.default?.subforms)
|
|
120
|
-
: undefined;
|
|
111
|
+
const isObjectForm = isObjectFormScreen(screen);
|
|
121
112
|
return (_jsx(Dialog, { open: true, onOpenChange: (o) => { if (!o && !submitting)
|
|
122
|
-
onClose(); }, children: _jsxs(DialogContent, { className: isObjectForm ? 'sm:max-w-3xl max-h-[90vh] overflow-y-auto' : 'sm:max-w-md', children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: screen.title || 'Input' }), screen.description && _jsx(DialogDescription, { children: screen.description })] }),
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
subforms,
|
|
131
|
-
onSuccess: onObjectFormSaved,
|
|
132
|
-
onCancel: onClose,
|
|
133
|
-
showSubmit: true,
|
|
134
|
-
showCancel: true,
|
|
135
|
-
submitText: 'Save & Continue',
|
|
136
|
-
cancelText: 'Cancel',
|
|
137
|
-
}, dataSource: dataSource }, screen.nodeId)) : (_jsx("div", { className: "text-sm text-destructive py-4", children: "This step renders an object form but no data source is available." })) })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "space-y-4 py-2", children: screen.fields.map((f) => (_jsxs("div", { className: "space-y-1.5", children: [_jsxs(Label, { htmlFor: `ff-${f.name}`, className: "text-sm", children: [f.label || f.name, f.required && _jsx("span", { className: "text-destructive", children: " *" })] }), _jsx(FieldInput, { field: f, value: values[f.name], onChange: (v) => setVal(f.name, v) })] }, f.name))) }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: onClose, disabled: submitting, children: "Cancel" }), _jsx(Button, { onClick: submit, disabled: submitting, children: submitting ? 'Submitting…' : 'Submit' })] })] }))] }) }));
|
|
138
|
-
}
|
|
139
|
-
function FieldInput({ field, value, onChange }) {
|
|
140
|
-
const id = `ff-${field.name}`;
|
|
141
|
-
const t = (field.type || 'text').toLowerCase();
|
|
142
|
-
if (Array.isArray(field.options) && field.options.length > 0) {
|
|
143
|
-
return (_jsxs(Select, { value: value != null ? String(value) : undefined, onValueChange: (v) => onChange(v), children: [_jsx(SelectTrigger, { id: id, children: _jsx(SelectValue, { placeholder: field.placeholder || 'Select…' }) }), _jsx(SelectContent, { children: field.options.map((o, i) => (_jsx(SelectItem, { value: String(o.value), children: o.label }, i))) })] }));
|
|
144
|
-
}
|
|
145
|
-
if (t === 'boolean' || t === 'checkbox') {
|
|
146
|
-
return _jsx(Checkbox, { id: id, checked: value === true, onCheckedChange: (c) => onChange(c === true) });
|
|
147
|
-
}
|
|
148
|
-
if (t === 'textarea' || t === 'markdown') {
|
|
149
|
-
return _jsx(Textarea, { id: id, value: value ?? '', placeholder: field.placeholder, onChange: (e) => onChange(e.target.value) });
|
|
150
|
-
}
|
|
151
|
-
const htmlType = t === 'number' || t === 'currency' ? 'number' : t === 'email' ? 'email' : t === 'date' ? 'date' : 'text';
|
|
152
|
-
return (_jsx(Input, { id: id, type: htmlType, value: value ?? '', placeholder: field.placeholder, onChange: (e) => onChange(htmlType === 'number' ? (e.target.value === '' ? undefined : Number(e.target.value)) : e.target.value) }));
|
|
113
|
+
onClose(); }, children: _jsxs(DialogContent, { className: isObjectForm ? 'sm:max-w-3xl max-h-[90vh] overflow-y-auto' : 'sm:max-w-md', children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: screen.title || 'Input' }), screen.description && _jsx(DialogDescription, { children: screen.description })] }), _jsx(ScreenView, { screen: screen, values: values, onValueChange: setVal, dataSource: dataSource, objects: objects, objectForm: {
|
|
114
|
+
onSuccess: onObjectFormSaved,
|
|
115
|
+
onCancel: onClose,
|
|
116
|
+
showSubmit: true,
|
|
117
|
+
showCancel: true,
|
|
118
|
+
submitText: 'Save & Continue',
|
|
119
|
+
cancelText: 'Cancel',
|
|
120
|
+
} }), !isObjectForm && (_jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: onClose, disabled: submitting, children: "Cancel" }), _jsx(Button, { onClick: submit, disabled: submitting, children: submitting ? 'Submitting…' : 'Submit' })] }))] }) }));
|
|
153
121
|
}
|
package/dist/views/ObjectView.js
CHANGED
|
@@ -42,6 +42,8 @@ import { useRealtimeSubscription, useConflictResolution } from '@object-ui/colla
|
|
|
42
42
|
import { ActionProvider, useNavigationOverlay, SchemaRenderer } from '@object-ui/react';
|
|
43
43
|
import { toast } from 'sonner';
|
|
44
44
|
import { useConsoleActionRuntime } from '../hooks/useConsoleActionRuntime';
|
|
45
|
+
import { useEnvironmentEntitlements } from '../environment/useEnvironmentEntitlements';
|
|
46
|
+
import { EnvironmentListToolbar } from '../environment/EnvironmentListToolbar';
|
|
45
47
|
/** Map view types to Lucide icons (Airtable-style) */
|
|
46
48
|
const VIEW_TYPE_ICONS = {
|
|
47
49
|
grid: TableIcon,
|
|
@@ -272,6 +274,27 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }) {
|
|
|
272
274
|
onRefresh: () => setRefreshKey((k) => k + 1),
|
|
273
275
|
});
|
|
274
276
|
const { confirmHandler, toastHandler } = actionRuntime;
|
|
277
|
+
// Environment list (sys_environment) is entitlement- + state-aware: born
|
|
278
|
+
// with one production env per org, its single `create_environment` action
|
|
279
|
+
// means different things — "Set up your production environment", "Add
|
|
280
|
+
// development environment", or an upgrade prompt — depending on org state.
|
|
281
|
+
// Resolved org-side here; the hook is a no-op for every other object.
|
|
282
|
+
const isEnvironmentList = objectDef.name === 'sys_environment';
|
|
283
|
+
const environmentEntitlements = useEnvironmentEntitlements({
|
|
284
|
+
enabled: isEnvironmentList,
|
|
285
|
+
dataSource,
|
|
286
|
+
authFetch: actionRuntime.authFetch,
|
|
287
|
+
apiBase: import.meta.env?.VITE_SERVER_URL || '',
|
|
288
|
+
refreshKey,
|
|
289
|
+
});
|
|
290
|
+
// Localized `list_toolbar` actions, shared by the generic action bar and the
|
|
291
|
+
// environment-aware toolbar (the action:bar renderer filters by location).
|
|
292
|
+
const localizedToolbarActions = useMemo(() => (objectDef.actions || []).map((a) => ({
|
|
293
|
+
...a,
|
|
294
|
+
label: actionLabel(objectDef.name, a.name, a.label || a.name),
|
|
295
|
+
...(a.confirmText !== undefined && { confirmText: actionConfirm(objectDef.name, a.name, a.confirmText) }),
|
|
296
|
+
...(a.successMessage !== undefined && { successMessage: actionSuccess(objectDef.name, a.name, a.successMessage) }),
|
|
297
|
+
})), [objectDef, actionLabel, actionConfirm, actionSuccess]);
|
|
275
298
|
// Resolve which generic CRUD affordances belong in the toolbar for
|
|
276
299
|
// this object's lifecycle bucket (`managedBy`). config tables show
|
|
277
300
|
// New/Edit/Delete but no CSV Import; system / append-only / better-auth
|
|
@@ -1333,19 +1356,10 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }) {
|
|
|
1333
1356
|
? t('common.removeFromFavorites', { defaultValue: 'Remove from favorites' })
|
|
1334
1357
|
: t('common.addToFavorites', { defaultValue: 'Add to favorites' }), "data-testid": `object-favorite-btn-${objectName}`, children: isFavorite(`object:${objectName}`)
|
|
1335
1358
|
? _jsx(Star, { className: "h-4 w-4 fill-amber-400 text-amber-400" })
|
|
1336
|
-
: _jsx(StarOff, { className: "h-4 w-4" }) })), affordances.create && can(objectDef.name, 'create') && (_jsxs(Button, { size: "sm", onClick: actions.create, className: "shadow-none gap-1.5 sm:gap-2 h-8 sm:h-9", children: [_jsx(Plus, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: t('console.objectView.new') })] })), affordances.import && can(objectDef.name, 'create') && (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => setShowImport(true), className: "hidden sm:inline-flex shadow-none gap-1.5 sm:gap-2 h-8 sm:h-9", title: t('console.objectView.importTitle'), "data-testid": "object-view-import-button", children: [_jsx(Upload, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: t('console.objectView.import') })] })), objectDef.actions?.some((a) => a.locations?.includes('list_toolbar')) && (_jsx(SchemaRenderer, { schema: {
|
|
1359
|
+
: _jsx(StarOff, { className: "h-4 w-4" }) })), affordances.create && can(objectDef.name, 'create') && (_jsxs(Button, { size: "sm", onClick: actions.create, className: "shadow-none gap-1.5 sm:gap-2 h-8 sm:h-9", children: [_jsx(Plus, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: t('console.objectView.new') })] })), affordances.import && can(objectDef.name, 'create') && (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => setShowImport(true), className: "hidden sm:inline-flex shadow-none gap-1.5 sm:gap-2 h-8 sm:h-9", title: t('console.objectView.importTitle'), "data-testid": "object-view-import-button", children: [_jsx(Upload, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: t('console.objectView.import') })] })), objectDef.actions?.some((a) => a.locations?.includes('list_toolbar')) && (isEnvironmentList ? (_jsx(EnvironmentListToolbar, { actions: localizedToolbarActions, entitlements: environmentEntitlements, onUpgrade: actionRuntime.openEntitlementDialog })) : (_jsx(SchemaRenderer, { schema: {
|
|
1337
1360
|
type: 'action:bar',
|
|
1338
1361
|
location: 'list_toolbar',
|
|
1339
|
-
actions:
|
|
1340
|
-
...a,
|
|
1341
|
-
label: actionLabel(objectDef.name, a.name, a.label || a.name),
|
|
1342
|
-
...(a.confirmText !== undefined && {
|
|
1343
|
-
confirmText: actionConfirm(objectDef.name, a.name, a.confirmText),
|
|
1344
|
-
}),
|
|
1345
|
-
...(a.successMessage !== undefined && {
|
|
1346
|
-
successMessage: actionSuccess(objectDef.name, a.name, a.successMessage),
|
|
1347
|
-
}),
|
|
1348
|
-
})),
|
|
1362
|
+
actions: localizedToolbarActions,
|
|
1349
1363
|
size: 'sm',
|
|
1350
1364
|
variant: 'outline',
|
|
1351
1365
|
// On mobile, collapse all schema-driven toolbar actions
|
|
@@ -1353,7 +1367,7 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }) {
|
|
|
1353
1367
|
// Import buttons stay visible without pushing the page
|
|
1354
1368
|
// title off-screen.
|
|
1355
1369
|
mobileMaxVisible: 0,
|
|
1356
|
-
} }))] }) }) }), affordances.create && can(objectDef.name, 'create') && (_jsx("button", { type: "button", onClick: actions.create, className: "sm:hidden fixed right-4 bottom-36 z-40 h-12 w-12 rounded-full bg-primary text-primary-foreground shadow-lg active:scale-95 transition-transform inline-flex items-center justify-center", "aria-label": t('console.objectView.new'), "data-testid": "mobile-fab-create", children: _jsx(Plus, { className: "h-5 w-5" }) })), showImport && (_jsx(Suspense, { fallback: null, children: _jsx(ImportWizard, { open: showImport, onOpenChange: setShowImport, objectName: objectDef.name, objectLabel: objectLabel(objectDef), fields: Object.entries(objectDef.fields || {}).map(([name, def]) => ({
|
|
1370
|
+
} })))] }) }) }), affordances.create && can(objectDef.name, 'create') && (_jsx("button", { type: "button", onClick: actions.create, className: "sm:hidden fixed right-4 bottom-36 z-40 h-12 w-12 rounded-full bg-primary text-primary-foreground shadow-lg active:scale-95 transition-transform inline-flex items-center justify-center", "aria-label": t('console.objectView.new'), "data-testid": "mobile-fab-create", children: _jsx(Plus, { className: "h-5 w-5" }) })), showImport && (_jsx(Suspense, { fallback: null, children: _jsx(ImportWizard, { open: showImport, onOpenChange: setShowImport, objectName: objectDef.name, objectLabel: objectLabel(objectDef), fields: Object.entries(objectDef.fields || {}).map(([name, def]) => ({
|
|
1357
1371
|
name,
|
|
1358
1372
|
label: def?.label || name,
|
|
1359
1373
|
type: def?.type || 'text',
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export interface ScreenFieldSpec {
|
|
2
|
+
name: string;
|
|
3
|
+
label?: string;
|
|
4
|
+
type?: string;
|
|
5
|
+
required?: boolean;
|
|
6
|
+
options?: Array<{
|
|
7
|
+
value: unknown;
|
|
8
|
+
label: string;
|
|
9
|
+
}>;
|
|
10
|
+
defaultValue?: unknown;
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ScreenSpec {
|
|
14
|
+
nodeId: string;
|
|
15
|
+
title?: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
fields: ScreenFieldSpec[];
|
|
18
|
+
/**
|
|
19
|
+
* `'object-form'` renders the named object's FULL create/edit form — incl.
|
|
20
|
+
* inline master-detail child grids — as a wizard step (vs. the flat `fields`
|
|
21
|
+
* list). The form persists the record (and its children, atomically) itself,
|
|
22
|
+
* then resumes the run with the saved id bound to `idVariable`.
|
|
23
|
+
*/
|
|
24
|
+
kind?: 'fields' | 'object-form';
|
|
25
|
+
objectName?: string;
|
|
26
|
+
mode?: 'create' | 'edit';
|
|
27
|
+
recordId?: string;
|
|
28
|
+
defaults?: Record<string, unknown>;
|
|
29
|
+
idVariable?: string;
|
|
30
|
+
}
|
|
31
|
+
/** Whether a screen renders the object-form body rather than the flat fields. */
|
|
32
|
+
export declare function isObjectFormScreen(screen: ScreenSpec): boolean;
|
|
33
|
+
/** Seed flat-field values from each field's `defaultValue`. */
|
|
34
|
+
export declare function initialScreenValues(screen: ScreenSpec): Record<string, unknown>;
|
|
35
|
+
/** Submit/cancel wiring for the object-form body — runtime persists & resumes;
|
|
36
|
+
* the design preview hides the bar (`showSubmit`/`showCancel` false). */
|
|
37
|
+
export interface ScreenObjectFormActions {
|
|
38
|
+
showSubmit?: boolean;
|
|
39
|
+
showCancel?: boolean;
|
|
40
|
+
submitText?: string;
|
|
41
|
+
cancelText?: string;
|
|
42
|
+
onSuccess?: (saved: any) => void;
|
|
43
|
+
onCancel?: () => void;
|
|
44
|
+
/** Overrides the "no data source" copy (the preview phrases it for authors). */
|
|
45
|
+
noDataSourceMessage?: React.ReactNode;
|
|
46
|
+
}
|
|
47
|
+
export interface ScreenViewProps {
|
|
48
|
+
screen: ScreenSpec;
|
|
49
|
+
/** Controlled values for the flat-fields body. */
|
|
50
|
+
values: Record<string, unknown>;
|
|
51
|
+
onValueChange: (name: string, value: unknown) => void;
|
|
52
|
+
/**
|
|
53
|
+
* Data source — required to render the `object-form` body. ObjectForm fetches
|
|
54
|
+
* the object schema (and persists) through this adapter.
|
|
55
|
+
*/
|
|
56
|
+
dataSource?: any;
|
|
57
|
+
/**
|
|
58
|
+
* Object definitions — used to derive an `object-form` step's inline
|
|
59
|
+
* master-detail `subforms` (mirrors RecordFormPage's create form).
|
|
60
|
+
*/
|
|
61
|
+
objects?: any[];
|
|
62
|
+
objectForm?: ScreenObjectFormActions;
|
|
63
|
+
className?: string;
|
|
64
|
+
}
|
|
65
|
+
export declare function ScreenView({ screen, values, onValueChange, dataSource, objects, objectForm, className }: ScreenViewProps): import("react").JSX.Element;
|
|
66
|
+
export declare function FieldInput({ field, value, onChange }: {
|
|
67
|
+
field: ScreenFieldSpec;
|
|
68
|
+
value: unknown;
|
|
69
|
+
onChange: (v: unknown) => void;
|
|
70
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
3
|
+
/**
|
|
4
|
+
* ScreenView — the presentational body of a flow `screen` (framework
|
|
5
|
+
* screen-flow runtime, ADR-0019): the flat input-field list OR the named
|
|
6
|
+
* object's full create/edit form.
|
|
7
|
+
*
|
|
8
|
+
* Extracted from {@link FlowRunner} so the exact same renderer drives both the
|
|
9
|
+
* runtime (paused screen-flow → collect input → resume) and the Studio design
|
|
10
|
+
* preview ({@link ScreenPreview}). Keeping ONE renderer is deliberate: a
|
|
11
|
+
* separate preview reimplementation would drift from runtime — the
|
|
12
|
+
* simulator-vs-engine divergence fixed in #1927.
|
|
13
|
+
*
|
|
14
|
+
* It owns no submit/resume behaviour and no Dialog chrome — the caller frames
|
|
15
|
+
* it (the runtime wraps it in a Dialog + footer and resumes the run; the
|
|
16
|
+
* preview wraps it in a card and hides the persist bar).
|
|
17
|
+
*/
|
|
18
|
+
import { Input, Label, Textarea, Checkbox, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, cn, } from '@object-ui/components';
|
|
19
|
+
import { ObjectForm } from '@object-ui/plugin-form';
|
|
20
|
+
/** Whether a screen renders the object-form body rather than the flat fields. */
|
|
21
|
+
export function isObjectFormScreen(screen) {
|
|
22
|
+
return screen.kind === 'object-form' && !!screen.objectName;
|
|
23
|
+
}
|
|
24
|
+
/** Seed flat-field values from each field's `defaultValue`. */
|
|
25
|
+
export function initialScreenValues(screen) {
|
|
26
|
+
const v = {};
|
|
27
|
+
for (const f of screen.fields)
|
|
28
|
+
if (f.defaultValue !== undefined)
|
|
29
|
+
v[f.name] = f.defaultValue;
|
|
30
|
+
return v;
|
|
31
|
+
}
|
|
32
|
+
export function ScreenView({ screen, values, onValueChange, dataSource, objects, objectForm, className }) {
|
|
33
|
+
if (isObjectFormScreen(screen)) {
|
|
34
|
+
const objectDef = Array.isArray(objects) ? objects.find((o) => o?.name === screen.objectName) : undefined;
|
|
35
|
+
const subforms = objectDef
|
|
36
|
+
? (objectDef.form?.subforms ?? objectDef.formViews?.default?.subforms)
|
|
37
|
+
: undefined;
|
|
38
|
+
// Full object create/edit form (with inline master-detail grids). At runtime
|
|
39
|
+
// the form owns its own Save/Cancel bar; the preview hides it.
|
|
40
|
+
return (_jsx("div", { className: cn('py-2', className), children: dataSource ? (_jsx(ObjectForm, { schema: {
|
|
41
|
+
type: 'object-form',
|
|
42
|
+
formType: 'simple',
|
|
43
|
+
objectName: screen.objectName,
|
|
44
|
+
mode: screen.mode === 'edit' ? 'edit' : 'create',
|
|
45
|
+
recordId: screen.mode === 'edit' ? screen.recordId : undefined,
|
|
46
|
+
...(screen.defaults ? { initialValues: screen.defaults } : {}),
|
|
47
|
+
layout: 'vertical',
|
|
48
|
+
subforms,
|
|
49
|
+
onSuccess: objectForm?.onSuccess,
|
|
50
|
+
onCancel: objectForm?.onCancel,
|
|
51
|
+
showSubmit: objectForm?.showSubmit ?? true,
|
|
52
|
+
showCancel: objectForm?.showCancel ?? true,
|
|
53
|
+
submitText: objectForm?.submitText ?? 'Save & Continue',
|
|
54
|
+
cancelText: objectForm?.cancelText ?? 'Cancel',
|
|
55
|
+
}, dataSource: dataSource }, screen.nodeId)) : (_jsx("div", { className: "text-sm text-destructive py-4", children: objectForm?.noDataSourceMessage ?? 'This step renders an object form but no data source is available.' })) }));
|
|
56
|
+
}
|
|
57
|
+
return (_jsx("div", { className: cn('space-y-4 py-2', className), children: screen.fields.map((f) => (_jsxs("div", { className: "space-y-1.5", children: [_jsxs(Label, { htmlFor: `ff-${f.name}`, className: "text-sm", children: [f.label || f.name, f.required && _jsx("span", { className: "text-destructive", children: " *" })] }), _jsx(FieldInput, { field: f, value: values[f.name], onChange: (v) => onValueChange(f.name, v) })] }, f.name))) }));
|
|
58
|
+
}
|
|
59
|
+
export function FieldInput({ field, value, onChange }) {
|
|
60
|
+
const id = `ff-${field.name}`;
|
|
61
|
+
const t = (field.type || 'text').toLowerCase();
|
|
62
|
+
if (Array.isArray(field.options) && field.options.length > 0) {
|
|
63
|
+
return (_jsxs(Select, { value: value != null ? String(value) : undefined, onValueChange: (v) => onChange(v), children: [_jsx(SelectTrigger, { id: id, children: _jsx(SelectValue, { placeholder: field.placeholder || 'Select…' }) }), _jsx(SelectContent, { children: field.options.map((o, i) => (_jsx(SelectItem, { value: String(o.value), children: o.label }, i))) })] }));
|
|
64
|
+
}
|
|
65
|
+
if (t === 'boolean' || t === 'checkbox') {
|
|
66
|
+
return _jsx(Checkbox, { id: id, checked: value === true, onCheckedChange: (c) => onChange(c === true) });
|
|
67
|
+
}
|
|
68
|
+
if (t === 'textarea' || t === 'markdown') {
|
|
69
|
+
return _jsx(Textarea, { id: id, value: value ?? '', placeholder: field.placeholder, onChange: (e) => onChange(e.target.value) });
|
|
70
|
+
}
|
|
71
|
+
const htmlType = t === 'number' || t === 'currency' ? 'number' : t === 'email' ? 'email' : t === 'date' ? 'date' : 'text';
|
|
72
|
+
return (_jsx(Input, { id: id, type: htmlType, value: value ?? '', placeholder: field.placeholder, onChange: (e) => onChange(htmlType === 'number' ? (e.target.value === '' ? undefined : Number(e.target.value)) : e.target.value) }));
|
|
73
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
* AssignedUsersSection — "Manage Assignments" for a permission set.
|
|
9
|
+
*
|
|
10
|
+
* The admin's mental model is "who holds this role / AI seat" — so this is a
|
|
11
|
+
* people-first list (name + email + remove), not a raw junction table. It reads
|
|
12
|
+
* `sys_user_permission_set` for the set, resolves each `user_id` to a real
|
|
13
|
+
* person, and uses the reusable `RecordPickerDialog` to assign more. Server-side
|
|
14
|
+
* rules on the junction insert (e.g. the AI-seat cap) are caught and shown as a
|
|
15
|
+
* friendly, localized inline message — not a raw developer error.
|
|
16
|
+
*
|
|
17
|
+
* Permission-set-agnostic: every role gets the same UI, and the AI seat
|
|
18
|
+
* (`ai_seat`) is just one of them. The generic add-by-picker engine (spec
|
|
19
|
+
* RecordRelatedListProps.add) powers the capability; this is the polished
|
|
20
|
+
* surface for the high-value case.
|
|
21
|
+
*/
|
|
22
|
+
import * as React from 'react';
|
|
23
|
+
export interface AssignedUsersSectionProps {
|
|
24
|
+
/** The permission set's machine name (e.g. `ai_seat`, `admin_full_access`). */
|
|
25
|
+
permissionSetName: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function AssignedUsersSection({ permissionSetName }: AssignedUsersSectionProps): React.JSX.Element;
|
|
28
|
+
export default AssignedUsersSection;
|