@object-ui/app-shell 7.1.0 → 7.3.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 (107) hide show
  1. package/CHANGELOG.md +320 -0
  2. package/dist/components/ManagedByBadge.js +1 -1
  3. package/dist/console/AppContent.js +9 -15
  4. package/dist/console/ConsoleShell.d.ts +16 -0
  5. package/dist/console/ConsoleShell.js +43 -2
  6. package/dist/console/ai/AiChatPage.js +64 -14
  7. package/dist/console/ai/BuildDebugDrawer.d.ts +20 -0
  8. package/dist/console/ai/BuildDebugDrawer.js +75 -0
  9. package/dist/console/ai/buildDebugApi.d.ts +94 -0
  10. package/dist/console/ai/buildDebugApi.js +16 -0
  11. package/dist/console/home/HomeLayout.js +5 -7
  12. package/dist/console/home/HomePage.js +1 -9
  13. package/dist/console/organizations/CreateWorkspaceDialog.js +15 -1
  14. package/dist/console/organizations/OrganizationsPage.js +32 -4
  15. package/dist/console/organizations/manage/OrganizationLayout.js +1 -1
  16. package/dist/console/organizations/provisionEnvironment.d.ts +53 -0
  17. package/dist/console/organizations/provisionEnvironment.js +64 -0
  18. package/dist/environment/EnvironmentEntitlementDialog.d.ts +34 -0
  19. package/dist/environment/EnvironmentEntitlementDialog.js +37 -0
  20. package/dist/environment/EnvironmentListToolbar.d.ts +33 -0
  21. package/dist/environment/EnvironmentListToolbar.js +59 -0
  22. package/dist/environment/entitlements.d.ts +90 -0
  23. package/dist/environment/entitlements.js +91 -0
  24. package/dist/environment/useEnvironmentEntitlements.d.ts +32 -0
  25. package/dist/environment/useEnvironmentEntitlements.js +108 -0
  26. package/dist/hooks/useActionModal.js +15 -1
  27. package/dist/hooks/useAiSurface.d.ts +59 -0
  28. package/dist/hooks/useAiSurface.js +78 -0
  29. package/dist/hooks/useConsoleActionRuntime.d.ts +3 -0
  30. package/dist/hooks/useConsoleActionRuntime.js +36 -8
  31. package/dist/index.d.ts +3 -1
  32. package/dist/index.js +5 -1
  33. package/dist/layout/AppHeader.js +30 -5
  34. package/dist/layout/ConsoleFloatingChatbot.js +22 -4
  35. package/dist/layout/ConsoleLayout.js +5 -6
  36. package/dist/layout/ContextSelectors.js +0 -19
  37. package/dist/layout/WorkspaceSwitcher.d.ts +14 -0
  38. package/dist/layout/WorkspaceSwitcher.js +76 -0
  39. package/dist/preview/DraftPreviewBar.js +20 -7
  40. package/dist/providers/ExpressionProvider.js +9 -3
  41. package/dist/utils/index.d.ts +2 -2
  42. package/dist/utils/index.js +1 -1
  43. package/dist/utils/managedByEmptyState.d.ts +1 -1
  44. package/dist/utils/managedByEmptyState.js +20 -2
  45. package/dist/utils/recordFormNavigation.d.ts +60 -0
  46. package/dist/utils/recordFormNavigation.js +35 -0
  47. package/dist/utils/resolvePageVarTokens.d.ts +31 -0
  48. package/dist/utils/resolvePageVarTokens.js +72 -0
  49. package/dist/views/CreateViewDialog.js +14 -1
  50. package/dist/views/ObjectView.js +27 -13
  51. package/dist/views/metadata-admin/AssignedUsersSection.d.ts +28 -0
  52. package/dist/views/metadata-admin/AssignedUsersSection.js +151 -0
  53. package/dist/views/metadata-admin/PackagesPage.d.ts +5 -0
  54. package/dist/views/metadata-admin/PackagesPage.js +49 -4
  55. package/dist/views/metadata-admin/PermissionMatrixEditor.js +2 -1
  56. package/dist/views/metadata-admin/ResourceEditPage.js +36 -4
  57. package/dist/views/metadata-admin/ResourceListPage.js +25 -10
  58. package/dist/views/metadata-admin/StudioHomePage.js +1 -5
  59. package/dist/views/metadata-admin/createBody.d.ts +26 -0
  60. package/dist/views/metadata-admin/createBody.js +30 -0
  61. package/dist/views/metadata-admin/i18n.js +20 -2
  62. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +8 -0
  63. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +17 -3
  64. package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +16 -2
  65. package/dist/views/metadata-admin/inspectors/FlowExprIssue.d.ts +21 -0
  66. package/dist/views/metadata-admin/inspectors/FlowExprIssue.js +13 -0
  67. package/dist/views/metadata-admin/inspectors/FlowKeyValueField.d.ts +20 -2
  68. package/dist/views/metadata-admin/inspectors/FlowKeyValueField.js +71 -28
  69. package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.d.ts +4 -1
  70. package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.js +24 -9
  71. package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +15 -3
  72. package/dist/views/metadata-admin/inspectors/FlowObjectListField.d.ts +4 -1
  73. package/dist/views/metadata-admin/inspectors/FlowObjectListField.js +8 -3
  74. package/dist/views/metadata-admin/inspectors/VariableTextInput.d.ts +47 -0
  75. package/dist/views/metadata-admin/inspectors/VariableTextInput.js +95 -0
  76. package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +6 -1
  77. package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +16 -1
  78. package/dist/views/metadata-admin/inspectors/flow-node-config.js +21 -10
  79. package/dist/views/metadata-admin/inspectors/flow-ref-check.d.ts +39 -0
  80. package/dist/views/metadata-admin/inspectors/flow-ref-check.js +114 -0
  81. package/dist/views/metadata-admin/inspectors/flow-scope.d.ts +109 -0
  82. package/dist/views/metadata-admin/inspectors/flow-scope.js +199 -0
  83. package/dist/views/metadata-admin/inspectors/useDatasetFields.d.ts +14 -3
  84. package/dist/views/metadata-admin/inspectors/useDatasetFields.js +0 -0
  85. package/dist/views/metadata-admin/inspectors/useFlowScope.d.ts +23 -0
  86. package/dist/views/metadata-admin/inspectors/useFlowScope.js +45 -0
  87. package/dist/views/metadata-admin/package-scope.d.ts +9 -19
  88. package/dist/views/metadata-admin/package-scope.js +11 -25
  89. package/dist/views/metadata-admin/preview-registry.d.ts +12 -0
  90. package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +22 -3
  91. package/dist/views/metadata-admin/previews/FlowCanvas.js +45 -6
  92. package/dist/views/metadata-admin/previews/FlowPreview.d.ts +1 -1
  93. package/dist/views/metadata-admin/previews/FlowPreview.js +42 -30
  94. package/dist/views/metadata-admin/previews/ObjectFormCanvas.js +9 -4
  95. package/dist/views/metadata-admin/previews/ProblemsPanel.d.ts +18 -0
  96. package/dist/views/metadata-admin/previews/ProblemsPanel.js +27 -0
  97. package/dist/views/metadata-admin/previews/ReportPreview.d.ts +9 -8
  98. package/dist/views/metadata-admin/previews/ReportPreview.js +33 -16
  99. package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +9 -1
  100. package/dist/views/metadata-admin/previews/flow-canvas-parts.js +5 -3
  101. package/dist/views/metadata-admin/previews/flow-expr-problems.d.ts +19 -0
  102. package/dist/views/metadata-admin/previews/flow-expr-problems.js +97 -0
  103. package/dist/views/metadata-admin/previews/flow-problems.d.ts +84 -0
  104. package/dist/views/metadata-admin/previews/flow-problems.js +209 -0
  105. package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +9 -0
  106. package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +4 -2
  107. package/package.json +38 -38
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  /**
3
3
  * ObjectUI
4
4
  * Copyright (c) 2024-present ObjectStack Inc.
@@ -16,7 +16,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
16
16
  import { useEffect, useState } from 'react';
17
17
  import { useLocation, useNavigate } from 'react-router-dom';
18
18
  import { Eye, X, Rocket, GitCompareArrows } from 'lucide-react';
19
- import { Button } from '@object-ui/components';
19
+ import { Button, cn } from '@object-ui/components';
20
20
  import { useObjectTranslation } from '@object-ui/i18n';
21
21
  import { usePreviewDrafts, markPreviewExit, PREVIEW_QUERY_FLAG } from './PreviewModeContext';
22
22
  import { usePublishAllDrafts } from './usePublishAllDrafts';
@@ -78,9 +78,22 @@ export function DraftPreviewBar() {
78
78
  }
79
79
  catch { /* ignore */ } }, 300);
