@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.
Files changed (141) hide show
  1. package/CHANGELOG.md +560 -0
  2. package/dist/console/AppContent.js +23 -17
  3. package/dist/console/ConsoleShell.d.ts +16 -0
  4. package/dist/console/ConsoleShell.js +43 -2
  5. package/dist/console/ai/AiChatPage.js +47 -16
  6. package/dist/console/ai/LiveCanvas.d.ts +8 -2
  7. package/dist/console/ai/LiveCanvas.js +6 -4
  8. package/dist/console/home/HomeLayout.js +5 -7
  9. package/dist/console/home/HomePage.js +1 -9
  10. package/dist/console/organizations/CreateWorkspaceDialog.js +15 -1
  11. package/dist/console/organizations/OrganizationsPage.js +22 -3
  12. package/dist/console/organizations/provisionEnvironment.d.ts +53 -0
  13. package/dist/console/organizations/provisionEnvironment.js +64 -0
  14. package/dist/environment/EnvironmentEntitlementDialog.d.ts +34 -0
  15. package/dist/environment/EnvironmentEntitlementDialog.js +37 -0
  16. package/dist/environment/EnvironmentListToolbar.d.ts +33 -0
  17. package/dist/environment/EnvironmentListToolbar.js +59 -0
  18. package/dist/environment/entitlements.d.ts +90 -0
  19. package/dist/environment/entitlements.js +91 -0
  20. package/dist/environment/useEnvironmentEntitlements.d.ts +32 -0
  21. package/dist/environment/useEnvironmentEntitlements.js +108 -0
  22. package/dist/hooks/useActionModal.js +15 -1
  23. package/dist/hooks/useAiSurface.d.ts +59 -0
  24. package/dist/hooks/useAiSurface.js +78 -0
  25. package/dist/hooks/useChatConversation.d.ts +30 -0
  26. package/dist/hooks/useChatConversation.js +63 -0
  27. package/dist/hooks/useConsoleActionRuntime.d.ts +3 -0
  28. package/dist/hooks/useConsoleActionRuntime.js +42 -10
  29. package/dist/index.d.ts +5 -2
  30. package/dist/index.js +10 -2
  31. package/dist/layout/AppHeader.js +28 -4
  32. package/dist/layout/ConsoleFloatingChatbot.d.ts +6 -4
  33. package/dist/layout/ConsoleFloatingChatbot.js +41 -10
  34. package/dist/layout/ConsoleLayout.js +5 -6
  35. package/dist/layout/ContextSelectors.js +59 -35
  36. package/dist/layout/agentPicker.d.ts +56 -0
  37. package/dist/layout/agentPicker.js +40 -0
  38. package/dist/preview/CommitTimeline.d.ts +15 -0
  39. package/dist/preview/CommitTimeline.js +82 -0
  40. package/dist/preview/DraftPreviewBar.js +20 -7
  41. package/dist/preview/UnpublishedAppBar.js +11 -7
  42. package/dist/preview/commitHistory.d.ts +28 -0
  43. package/dist/preview/commitHistory.js +48 -0
  44. package/dist/providers/ExpressionProvider.js +9 -3
  45. package/dist/providers/MetadataProvider.js +9 -0
  46. package/dist/utils/index.d.ts +2 -2
  47. package/dist/utils/index.js +1 -1
  48. package/dist/utils/recordFormNavigation.d.ts +60 -0
  49. package/dist/utils/recordFormNavigation.js +35 -0
  50. package/dist/utils/resolvePageVarTokens.d.ts +31 -0
  51. package/dist/utils/resolvePageVarTokens.js +72 -0
  52. package/dist/views/CreateViewDialog.js +14 -1
  53. package/dist/views/FlowRunner.d.ts +2 -30
  54. package/dist/views/FlowRunner.js +18 -50
  55. package/dist/views/ObjectView.js +26 -12
  56. package/dist/views/ScreenView.d.ts +70 -0
  57. package/dist/views/ScreenView.js +73 -0
  58. package/dist/views/metadata-admin/AssignedUsersSection.d.ts +28 -0
  59. package/dist/views/metadata-admin/AssignedUsersSection.js +151 -0
  60. package/dist/views/metadata-admin/DirectoryPage.js +2 -14
  61. package/dist/views/metadata-admin/JsonSourceEditor.d.ts +3 -1
  62. package/dist/views/metadata-admin/JsonSourceEditor.js +21 -3
  63. package/dist/views/metadata-admin/PackagesPage.d.ts +5 -0
  64. package/dist/views/metadata-admin/PackagesPage.js +58 -5
  65. package/dist/views/metadata-admin/PermissionMatrixEditor.js +2 -1
  66. package/dist/views/metadata-admin/ResourceEditPage.js +83 -24
  67. package/dist/views/metadata-admin/ResourceListPage.js +28 -19
  68. package/dist/views/metadata-admin/StudioHomePage.js +6 -14
  69. package/dist/views/metadata-admin/anchors.js +20 -2
  70. package/dist/views/metadata-admin/createBody.d.ts +26 -0
  71. package/dist/views/metadata-admin/createBody.js +30 -0
  72. package/dist/views/metadata-admin/i18n.js +108 -2
  73. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +10 -2
  74. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +136 -8
  75. package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +99 -4
  76. package/dist/views/metadata-admin/inspectors/FlowExprIssue.d.ts +21 -0
  77. package/dist/views/metadata-admin/inspectors/FlowExprIssue.js +13 -0
  78. package/dist/views/metadata-admin/inspectors/FlowKeyValueField.d.ts +20 -2
  79. package/dist/views/metadata-admin/inspectors/FlowKeyValueField.js +71 -28
  80. package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.d.ts +4 -1
  81. package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.js +24 -9
  82. package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +81 -4
  83. package/dist/views/metadata-admin/inspectors/FlowObjectListField.d.ts +4 -1
  84. package/dist/views/metadata-admin/inspectors/FlowObjectListField.js +8 -3
  85. package/dist/views/metadata-admin/inspectors/ObjectDefaultInspector.js +5 -4
  86. package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +47 -12
  87. package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.d.ts +1 -1
  88. package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.js +60 -2
  89. package/dist/views/metadata-admin/inspectors/VariableTextInput.d.ts +47 -0
  90. package/dist/views/metadata-admin/inspectors/VariableTextInput.js +95 -0
  91. package/dist/views/metadata-admin/inspectors/_shared.d.ts +5 -1
  92. package/dist/views/metadata-admin/inspectors/_shared.js +2 -2
  93. package/dist/views/metadata-admin/inspectors/datasetFilterCondition.d.ts +24 -0
  94. package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +102 -0
  95. package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +16 -1
  96. package/dist/views/metadata-admin/inspectors/flow-node-config.js +67 -11
  97. package/dist/views/metadata-admin/inspectors/flow-ref-check.d.ts +39 -0
  98. package/dist/views/metadata-admin/inspectors/flow-ref-check.js +114 -0
  99. package/dist/views/metadata-admin/inspectors/flow-scope.d.ts +109 -0
  100. package/dist/views/metadata-admin/inspectors/flow-scope.js +199 -0
  101. package/dist/views/metadata-admin/inspectors/useDatasetFields.d.ts +14 -3
  102. package/dist/views/metadata-admin/inspectors/useDatasetFields.js +0 -0
  103. package/dist/views/metadata-admin/inspectors/useFlowScope.d.ts +23 -0
  104. package/dist/views/metadata-admin/inspectors/useFlowScope.js +45 -0
  105. package/dist/views/metadata-admin/issuePath.d.ts +22 -0
  106. package/dist/views/metadata-admin/issuePath.js +65 -0
  107. package/dist/views/metadata-admin/package-scope.d.ts +41 -0
  108. package/dist/views/metadata-admin/package-scope.js +59 -0
  109. package/dist/views/metadata-admin/preview-registry.d.ts +12 -0
  110. package/dist/views/metadata-admin/previews/DatasetPreview.js +21 -5
  111. package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +26 -1
  112. package/dist/views/metadata-admin/previews/FlowCanvas.js +143 -16
  113. package/dist/views/metadata-admin/previews/FlowPreview.d.ts +1 -1
  114. package/dist/views/metadata-admin/previews/FlowPreview.js +47 -7
  115. package/dist/views/metadata-admin/previews/FlowSimulatorPanel.js +37 -3
  116. package/dist/views/metadata-admin/previews/ObjectFormCanvas.js +9 -4
  117. package/dist/views/metadata-admin/previews/PagePreview.js +112 -3
  118. package/dist/views/metadata-admin/previews/ProblemsPanel.d.ts +18 -0
  119. package/dist/views/metadata-admin/previews/ProblemsPanel.js +27 -0
  120. package/dist/views/metadata-admin/previews/ReportPreview.d.ts +9 -8
  121. package/dist/views/metadata-admin/previews/ReportPreview.js +33 -16
  122. package/dist/views/metadata-admin/previews/ScreenPreview.d.ts +38 -0
  123. package/dist/views/metadata-admin/previews/ScreenPreview.js +61 -0
  124. package/dist/views/metadata-admin/previews/flow-canvas-layout.d.ts +14 -0
  125. package/dist/views/metadata-admin/previews/flow-canvas-layout.js +37 -0
  126. package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +17 -1
  127. package/dist/views/metadata-admin/previews/flow-canvas-parts.js +23 -6
  128. package/dist/views/metadata-admin/previews/flow-expr-problems.d.ts +19 -0
  129. package/dist/views/metadata-admin/previews/flow-expr-problems.js +97 -0
  130. package/dist/views/metadata-admin/previews/flow-problems.d.ts +84 -0
  131. package/dist/views/metadata-admin/previews/flow-problems.js +209 -0
  132. package/dist/views/metadata-admin/previews/object-fields-io.d.ts +21 -0
  133. package/dist/views/metadata-admin/previews/object-fields-io.js +37 -2
  134. package/dist/views/metadata-admin/previews/screen-spec.d.ts +43 -0
  135. package/dist/views/metadata-admin/previews/screen-spec.js +108 -0
  136. package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +20 -0
  137. package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.d.ts +7 -0
  138. package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +76 -2
  139. package/dist/views/metadata-admin/previews/simulator/flow-simulator.d.ts +32 -3
  140. package/dist/views/metadata-admin/previews/simulator/flow-simulator.js +119 -9
  141. 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
- 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
- }
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;
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
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, Input, Label, Textarea, Checkbox, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, } from '@object-ui/components';
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
- function initialValues(screen) {
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(initialValues(state.screen));
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(initialValues(data.screen));
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.kind === 'object-form' && !!screen.objectName;
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 })] }), isObjectForm ? (_jsx("div", { className: "py-2", children: dataSource ? (_jsx(ObjectForm, { schema: {
123
- type: 'object-form',
124
- formType: 'simple',
125
- objectName: screen.objectName,
126
- mode: screen.mode === 'edit' ? 'edit' : 'create',
127
- recordId: screen.mode === 'edit' ? screen.recordId : undefined,
128
- ...(screen.defaults ? { initialValues: screen.defaults } : {}),
129
- layout: 'vertical',
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
  }
@@ -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: (objectDef.actions || []).map((a) => ({
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;