@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
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * EnvironmentEntitlementDialog — a friendly upgrade / limit dialog shown instead
4
+ * of a raw red error toast when an environment-create is gated by plan or
5
+ * capacity (DEV_ENV_PLAN_LOCKED / DEV_ENV_LIMIT / PRODUCTION_ENV_LIMIT).
6
+ *
7
+ * Driven by a single {@link EntitlementDialogSpec}, opened from two places:
8
+ * • proactively, from the env-list toolbar (a free-plan org clicking
9
+ * "Add environment" — see EnvironmentListToolbar), and
10
+ * • reactively, from the action runtime's apiHandler when the create POST
11
+ * comes back with an entitlement 403 (the safety net).
12
+ *
13
+ * The CTA renders as an anchor (not an SPA navigation) so a control-plane URL
14
+ * like `/settings/billing` always lands on the real page regardless of the
15
+ * console's own router. Relative URLs resolve against the control-plane origin
16
+ * (`apiBase`); absolute / mailto URLs are used as-is.
17
+ */
18
+ import { AlertDialog, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogCancel, Button, } from '@object-ui/components';
19
+ /** Resolve a CTA URL to a concrete href + whether it should open in a new tab. */
20
+ export function resolveCtaHref(url, apiBase) {
21
+ if (/^https?:\/\//i.test(url) || url.startsWith('mailto:')) {
22
+ return { href: url, external: !url.startsWith('mailto:') };
23
+ }
24
+ const base = (apiBase || '').replace(/\/+$/, '');
25
+ // A control-plane-relative path: prefix the API origin when we have one (dev:
26
+ // split SPA + backend). Empty base → same-origin relative (prod).
27
+ return { href: `${base}${url}`, external: Boolean(base) };
28
+ }
29
+ function CtaButton({ cta, apiBase, primary, onNavigate, }) {
30
+ const { href, external } = resolveCtaHref(cta.url, apiBase);
31
+ return (_jsx(Button, { asChild: true, variant: primary ? 'default' : 'outline', size: "sm", children: _jsx("a", { href: href, onClick: onNavigate, ...(external ? { target: '_blank', rel: 'noopener noreferrer' } : {}), "data-testid": `entitlement-cta-${primary ? 'primary' : 'secondary'}`, children: cta.label }) }));
32
+ }
33
+ export function EnvironmentEntitlementDialog({ state, apiBase, onOpenChange }) {
34
+ const spec = state.spec;
35
+ return (_jsx(AlertDialog, { open: state.open, onOpenChange: (open) => { if (!open)
36
+ onOpenChange(false); }, children: _jsxs(AlertDialogContent, { "data-testid": "environment-entitlement-dialog", children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: spec?.title }), _jsx(AlertDialogDescription, { children: spec?.message })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Close" }), spec?.secondaryCta && (_jsx(CtaButton, { cta: spec.secondaryCta, apiBase: apiBase, primary: false, onNavigate: () => onOpenChange(false) })), spec?.cta && (_jsx(CtaButton, { cta: spec.cta, apiBase: apiBase, primary: true, onNavigate: () => onOpenChange(false) }))] })] }) }));
37
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * EnvironmentListToolbar — the state-aware replacement for the generic
3
+ * `list_toolbar` action bar on the `sys_environment` list.
4
+ *
5
+ * The cloud serves ONE `create_environment` action; born-with-env makes its
6
+ * meaning depend on org state (which the action metadata can't express). This
7
+ * component reads the resolved entitlement state and renders the right
8
+ * affordance:
9
+ * • no production env → "Set up your production environment" (primary);
10
+ * the create POST provisions the org's one
11
+ * production env — the historical-data path that
12
+ * must never error.
13
+ * • has prod + dev allowed → "Add development environment" (the create POST
14
+ * makes a dev env).
15
+ * • has prod + dev NOT allowed → "Add environment" that opens an UPGRADE prompt
16
+ * instead of POST-ing into a 403.
17
+ * • still resolving / unknown → the action's default label (neutral), with the
18
+ * apiHandler entitlement dialog as the safety net.
19
+ *
20
+ * Create flows reuse the standard `action:bar` runner (name modal → apiHandler),
21
+ * so only the label/variant changes — no duplicate POST logic here.
22
+ */
23
+ import { type EntitlementDialogSpec, type EnvironmentEntitlementsState } from './entitlements';
24
+ interface Props {
25
+ /** Toolbar actions already localized by the caller (ObjectView). */
26
+ actions: any[];
27
+ /** Resolved entitlement state, or null while still loading. */
28
+ entitlements: EnvironmentEntitlementsState | null;
29
+ /** Open the shared entitlement dialog (proactive upgrade prompt). */
30
+ onUpgrade: (spec: EntitlementDialogSpec) => void;
31
+ }
32
+ export declare function EnvironmentListToolbar({ actions, entitlements, onUpgrade }: Props): import("react").JSX.Element | null;
33
+ export {};
@@ -0,0 +1,59 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * EnvironmentListToolbar — the state-aware replacement for the generic
4
+ * `list_toolbar` action bar on the `sys_environment` list.
5
+ *
6
+ * The cloud serves ONE `create_environment` action; born-with-env makes its
7
+ * meaning depend on org state (which the action metadata can't express). This
8
+ * component reads the resolved entitlement state and renders the right
9
+ * affordance:
10
+ * • no production env → "Set up your production environment" (primary);
11
+ * the create POST provisions the org's one
12
+ * production env — the historical-data path that
13
+ * must never error.
14
+ * • has prod + dev allowed → "Add development environment" (the create POST
15
+ * makes a dev env).
16
+ * • has prod + dev NOT allowed → "Add environment" that opens an UPGRADE prompt
17
+ * instead of POST-ing into a 403.
18
+ * • still resolving / unknown → the action's default label (neutral), with the
19
+ * apiHandler entitlement dialog as the safety net.
20
+ *
21
+ * Create flows reuse the standard `action:bar` runner (name modal → apiHandler),
22
+ * so only the label/variant changes — no duplicate POST logic here.
23
+ */
24
+ import { SchemaRenderer } from '@object-ui/react';
25
+ import { Button } from '@object-ui/components';
26
+ import { Plus } from 'lucide-react';
27
+ import { decideEnvironmentCta, upgradeDialogSpec, } from './entitlements';
28
+ const CREATE_ACTION = 'create_environment';
29
+ export function EnvironmentListToolbar({ actions, entitlements, onUpgrade }) {
30
+ const toolbarActions = (actions || []).filter((a) => a?.locations?.includes('list_toolbar'));
31
+ if (toolbarActions.length === 0)
32
+ return null;
33
+ const ctaKind = entitlements?.ready ? decideEnvironmentCta(entitlements) : null;
34
+ // Upgrade state: a free-plan org clicking "create" must NOT POST-then-403.
35
+ // Render a primary button that opens the upgrade prompt, plus any other
36
+ // (non-create) toolbar actions through the normal bar.
37
+ if (ctaKind === 'upgrade_for_development') {
38
+ const others = toolbarActions.filter((a) => a?.name !== CREATE_ACTION);
39
+ return (_jsxs(_Fragment, { children: [others.length > 0 && (_jsx(SchemaRenderer, { schema: { type: 'action:bar', location: 'list_toolbar', actions: others, size: 'sm', variant: 'outline' } })), _jsxs(Button, { size: "sm", onClick: () => onUpgrade(upgradeDialogSpec(entitlements)), className: "shadow-none gap-1.5 sm:gap-2 h-8 sm:h-9", "data-testid": "environment-add-upgrade", children: [_jsx(Plus, { className: "h-4 w-4" }), _jsx("span", { children: "Add environment" })] })] }));
40
+ }
41
+ // setup_production / add_development / loading: render the bar, overriding only
42
+ // the create action's label (and promoting production setup to a primary CTA).
43
+ const renderedActions = toolbarActions.map((a) => {
44
+ if (a?.name !== CREATE_ACTION || ctaKind == null)
45
+ return a;
46
+ if (ctaKind === 'setup_production') {
47
+ return { ...a, label: 'Set up your production environment', variant: 'primary' };
48
+ }
49
+ // add_development
50
+ return { ...a, label: 'Add development environment' };
51
+ });
52
+ return (_jsx(SchemaRenderer, { schema: {
53
+ type: 'action:bar',
54
+ location: 'list_toolbar',
55
+ actions: renderedActions,
56
+ size: 'sm',
57
+ variant: 'outline',
58
+ } }));
59
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Environment entitlement logic — the React-free decision layer behind the
3
+ * state-aware "create environment" affordance and the entitlement-error dialog.
4
+ *
5
+ * The Console environment list (`sys_environment`) renders a single
6
+ * `create_environment` toolbar action. Whether that should read "Set up your
7
+ * production environment", "Add development environment", or open an upgrade
8
+ * prompt depends on org state the action metadata can't express (does the org
9
+ * already own its one production env? is its plan allowed development envs?).
10
+ * This module turns the org-scoped capacity summary
11
+ * (GET /cloud/environment-entitlements) — with a row-derived fallback — into
12
+ * that decision, and maps the cloud env-create 403 bodies to a friendly dialog
13
+ * so a confused user never sees a raw red error toast.
14
+ *
15
+ * Kept dependency-free so it is trivially unit-testable.
16
+ */
17
+ export type EntitlementErrorCode = 'DEV_ENV_PLAN_LOCKED' | 'DEV_ENV_LIMIT' | 'PRODUCTION_ENV_LIMIT';
18
+ export declare function isEntitlementErrorCode(code: unknown): code is EntitlementErrorCode;
19
+ /** A CTA link rendered in the entitlement dialog. */
20
+ export interface EntitlementCta {
21
+ label: string;
22
+ /** Absolute (http/mailto) or a control-plane-relative path (e.g. `/settings/billing`). */
23
+ url: string;
24
+ }
25
+ /** Declarative spec the {@link EnvironmentEntitlementDialog} renders. */
26
+ export interface EntitlementDialogSpec {
27
+ code: string;
28
+ title: string;
29
+ message: string;
30
+ /** Primary CTA (e.g. Upgrade plan). */
31
+ cta?: EntitlementCta;
32
+ /** Secondary CTA (e.g. Contact sales). */
33
+ secondaryCta?: EntitlementCta;
34
+ }
35
+ export declare const DEFAULT_UPGRADE_URL = "/settings/billing";
36
+ /**
37
+ * Map a cloud env-create 403 body
38
+ * (`{ error, code, upgrade_url, contact_url, plan, current, limit }`) to a
39
+ * dialog spec. Returns `null` for any non-entitlement error so the caller falls
40
+ * back to its normal error handling (a red toast). This is the safety net: it
41
+ * fires regardless of whether the up-front state-aware presentation was right.
42
+ */
43
+ export declare function entitlementDialogFromError(body: any): EntitlementDialogSpec | null;
44
+ /** Server summary shape (GET /cloud/environment-entitlements → `data`). */
45
+ export interface EnvironmentEntitlementsSummary {
46
+ plan?: string;
47
+ hasProductionEnv?: boolean;
48
+ production?: {
49
+ used: number;
50
+ limit: number;
51
+ canCreate: boolean;
52
+ };
53
+ development?: {
54
+ used: number;
55
+ limit: number;
56
+ canCreate: boolean;
57
+ };
58
+ seatCount?: number;
59
+ upgradeUrl?: string;
60
+ contactSalesUrl?: string;
61
+ }
62
+ /** Combined client state (authoritative summary, or a row-derived fallback). */
63
+ export interface EnvironmentEntitlementsState {
64
+ /** True once a usable signal exists (summary OR derived rows). */
65
+ ready: boolean;
66
+ hasProductionEnv: boolean;
67
+ /** Authoritative dev-create capability; `undefined` when unknown (no summary). */
68
+ canCreateDevelopmentEnv?: boolean;
69
+ plan?: string;
70
+ upgradeUrl: string;
71
+ contactSalesUrl?: string;
72
+ /** Where the signal came from — telemetry + degradation note + tests. */
73
+ source: 'summary' | 'derived' | 'unknown';
74
+ }
75
+ export type EnvironmentCtaKind = 'setup_production' | 'add_development' | 'upgrade_for_development';
76
+ /**
77
+ * Decide which toolbar affordance to present:
78
+ * • no production env → set up production (the create POST makes one;
79
+ * the critical historical-data path — never errors)
80
+ * • has prod + dev allowed → add development (POST makes a dev env)
81
+ * • has prod + dev NOT allowed → upgrade prompt (no POST)
82
+ * • has prod + dev unknown → add development (let the POST + dialog decide)
83
+ */
84
+ export declare function decideEnvironmentCta(state: EnvironmentEntitlementsState): EnvironmentCtaKind;
85
+ /**
86
+ * The proactive (pre-POST) upgrade dialog shown when a free-plan org clicks
87
+ * "Add environment" but development envs aren't in its plan. Mirrors the copy
88
+ * of the reactive DEV_ENV_PLAN_LOCKED error so both paths read identically.
89
+ */
90
+ export declare function upgradeDialogSpec(state: EnvironmentEntitlementsState): EntitlementDialogSpec;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Environment entitlement logic — the React-free decision layer behind the
3
+ * state-aware "create environment" affordance and the entitlement-error dialog.
4
+ *
5
+ * The Console environment list (`sys_environment`) renders a single
6
+ * `create_environment` toolbar action. Whether that should read "Set up your
7
+ * production environment", "Add development environment", or open an upgrade
8
+ * prompt depends on org state the action metadata can't express (does the org
9
+ * already own its one production env? is its plan allowed development envs?).
10
+ * This module turns the org-scoped capacity summary
11
+ * (GET /cloud/environment-entitlements) — with a row-derived fallback — into
12
+ * that decision, and maps the cloud env-create 403 bodies to a friendly dialog
13
+ * so a confused user never sees a raw red error toast.
14
+ *
15
+ * Kept dependency-free so it is trivially unit-testable.
16
+ */
17
+ const ENTITLEMENT_ERROR_CODES = new Set([
18
+ 'DEV_ENV_PLAN_LOCKED',
19
+ 'DEV_ENV_LIMIT',
20
+ 'PRODUCTION_ENV_LIMIT',
21
+ ]);
22
+ export function isEntitlementErrorCode(code) {
23
+ return typeof code === 'string' && ENTITLEMENT_ERROR_CODES.has(code);
24
+ }
25
+ export const DEFAULT_UPGRADE_URL = '/settings/billing';
26
+ /**
27
+ * Map a cloud env-create 403 body
28
+ * (`{ error, code, upgrade_url, contact_url, plan, current, limit }`) to a
29
+ * dialog spec. Returns `null` for any non-entitlement error so the caller falls
30
+ * back to its normal error handling (a red toast). This is the safety net: it
31
+ * fires regardless of whether the up-front state-aware presentation was right.
32
+ */
33
+ export function entitlementDialogFromError(body) {
34
+ const code = body?.code;
35
+ if (!isEntitlementErrorCode(code))
36
+ return null;
37
+ const serverMessage = typeof body?.error === 'string' && body.error ? body.error
38
+ : typeof body?.message === 'string' ? body.message : '';
39
+ const upgradeUrl = typeof body?.upgrade_url === 'string' && body.upgrade_url ? body.upgrade_url : DEFAULT_UPGRADE_URL;
40
+ const contactUrl = typeof body?.contact_url === 'string' && body.contact_url ? body.contact_url : '';
41
+ if (code === 'PRODUCTION_ENV_LIMIT') {
42
+ return {
43
+ code,
44
+ title: 'You already have your production environment',
45
+ message: serverMessage ||
46
+ 'Each organization includes exactly one production environment. Create a separate organization for another, or contact us about an Enterprise arrangement.',
47
+ cta: contactUrl ? { label: 'Contact sales', url: contactUrl } : undefined,
48
+ };
49
+ }
50
+ // DEV_ENV_PLAN_LOCKED / DEV_ENV_LIMIT — both resolve via an upgrade CTA.
51
+ return {
52
+ code,
53
+ title: code === 'DEV_ENV_PLAN_LOCKED'
54
+ ? 'Development environments are a paid feature'
55
+ : 'Development environment limit reached',
56
+ message: serverMessage ||
57
+ (code === 'DEV_ENV_PLAN_LOCKED'
58
+ ? 'Your free plan includes one production environment. Upgrade to add development environments — build in dev, then publish to production.'
59
+ : 'Capacity scales with AI seats. Add an AI seat, or archive an unused development environment to free one up.'),
60
+ cta: { label: 'Upgrade plan', url: upgradeUrl },
61
+ };
62
+ }
63
+ /**
64
+ * Decide which toolbar affordance to present:
65
+ * • no production env → set up production (the create POST makes one;
66
+ * the critical historical-data path — never errors)
67
+ * • has prod + dev allowed → add development (POST makes a dev env)
68
+ * • has prod + dev NOT allowed → upgrade prompt (no POST)
69
+ * • has prod + dev unknown → add development (let the POST + dialog decide)
70
+ */
71
+ export function decideEnvironmentCta(state) {
72
+ if (!state.hasProductionEnv)
73
+ return 'setup_production';
74
+ if (state.canCreateDevelopmentEnv === false)
75
+ return 'upgrade_for_development';
76
+ return 'add_development';
77
+ }
78
+ /**
79
+ * The proactive (pre-POST) upgrade dialog shown when a free-plan org clicks
80
+ * "Add environment" but development envs aren't in its plan. Mirrors the copy
81
+ * of the reactive DEV_ENV_PLAN_LOCKED error so both paths read identically.
82
+ */
83
+ export function upgradeDialogSpec(state) {
84
+ const planLabel = state.plan && state.plan !== 'free' ? `your ${state.plan} plan` : 'your free plan';
85
+ return {
86
+ code: 'DEV_ENV_PLAN_LOCKED',
87
+ title: 'Development environments are a paid feature',
88
+ message: `${planLabel} includes one production environment. Upgrade to add development environments — build in dev, then publish to production.`,
89
+ cta: { label: 'Upgrade plan', url: state.upgradeUrl || DEFAULT_UPGRADE_URL },
90
+ };
91
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * useEnvironmentEntitlements — resolve the org's environment-capacity state so
3
+ * the `sys_environment` list can present the right "create" affordance up front.
4
+ *
5
+ * Two signals, in priority order:
6
+ * 1. AUTHORITATIVE — GET /cloud/environment-entitlements (org-scoped, computed
7
+ * by the same helper the create guard uses). Gives plan + dev-create
8
+ * capability precisely, including the seat-scaled / subscription cases the
9
+ * client can't derive from rows.
10
+ * 2. FALLBACK — when that endpoint is unavailable (older control plane / error),
11
+ * derive `hasProductionEnv` from the org's env rows via the data API (which
12
+ * is org-scoped on the control plane). This keeps the critical
13
+ * "set up your production environment" path working without a backend deploy;
14
+ * free-vs-paid is left unknown, and a stray create POST is caught by the
15
+ * entitlement dialog safety net.
16
+ *
17
+ * Returns `null` when disabled (not the environment list) so callers can cheaply
18
+ * branch. Re-resolves when `refreshKey` changes (e.g. after a create).
19
+ */
20
+ import { type EnvironmentEntitlementsState } from './entitlements';
21
+ export interface UseEnvironmentEntitlementsOptions {
22
+ /** Only fetch when this is the environment list (objectName === 'sys_environment'). */
23
+ enabled: boolean;
24
+ dataSource: any;
25
+ /** Authenticated fetch (Bearer + tenant + cookies) — from the action runtime. */
26
+ authFetch: (url: string, init?: any) => Promise<Response>;
27
+ /** Control-plane origin (VITE_SERVER_URL); '' in same-origin production. */
28
+ apiBase: string;
29
+ /** Bump to re-resolve (e.g. the list's refreshKey after a successful create). */
30
+ refreshKey?: unknown;
31
+ }
32
+ export declare function useEnvironmentEntitlements(opts: UseEnvironmentEntitlementsOptions): EnvironmentEntitlementsState | null;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * useEnvironmentEntitlements — resolve the org's environment-capacity state so
3
+ * the `sys_environment` list can present the right "create" affordance up front.
4
+ *
5
+ * Two signals, in priority order:
6
+ * 1. AUTHORITATIVE — GET /cloud/environment-entitlements (org-scoped, computed
7
+ * by the same helper the create guard uses). Gives plan + dev-create
8
+ * capability precisely, including the seat-scaled / subscription cases the
9
+ * client can't derive from rows.
10
+ * 2. FALLBACK — when that endpoint is unavailable (older control plane / error),
11
+ * derive `hasProductionEnv` from the org's env rows via the data API (which
12
+ * is org-scoped on the control plane). This keeps the critical
13
+ * "set up your production environment" path working without a backend deploy;
14
+ * free-vs-paid is left unknown, and a stray create POST is caught by the
15
+ * entitlement dialog safety net.
16
+ *
17
+ * Returns `null` when disabled (not the environment list) so callers can cheaply
18
+ * branch. Re-resolves when `refreshKey` changes (e.g. after a create).
19
+ */
20
+ import { useEffect, useState } from 'react';
21
+ import { useAuth } from '@object-ui/auth';
22
+ import { DEFAULT_UPGRADE_URL, } from './entitlements';
23
+ const TERMINAL_STATUSES = new Set(['archived', 'failed']);
24
+ /** Mirror of the server `classifyEnvironmentType`: explicit type, else default→prod. */
25
+ function classifyEnvironmentType(row) {
26
+ const t = String(row?.environment_type ?? '').trim().toLowerCase();
27
+ if (t === 'production' || t === 'prod')
28
+ return 'production';
29
+ if (t)
30
+ return 'development';
31
+ return row?.is_default === true || row?.is_default === 1 ? 'production' : 'development';
32
+ }
33
+ export function useEnvironmentEntitlements(opts) {
34
+ const { enabled, dataSource, authFetch, apiBase, refreshKey } = opts;
35
+ const { activeOrganization } = useAuth();
36
+ const orgId = activeOrganization?.id;
37
+ const [state, setState] = useState(null);
38
+ useEffect(() => {
39
+ if (!enabled) {
40
+ setState(null);
41
+ return;
42
+ }
43
+ let cancelled = false;
44
+ const fromSummary = async () => {
45
+ try {
46
+ const base = (apiBase || '').replace(/\/+$/, '');
47
+ const qs = orgId ? `?organizationId=${encodeURIComponent(orgId)}` : '';
48
+ const res = await authFetch(`${base}/api/v1/cloud/environment-entitlements${qs}`, {
49
+ method: 'GET',
50
+ credentials: 'include',
51
+ });
52
+ if (!res.ok)
53
+ return null;
54
+ const json = await res.json().catch(() => null);
55
+ const data = (json?.data ?? json);
56
+ if (!data || typeof data !== 'object')
57
+ return null;
58
+ return {
59
+ ready: true,
60
+ hasProductionEnv: data.hasProductionEnv === true,
61
+ canCreateDevelopmentEnv: data.development?.canCreate,
62
+ plan: data.plan,
63
+ upgradeUrl: data.upgradeUrl || DEFAULT_UPGRADE_URL,
64
+ contactSalesUrl: data.contactSalesUrl,
65
+ source: 'summary',
66
+ };
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ };
72
+ const fromRows = async () => {
73
+ try {
74
+ const params = { $top: 200 };
75
+ if (orgId)
76
+ params.$filter = { organization_id: orgId };
77
+ const res = await dataSource.find('sys_environment', params);
78
+ const rows = Array.isArray(res) ? res : res?.data ?? [];
79
+ const active = rows.filter((r) => !TERMINAL_STATUSES.has(String(r?.status ?? '')));
80
+ const hasProductionEnv = active.some((r) => classifyEnvironmentType(r) === 'production');
81
+ return {
82
+ ready: true,
83
+ hasProductionEnv,
84
+ canCreateDevelopmentEnv: undefined,
85
+ upgradeUrl: DEFAULT_UPGRADE_URL,
86
+ source: 'derived',
87
+ };
88
+ }
89
+ catch {
90
+ return {
91
+ ready: false,
92
+ hasProductionEnv: false,
93
+ canCreateDevelopmentEnv: undefined,
94
+ upgradeUrl: DEFAULT_UPGRADE_URL,
95
+ source: 'unknown',
96
+ };
97
+ }
98
+ };
99
+ (async () => {
100
+ const summary = await fromSummary();
101
+ const next = summary ?? (await fromRows());
102
+ if (!cancelled)
103
+ setState(next);
104
+ })();
105
+ return () => { cancelled = true; };
106
+ }, [enabled, orgId, apiBase, authFetch, dataSource, refreshKey]);
107
+ return state;
108
+ }
@@ -23,8 +23,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
23
23
  */
24
24
  import { useCallback, useState } from 'react';
25
25
  import { Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, cn, } from '@object-ui/components';
26
- import { SchemaRenderer } from '@object-ui/react';
26
+ import { SchemaRenderer, useMetadata } from '@object-ui/react';
27
27
  import { ModalForm } from '@object-ui/plugin-form';
28
+ import { resolveFormViewLayout } from '../utils/recordFormNavigation';
28
29
  const SIZE_CLASS = {
29
30
  sm: 'sm:max-w-sm',
30
31
  default: 'sm:max-w-lg',
@@ -56,6 +57,10 @@ export function normalizeModalSchema(schema) {
56
57
  }
57
58
  export function useActionModal(dataSource) {
58
59
  const [state, setState] = useState(null);
60
+ // Object metadata — degrades to an empty list outside a MetadataProvider
61
+ // (see useMetadata). Used to resolve the object's default form view so the
62
+ // create/edit modal honors its curated sections + field selection/order.
63
+ const { objects } = useMetadata();
59
64
  const close = useCallback((r) => {
60
65
  setState((s) => {
61
66
  s?.resolve(r);
@@ -73,6 +78,14 @@ export function useActionModal(dataSource) {
73
78
  close({ success: false });
74
79
  };
75
80
  if (d.objectName && !d.content) {
81
+ // Honor the object's default FORM VIEW (curated sections + field
82
+ // selection/order + master-detail subforms) unless the action descriptor
83
+ // passed an explicit field list. Without this the modal falls back to the
84
+ // raw object schema — every field, in schema order. Mirrors the global
85
+ // New/Edit modal in AppContent so action-opened forms stay consistent.
86
+ const viewLayout = (d.fields || d.sections)
87
+ ? {}
88
+ : resolveFormViewLayout(objects.find((o) => o?.name === d.objectName));
76
89
  modalElement = (_jsx(ModalForm, { schema: {
77
90
  type: 'object-form',
78
91
  formType: 'modal',
@@ -82,6 +95,7 @@ export function useActionModal(dataSource) {
82
95
  title: d.title,
83
96
  description: d.description,
84
97
  fields: d.fields,
98
+ ...viewLayout,
85
99
  modalSize: d.size,
86
100
  open: true,
87
101
  onOpenChange,
@@ -0,0 +1,59 @@
1
+ /**
2
+ * useAiSurfaceEnabled
3
+ *
4
+ * Single source of truth for "should the in-UI AI surface be shown — for THIS
5
+ * user, on this deployment?". The console ships under MIT and is edition- and
6
+ * seat-agnostic at build time; it decides purely at runtime from what the
7
+ * server reports — no `VITE_EDITION` flag, no tree-shake.
8
+ *
9
+ * The signal is **the agent catalog** (`GET /api/v1/ai/agents`): the surface
10
+ * shows iff that returns >= 1 agent. The catalog is the right signal because it
11
+ * is the ONLY one that is BOTH edition- AND user-aware:
12
+ *
13
+ * • The route is access-filtered server-side (ADR-0049 / ADR-0068): it returns
14
+ * only the agents the CALLER may chat. A user WITHOUT the per-user AI seat
15
+ * (the `ai_seat` permission) gets an EMPTY catalog -> the whole AI surface
16
+ * hides for them, instead of showing a button that 403s on click. The
17
+ * deployment-wide discovery `services.ai` flag CANNOT express this — it is
18
+ * identical for every user — which is exactly why we do NOT gate on it.
19
+ * • It is ALSO the honest edition signal: a Community-Edition runtime that
20
+ * ships no `@objectstack/service-ai` registers no AI service and persists no
21
+ * agents -> empty catalog -> hidden. (The old "headless service reports
22
+ * available in CE" worry is moot: empty catalog hides the surface either way.)
23
+ *
24
+ * ⚠️ Do NOT "simplify" this back to `discovery.services.ai` (isAiEnabled): that
25
+ * reintroduces the per-user gap — seat-less users would see the FAB / links and
26
+ * hit 403 on click. The per-user AI-seat gate (ADR-0068) DEPENDS on this catalog
27
+ * signal. (This reverts objectui#1992, which dropped the per-user dimension.)
28
+ *
29
+ * The `VITE_AI_BASE_URL` opt-in flows through naturally: {@link resolveAiApiBase}
30
+ * points the catalog fetch at the configured server, so an external AI server
31
+ * with reachable agents lights the surface up and an agent-less one keeps it hidden.
32
+ *
33
+ * `isLoading` is surfaced so the `/ai` route guard can wait for the catalog to
34
+ * resolve before redirecting — otherwise a stale bookmark would flash a redirect
35
+ * to home before the fetch even starts. Entry-point buttons (FAB, top-bar link,
36
+ * designer "Ask AI") ignore it: staying hidden during the brief load is the
37
+ * correct, flash-free behaviour for a control that must not appear unless AI can
38
+ * actually answer.
39
+ *
40
+ * @module
41
+ */
42
+ /**
43
+ * Resolve the AI service base URL, mirroring AiChatPage / the Home CTAs:
44
+ * an explicit `VITE_AI_BASE_URL` wins, otherwise `${VITE_SERVER_URL}/api/v1/ai`.
45
+ * Shared so every catalog fetch (route guard, layouts, Home) hits the same URL.
46
+ */
47
+ export declare function resolveAiApiBase(): string;
48
+ export interface AiSurfaceState {
49
+ /** True when the AI UI should render — the CALLER can reach >= 1 agent (access-filtered). */
50
+ enabled: boolean;
51
+ /** True until the agent catalog has resolved; route guards wait on this. */
52
+ isLoading: boolean;
53
+ }
54
+ /**
55
+ * Whether the console's AI surface (FAB, `/ai` routes, "Ask AI" affordances)
56
+ * should be shown FOR THE CURRENT USER, driven off the access-filtered agent
57
+ * catalog (empty for seat-less users -> AI hidden; ADR-0068).
58
+ */
59
+ export declare function useAiSurfaceEnabled(): AiSurfaceState;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * useAiSurfaceEnabled
3
+ *
4
+ * Single source of truth for "should the in-UI AI surface be shown — for THIS
5
+ * user, on this deployment?". The console ships under MIT and is edition- and
6
+ * seat-agnostic at build time; it decides purely at runtime from what the
7
+ * server reports — no `VITE_EDITION` flag, no tree-shake.
8
+ *
9
+ * The signal is **the agent catalog** (`GET /api/v1/ai/agents`): the surface
10
+ * shows iff that returns >= 1 agent. The catalog is the right signal because it
11
+ * is the ONLY one that is BOTH edition- AND user-aware:
12
+ *
13
+ * • The route is access-filtered server-side (ADR-0049 / ADR-0068): it returns
14
+ * only the agents the CALLER may chat. A user WITHOUT the per-user AI seat
15
+ * (the `ai_seat` permission) gets an EMPTY catalog -> the whole AI surface
16
+ * hides for them, instead of showing a button that 403s on click. The
17
+ * deployment-wide discovery `services.ai` flag CANNOT express this — it is
18
+ * identical for every user — which is exactly why we do NOT gate on it.
19
+ * • It is ALSO the honest edition signal: a Community-Edition runtime that
20
+ * ships no `@objectstack/service-ai` registers no AI service and persists no
21
+ * agents -> empty catalog -> hidden. (The old "headless service reports
22
+ * available in CE" worry is moot: empty catalog hides the surface either way.)
23
+ *
24
+ * ⚠️ Do NOT "simplify" this back to `discovery.services.ai` (isAiEnabled): that
25
+ * reintroduces the per-user gap — seat-less users would see the FAB / links and
26
+ * hit 403 on click. The per-user AI-seat gate (ADR-0068) DEPENDS on this catalog
27
+ * signal. (This reverts objectui#1992, which dropped the per-user dimension.)
28
+ *
29
+ * The `VITE_AI_BASE_URL` opt-in flows through naturally: {@link resolveAiApiBase}
30
+ * points the catalog fetch at the configured server, so an external AI server
31
+ * with reachable agents lights the surface up and an agent-less one keeps it hidden.
32
+ *
33
+ * `isLoading` is surfaced so the `/ai` route guard can wait for the catalog to
34
+ * resolve before redirecting — otherwise a stale bookmark would flash a redirect
35
+ * to home before the fetch even starts. Entry-point buttons (FAB, top-bar link,
36
+ * designer "Ask AI") ignore it: staying hidden during the brief load is the
37
+ * correct, flash-free behaviour for a control that must not appear unless AI can
38
+ * actually answer.
39
+ *
40
+ * @module
41
+ */
42
+ import { useRef } from 'react';
43
+ import { useAgents } from '@object-ui/plugin-chatbot';
44
+ /**
45
+ * Resolve the AI service base URL, mirroring AiChatPage / the Home CTAs:
46
+ * an explicit `VITE_AI_BASE_URL` wins, otherwise `${VITE_SERVER_URL}/api/v1/ai`.
47
+ * Shared so every catalog fetch (route guard, layouts, Home) hits the same URL.
48
+ */
49
+ export function resolveAiApiBase() {
50
+ const env = import.meta.env ?? {};
51
+ const fromEnv = env.VITE_AI_BASE_URL;
52
+ if (fromEnv)
53
+ return fromEnv.replace(/\/$/, '');
54
+ const serverUrl = env.VITE_SERVER_URL ?? '';
55
+ return `${serverUrl.replace(/\/$/, '')}/api/v1/ai`;
56
+ }
57
+ /**
58
+ * Whether the console's AI surface (FAB, `/ai` routes, "Ask AI" affordances)
59
+ * should be shown FOR THE CURRENT USER, driven off the access-filtered agent
60
+ * catalog (empty for seat-less users -> AI hidden; ADR-0068).
61
+ */
62
+ export function useAiSurfaceEnabled() {
63
+ const { agents, isLoading } = useAgents({ apiBase: resolveAiApiBase() });
64
+ // useAgents starts `isLoading=false` and only kicks off the fetch in an effect
65
+ // a tick later, so the first render's empty list means "not fetched yet", not
66
+ // "no agents". Latch whether a fetch has actually been in flight so the route
67
+ // guard treats that initial frame as loading (not a definitive empty → redirect).
68
+ const fetchStartedRef = useRef(false);
69
+ if (isLoading)
70
+ fetchStartedRef.current = true;
71
+ const enabled = agents.length > 0;
72
+ return {
73
+ enabled,
74
+ // Agents present → resolved/available. Otherwise we're loading until a fetch
75
+ // has both started and finished with an empty result.
76
+ isLoading: enabled ? false : isLoading || !fetchStartedRef.current,
77
+ };
78
+ }