80
80
  };
81
- return (_jsxs("div", { className: "sticky top-0 z-40 flex items-center gap-3 border-b border-amber-300/70 bg-amber-50 px-4 py-2 text-sm text-amber-900 dark:border-amber-700/60 dark:bg-amber-950/40 dark:text-amber-200", "data-testid": "draft-preview-bar", children: [_jsx(Eye, { className: "h-4 w-4 shrink-0" }), _jsx("p", { className: "min-w-0 flex-1 truncate", children: t('preview.draftBar.message', {
82
- defaultValue: 'Draft preview you are seeing unpublished changes. Nothing here is live until you publish.',
83
- }) }), _jsxs(Button, { size: "sm", variant: "outline", onClick: () => setChangesOpen(true), "data-testid": "draft-preview-changes", children: [_jsx(GitCompareArrows, { className: "mr-1 h-3.5 w-3.5" }), t('preview.draftBar.changes', { defaultValue: 'Changes' }), typeof pendingCount === 'number' ? ` (${pendingCount})` : ''] }), _jsxs(Button, { size: "sm", onClick: publish, disabled: publishing, "data-testid": "draft-preview-publish", children: [_jsx(Rocket, { className: "mr-1 h-3.5 w-3.5" }), publishing
84
- ? t('preview.draftBar.publishing', { defaultValue: 'Publishing…' })
85
- : t('preview.draftBar.publish', { defaultValue: 'Publish' })] }), _jsx(DraftChangesPanel, { open: changesOpen, onOpenChange: setChangesOpen }), _jsxs(Button, { size: "sm", variant: "outline", onClick: exit, "data-testid": "draft-preview-exit", children: [_jsx(X, { className: "mr-1 h-3.5 w-3.5" }), t('preview.draftBar.exit', { defaultValue: 'Exit preview' })] })] }));
81
+ // Under the auto-publish posture (and any time a draft preview is opened with
82
+ // nothing staged) there are zero pending drafts. Claiming "nothing is live
83
+ // until you publish" and offering a Publish button then is both false and a
84
+ // no-op, so the bar drops the publish affordance and softens to a neutral
85
+ // preview indicator. An UNKNOWN count (null still loading or the fetch
86
+ // failed) keeps the publish path: we only relax when we KNOW the count is zero.
87
+ const noChanges = pendingCount === 0;
88
+ return (_jsxs("div", { className: cn('sticky top-0 z-40 flex items-center gap-3 border-b px-4 py-2 text-sm', noChanges
89
+ ? 'border-slate-200 bg-slate-50 text-slate-700 dark:border-slate-700/60 dark:bg-slate-900/40 dark:text-slate-300'
90
+ : 'border-amber-300/70 bg-amber-50 text-amber-900 dark:border-amber-700/60 dark:bg-amber-950/40 dark:text-amber-200'), "data-testid": "draft-preview-bar", children: [_jsx(Eye, { className: "h-4 w-4 shrink-0" }), _jsx("p", { className: "min-w-0 flex-1 truncate", children: noChanges
91
+ ? t('preview.draftBar.messageClean', {
92
+ defaultValue: 'Draft preview — no unpublished changes; everything here is already live.',
93
+ })
94
+ : t('preview.draftBar.message', {
95
+ defaultValue: 'Draft preview — you are seeing unpublished changes. Nothing here is live until you publish.',
96
+ }) }), !noChanges && (_jsxs(_Fragment, { children: [_jsxs(Button, { size: "sm", variant: "outline", onClick: () => setChangesOpen(true), "data-testid": "draft-preview-changes", children: [_jsx(GitCompareArrows, { className: "mr-1 h-3.5 w-3.5" }), t('preview.draftBar.changes', { defaultValue: 'Changes' }), typeof pendingCount === 'number' ? ` (${pendingCount})` : ''] }), _jsxs(Button, { size: "sm", onClick: publish, disabled: publishing, "data-testid": "draft-preview-publish", children: [_jsx(Rocket, { className: "mr-1 h-3.5 w-3.5" }), publishing
97
+ ? t('preview.draftBar.publishing', { defaultValue: 'Publishing…' })
98
+ : t('preview.draftBar.publish', { defaultValue: 'Publish' })] }), _jsx(DraftChangesPanel, { open: changesOpen, onOpenChange: setChangesOpen })] })), _jsxs(Button, { size: "sm", variant: "outline", onClick: exit, "data-testid": "draft-preview-exit", children: [_jsx(X, { className: "mr-1 h-3.5 w-3.5" }), t('preview.draftBar.exit', { defaultValue: 'Exit preview' })] })] }));
86
99
  }
@@ -20,14 +20,19 @@ import { PredicateScopeProvider } from '@object-ui/react';
20
20
  const ExprCtx = createContext(null);
21
21
  export function ExpressionProvider({ children, user = {}, app = {}, data = {}, features = {} }) {
22
22
  const value = useMemo(() => {
23
- const context = { user, app, data, features };
23
+ // ADR-0068: expose the SAME user object under the canonical `current_user`
24
+ // plus the back-compat `user` alias and the server-RLS-parity `ctx.user`
25
+ // alias, so a predicate authored against any one form evaluates identically
26
+ // on client, server-formula, and server-RLS.
27
+ const context = { current_user: user, user, ctx: { user }, app, data, features };
24
28
  const evaluator = new ExpressionEvaluator(context);
25
29
  return { user, app, data, features, evaluator };
26
30
  }, [user, app, data, features]);
27
31
  // Also feed the predicate scope used by useCondition/useExpression in
28
32
  // @object-ui/react so action visibility predicates (e.g. on toolbar
29
33
  // buttons) can see deployment-level flags like features.multiOrgEnabled.
30
- const scope = useMemo(() => ({ user, app, data, features }), [user, app, data, features]);
34
+ // Mirror the canonical `current_user`/`user`/`ctx.user` aliases here too.
35
+ const scope = useMemo(() => ({ current_user: user, user, ctx: { user }, app, data, features }), [user, app, data, features]);
31
36
  return (_jsx(ExprCtx.Provider, { value: value, children: _jsx(PredicateScopeProvider, { scope: scope, children: children }) }));
32
37
  }
33
38
  /**
@@ -39,7 +44,8 @@ export function useExpressionContext() {
39
44
  if (!ctx) {
40
45
  // Return a safe default so components can be used outside the provider
41
46
  const fallback = { user: {}, app: {}, data: {}, features: {} };
42
- return { ...fallback, evaluator: new ExpressionEvaluator(fallback) };
47
+ const evalContext = { current_user: {}, ctx: { user: {} }, ...fallback };
48
+ return { ...fallback, evaluator: new ExpressionEvaluator(evalContext) };
43
49
  }
44
50
  return ctx;
45
51
  }
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Utility functions for ObjectStack Console
3
3
  */
4
- export { resolveRecordFormTarget, } from './recordFormNavigation';
5
- export type { ObjectDefinitionForNavigation, RecordFormTarget, } from './recordFormNavigation';
4
+ export { resolveRecordFormTarget, resolveFormViewLayout, } from './recordFormNavigation';
5
+ export type { ObjectDefinitionForNavigation, RecordFormTarget, ObjectDefinitionForFormView, FormViewDefinition, FormViewModalLayout, } from './recordFormNavigation';
6
6
  export { deriveRelatedLists } from './deriveRelatedLists';
7
7
  export type { DerivedRelatedList } from './deriveRelatedLists';
8
8
  export { preferLocal } from './preferLocal';
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Utility functions for ObjectStack Console
3
3
  */
4
- export { resolveRecordFormTarget, } from './recordFormNavigation';
4
+ export { resolveRecordFormTarget, resolveFormViewLayout, } from './recordFormNavigation';
5
5
  export { deriveRelatedLists } from './deriveRelatedLists';
6
6
  export { preferLocal } from './preferLocal';
7
7
  export { appRouteSegment, matchAppBySegment } from './appRoute';
@@ -32,5 +32,5 @@ export interface ManagedByEmptyState {
32
32
  * `defaultValue` used as the English fallback when a locale lacks the key).
33
33
  */
34
34
  type TranslateFn = (key: string, options?: Record<string, unknown>) => string;
35
- export declare function resolveManagedByEmptyState(managedBy: string | undefined | null, t: TranslateFn): ManagedByEmptyState | undefined;
35
+ export declare function resolveManagedByEmptyState(managedBy: string | undefined | null, t: TranslateFn, objectName?: string | null): ManagedByEmptyState | undefined;
36
36
  export {};
@@ -1,4 +1,4 @@
1
- export function resolveManagedByEmptyState(managedBy, t) {
1
+ export function resolveManagedByEmptyState(managedBy, t, objectName) {
2
2
  switch (managedBy) {
3
3
  case 'system':
4
4
  return {
@@ -17,11 +17,29 @@ export function resolveManagedByEmptyState(managedBy, t) {
17
17
  }),
18
18
  };
19
19
  case 'better-auth':
20
+ // `sys_user` is the one identity table with a concrete onboarding
21
+ // answer, so give it actionable guidance: teammates arrive via an
22
+ // org-level invite + SSO just-in-time provisioning (ADR-0024 D9), and
23
+ // app end-users self-register. We deliberately do NOT name the env-level
24
+ // "Invite User" action — it is multi-org-gated and hidden in single-org —
25
+ // nor a "Reset Password" toolbar action, which does not exist (cloud#580).
26
+ // Every other identity table (sessions, accounts, tokens, jwks,
27
+ // verifications, …) is written purely by auth flows, so keep the generic
28
+ // copy — naming an invite/signup CTA on a token list would be wrong.
29
+ if (objectName === 'sys_user') {
30
+ return {
31
+ icon: 'ShieldAlert',
32
+ title: t('list.managedBy.betterAuthUser.title', { defaultValue: 'No users yet' }),
33
+ message: t('list.managedBy.betterAuthUser.message', {
34
+ defaultValue: 'User accounts are provisioned by the authentication provider, not created here. Invite teammates to your organization and they appear automatically on first sign-in (SSO just-in-time provisioning). App end-users arrive when they sign up through your app.',
35
+ }),
36
+ };
37
+ }
20
38
  return {
21
39
  icon: 'ShieldAlert',
22
40
  title: t('list.managedBy.betterAuth.title', { defaultValue: 'No identity records' }),
23
41
  message: t('list.managedBy.betterAuth.message', {
24
- defaultValue: 'Identity rows are managed by the authentication provider. Use the dedicated identity workflows (Invite User, Reset Password, …) to create new entries.',
42
+ defaultValue: 'These records are created by the authentication provider through sign-in, provisioning, and security flows not added by hand here.',
25
43
  }),
26
44
  };
27
45
  default:
@@ -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, }) {
@@ -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
@@ -1166,7 +1189,7 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }) {
1166
1189
  virtualScroll: viewDef.virtualScroll ?? listSchema.virtualScroll,
1167
1190
  emptyState: viewEmptyState(objectDef.name, viewDef.name || viewDef.id || '', viewDef.emptyState
1168
1191
  ?? listSchema.emptyState
1169
- ?? resolveManagedByEmptyState(objectDef?.managedBy, t)),
1192
+ ?? resolveManagedByEmptyState(objectDef?.managedBy, t, objectDef.name)),
1170
1193
  aria: viewDef.aria ?? listSchema.aria,
1171
1194
  // Propagate filter/sort as default filters/sort for data flow
1172
1195
  ...((() => {
@@ -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,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;