@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.
- package/CHANGELOG.md +320 -0
- package/dist/components/ManagedByBadge.js +1 -1
- package/dist/console/AppContent.js +9 -15
- package/dist/console/ConsoleShell.d.ts +16 -0
- package/dist/console/ConsoleShell.js +43 -2
- package/dist/console/ai/AiChatPage.js +64 -14
- package/dist/console/ai/BuildDebugDrawer.d.ts +20 -0
- package/dist/console/ai/BuildDebugDrawer.js +75 -0
- package/dist/console/ai/buildDebugApi.d.ts +94 -0
- package/dist/console/ai/buildDebugApi.js +16 -0
- package/dist/console/home/HomeLayout.js +5 -7
- package/dist/console/home/HomePage.js +1 -9
- package/dist/console/organizations/CreateWorkspaceDialog.js +15 -1
- package/dist/console/organizations/OrganizationsPage.js +32 -4
- package/dist/console/organizations/manage/OrganizationLayout.js +1 -1
- package/dist/console/organizations/provisionEnvironment.d.ts +53 -0
- package/dist/console/organizations/provisionEnvironment.js +64 -0
- package/dist/environment/EnvironmentEntitlementDialog.d.ts +34 -0
- package/dist/environment/EnvironmentEntitlementDialog.js +37 -0
- package/dist/environment/EnvironmentListToolbar.d.ts +33 -0
- package/dist/environment/EnvironmentListToolbar.js +59 -0
- package/dist/environment/entitlements.d.ts +90 -0
- package/dist/environment/entitlements.js +91 -0
- package/dist/environment/useEnvironmentEntitlements.d.ts +32 -0
- package/dist/environment/useEnvironmentEntitlements.js +108 -0
- package/dist/hooks/useActionModal.js +15 -1
- package/dist/hooks/useAiSurface.d.ts +59 -0
- package/dist/hooks/useAiSurface.js +78 -0
- package/dist/hooks/useConsoleActionRuntime.d.ts +3 -0
- package/dist/hooks/useConsoleActionRuntime.js +36 -8
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -1
- package/dist/layout/AppHeader.js +30 -5
- package/dist/layout/ConsoleFloatingChatbot.js +22 -4
- package/dist/layout/ConsoleLayout.js +5 -6
- package/dist/layout/ContextSelectors.js +0 -19
- package/dist/layout/WorkspaceSwitcher.d.ts +14 -0
- package/dist/layout/WorkspaceSwitcher.js +76 -0
- package/dist/preview/DraftPreviewBar.js +20 -7
- package/dist/providers/ExpressionProvider.js +9 -3
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.js +1 -1
- package/dist/utils/managedByEmptyState.d.ts +1 -1
- package/dist/utils/managedByEmptyState.js +20 -2
- package/dist/utils/recordFormNavigation.d.ts +60 -0
- package/dist/utils/recordFormNavigation.js +35 -0
- package/dist/utils/resolvePageVarTokens.d.ts +31 -0
- package/dist/utils/resolvePageVarTokens.js +72 -0
- package/dist/views/CreateViewDialog.js +14 -1
- package/dist/views/ObjectView.js +27 -13
- package/dist/views/metadata-admin/AssignedUsersSection.d.ts +28 -0
- package/dist/views/metadata-admin/AssignedUsersSection.js +151 -0
- package/dist/views/metadata-admin/PackagesPage.d.ts +5 -0
- package/dist/views/metadata-admin/PackagesPage.js +49 -4
- package/dist/views/metadata-admin/PermissionMatrixEditor.js +2 -1
- package/dist/views/metadata-admin/ResourceEditPage.js +36 -4
- package/dist/views/metadata-admin/ResourceListPage.js +25 -10
- package/dist/views/metadata-admin/StudioHomePage.js +1 -5
- package/dist/views/metadata-admin/createBody.d.ts +26 -0
- package/dist/views/metadata-admin/createBody.js +30 -0
- package/dist/views/metadata-admin/i18n.js +20 -2
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +8 -0
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +17 -3
- package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +16 -2
- package/dist/views/metadata-admin/inspectors/FlowExprIssue.d.ts +21 -0
- package/dist/views/metadata-admin/inspectors/FlowExprIssue.js +13 -0
- package/dist/views/metadata-admin/inspectors/FlowKeyValueField.d.ts +20 -2
- package/dist/views/metadata-admin/inspectors/FlowKeyValueField.js +71 -28
- package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.d.ts +4 -1
- package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.js +24 -9
- package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +15 -3
- package/dist/views/metadata-admin/inspectors/FlowObjectListField.d.ts +4 -1
- package/dist/views/metadata-admin/inspectors/FlowObjectListField.js +8 -3
- package/dist/views/metadata-admin/inspectors/VariableTextInput.d.ts +47 -0
- package/dist/views/metadata-admin/inspectors/VariableTextInput.js +95 -0
- package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +6 -1
- package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +16 -1
- package/dist/views/metadata-admin/inspectors/flow-node-config.js +21 -10
- package/dist/views/metadata-admin/inspectors/flow-ref-check.d.ts +39 -0
- package/dist/views/metadata-admin/inspectors/flow-ref-check.js +114 -0
- package/dist/views/metadata-admin/inspectors/flow-scope.d.ts +109 -0
- package/dist/views/metadata-admin/inspectors/flow-scope.js +199 -0
- package/dist/views/metadata-admin/inspectors/useDatasetFields.d.ts +14 -3
- package/dist/views/metadata-admin/inspectors/useDatasetFields.js +0 -0
- package/dist/views/metadata-admin/inspectors/useFlowScope.d.ts +23 -0
- package/dist/views/metadata-admin/inspectors/useFlowScope.js +45 -0
- package/dist/views/metadata-admin/package-scope.d.ts +9 -19
- package/dist/views/metadata-admin/package-scope.js +11 -25
- package/dist/views/metadata-admin/preview-registry.d.ts +12 -0
- package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +22 -3
- package/dist/views/metadata-admin/previews/FlowCanvas.js +45 -6
- package/dist/views/metadata-admin/previews/FlowPreview.d.ts +1 -1
- package/dist/views/metadata-admin/previews/FlowPreview.js +42 -30
- package/dist/views/metadata-admin/previews/ObjectFormCanvas.js +9 -4
- package/dist/views/metadata-admin/previews/ProblemsPanel.d.ts +18 -0
- package/dist/views/metadata-admin/previews/ProblemsPanel.js +27 -0
- package/dist/views/metadata-admin/previews/ReportPreview.d.ts +9 -8
- package/dist/views/metadata-admin/previews/ReportPreview.js +33 -16
- package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +9 -1
- package/dist/views/metadata-admin/previews/flow-canvas-parts.js +5 -3
- package/dist/views/metadata-admin/previews/flow-expr-problems.d.ts +19 -0
- package/dist/views/metadata-admin/previews/flow-expr-problems.js +97 -0
- package/dist/views/metadata-admin/previews/flow-problems.d.ts +84 -0
- package/dist/views/metadata-admin/previews/flow-problems.js +209 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +9 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +4 -2
- package/package.json +38 -38
|
@@ -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
|
+
}
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import React from 'react';
|
|
24
24
|
import { createAuthenticatedFetch } from '@object-ui/auth';
|
|
25
25
|
import type { ActionContext, ActionDef, ActionResult, ConfirmationHandler, NavigationHandler, ParamCollectionHandler, ResultDialogHandler, ToastHandler } from '@object-ui/core';
|
|
26
|
+
import { type EntitlementDialogSpec } from '../environment/entitlements';
|
|
26
27
|
export interface ConsoleActionRuntimeOptions {
|
|
27
28
|
/** Adapter for generic CRUD / execute calls. */
|
|
28
29
|
dataSource: any;
|
|
@@ -45,6 +46,8 @@ export interface ConsoleActionRuntime {
|
|
|
45
46
|
serverActionHandler: (action: ActionDef, context?: ActionContext) => Promise<ActionResult>;
|
|
46
47
|
/** Authenticated fetch wrapper (Bearer + tenant + cookies). */
|
|
47
48
|
authFetch: ReturnType<typeof createAuthenticatedFetch>;
|
|
49
|
+
/** Open the shared environment entitlement (upgrade / limit) dialog. */
|
|
50
|
+
openEntitlementDialog: (spec: EntitlementDialogSpec) => void;
|
|
48
51
|
/** Props to spread onto `<ActionProvider>`. */
|
|
49
52
|
actionProviderProps: {
|
|
50
53
|
context: Record<string, any>;
|
|
@@ -33,6 +33,9 @@ import { ActionParamDialog } from '../views/ActionParamDialog';
|
|
|
33
33
|
import { ActionResultDialog } from '../views/ActionResultDialog';
|
|
34
34
|
import { FlowRunner } from '../views/FlowRunner';
|
|
35
35
|
import { resolveActionParams } from '../utils/resolveActionParams';
|
|
36
|
+
import { EnvironmentEntitlementDialog } from '../environment/EnvironmentEntitlementDialog';
|
|
37
|
+
import { entitlementDialogFromError } from '../environment/entitlements';
|
|
38
|
+
import { resolvePageVarTokens } from '../utils/resolvePageVarTokens';
|
|
36
39
|
const FALLBACK_USER = { id: 'current-user', name: 'Demo User', isPlatformAdmin: false };
|
|
37
40
|
export function useConsoleActionRuntime(opts) {
|
|
38
41
|
const { dataSource, objects, objectName, onRefresh } = opts;
|
|
@@ -60,6 +63,9 @@ export function useConsoleActionRuntime(opts) {
|
|
|
60
63
|
const [resultDialogState, setResultDialogState] = useState({ open: false });
|
|
61
64
|
// A paused `screen`-node flow awaiting user input.
|
|
62
65
|
const [screenFlow, setScreenFlow] = useState(null);
|
|
66
|
+
// Plan/capacity gate dialog (upgrade / limit), shared by the env-list toolbar
|
|
67
|
+
// (proactive) and the api-action error path below (reactive safety net).
|
|
68
|
+
const [entitlementDialog, setEntitlementDialog] = useState({ open: false });
|
|
63
69
|
// Guards against double-firing a server action (slow SSO handoff, etc.).
|
|
64
70
|
const serverActionInFlight = useRef(new Set());
|
|
65
71
|
const resultDialogHandler = useCallback((spec, data) => new Promise((resolve) => {
|
|
@@ -133,7 +139,10 @@ export function useConsoleActionRuntime(opts) {
|
|
|
133
139
|
}, [navigate]);
|
|
134
140
|
// Authenticated fetch for direct backend calls. Declared before apiHandler.
|
|
135
141
|
const authFetch = useMemo(() => createAuthenticatedFetch(), []);
|
|
136
|
-
const
|
|
142
|
+
const openEntitlementDialog = useCallback((spec) => {
|
|
143
|
+
setEntitlementDialog({ open: true, spec });
|
|
144
|
+
}, []);
|
|
145
|
+
const apiHandler = useCallback(async (action, context) => {
|
|
137
146
|
try {
|
|
138
147
|
const target = action.target || action.name;
|
|
139
148
|
const params = action.params || {};
|
|
@@ -148,6 +157,12 @@ export function useConsoleActionRuntime(opts) {
|
|
|
148
157
|
const rawParams = { ...params };
|
|
149
158
|
const rowRecord = rawParams._rowRecord;
|
|
150
159
|
delete rawParams._rowRecord;
|
|
160
|
+
// Resolve `{{page.<var>}}` tokens against the live page-variable snapshot
|
|
161
|
+
// (published into the action context by PageVariableActionBridge). This is
|
|
162
|
+
// what lets a pure-SDUI form submit the values its inputs wrote into page
|
|
163
|
+
// variables; whole-value tokens preserve type. See resolvePageVarTokens.
|
|
164
|
+
const pageVars = (context?.pageVariables ?? undefined);
|
|
165
|
+
const resolvedParams = resolvePageVarTokens(rawParams, pageVars);
|
|
151
166
|
// Interpolate `{field}` tokens in the target URL from the row record.
|
|
152
167
|
let resolvedTarget = targetStr;
|
|
153
168
|
if (rowRecord && /\{[a-z_][a-z0-9_]*\}/i.test(resolvedTarget)) {
|
|
@@ -160,7 +175,7 @@ export function useConsoleActionRuntime(opts) {
|
|
|
160
175
|
const wrap = action.bodyShape && typeof action.bodyShape === 'object' && action.bodyShape.wrap
|
|
161
176
|
? action.bodyShape.wrap
|
|
162
177
|
: undefined;
|
|
163
|
-
const body = wrap ? { [wrap]:
|
|
178
|
+
const body = wrap ? { [wrap]: resolvedParams } : { ...resolvedParams };
|
|
164
179
|
if (rowRecord && action.recordIdParam) {
|
|
165
180
|
const rowField = action.recordIdField || 'id';
|
|
166
181
|
const rowValue = rowRecord[rowField];
|
|
@@ -172,7 +187,7 @@ export function useConsoleActionRuntime(opts) {
|
|
|
172
187
|
body.organizationId = activeOrganization.id;
|
|
173
188
|
}
|
|
174
189
|
if (action.bodyExtra && typeof action.bodyExtra === 'object') {
|
|
175
|
-
Object.assign(body, action.bodyExtra);
|
|
190
|
+
Object.assign(body, resolvePageVarTokens(action.bodyExtra, pageVars));
|
|
176
191
|
}
|
|
177
192
|
const method = (action.method || 'POST').toUpperCase();
|
|
178
193
|
const init = {
|
|
@@ -185,12 +200,23 @@ export function useConsoleActionRuntime(opts) {
|
|
|
185
200
|
}
|
|
186
201
|
const res = await authFetch(url, init);
|
|
187
202
|
if (!res.ok) {
|
|
188
|
-
let
|
|
203
|
+
let body = null;
|
|
189
204
|
try {
|
|
190
|
-
|
|
191
|
-
detail = j?.error || j?.message || detail;
|
|
205
|
+
body = await res.json();
|
|
192
206
|
}
|
|
193
207
|
catch { /* response body not JSON */ }
|
|
208
|
+
// Plan/capacity gates (e.g. creating an environment the org's plan
|
|
209
|
+
// doesn't include) come back as coded 403s. Surface them as a friendly
|
|
210
|
+
// upgrade/limit DIALOG with a CTA — never a generic red error toast.
|
|
211
|
+
// Returning success:false WITHOUT an `error` suppresses the runner's
|
|
212
|
+
// error toast (ActionRunner.handlePostExecution); the dialog owns the
|
|
213
|
+
// messaging.
|
|
214
|
+
const entitlementSpec = entitlementDialogFromError(body);
|
|
215
|
+
if (entitlementSpec) {
|
|
216
|
+
openEntitlementDialog(entitlementSpec);
|
|
217
|
+
return { success: false };
|
|
218
|
+
}
|
|
219
|
+
const detail = body?.error || body?.message || `HTTP ${res.status}`;
|
|
194
220
|
return { success: false, error: detail };
|
|
195
221
|
}
|
|
196
222
|
const data = await res.json().catch(() => ({}));
|
|
@@ -251,7 +277,7 @@ export function useConsoleActionRuntime(opts) {
|
|
|
251
277
|
catch (error) {
|
|
252
278
|
return { success: false, error: error.message };
|
|
253
279
|
}
|
|
254
|
-
}, [dataSource, objApiName, authFetch, activeOrganization, refresh]);
|
|
280
|
+
}, [dataSource, objApiName, authFetch, activeOrganization, refresh, openEntitlementDialog]);
|
|
255
281
|
// Flow action handler — POST to /api/v1/automation/{name}/trigger.
|
|
256
282
|
// `context` is the shared ActionRunner context (registered handlers are
|
|
257
283
|
// invoked as `handler(action, runnerContext)`).
|
|
@@ -537,7 +563,8 @@ export function useConsoleActionRuntime(opts) {
|
|
|
537
563
|
} }), _jsx(ActionResultDialog, { state: resultDialogState, onAcknowledge: () => {
|
|
538
564
|
resultDialogState.resolve?.();
|
|
539
565
|
setResultDialogState({ open: false });
|
|
540
|
-
} }), _jsx(FlowRunner, { state: screenFlow, authFetch: authFetch, baseUrl: import.meta.env.VITE_SERVER_URL || '', dataSource: dataSource, objects: objects, onClose: () => setScreenFlow(null), onComplete: () => { setScreenFlow(null); refresh(); } })
|
|
566
|
+
} }), _jsx(FlowRunner, { state: screenFlow, authFetch: authFetch, baseUrl: import.meta.env.VITE_SERVER_URL || '', dataSource: dataSource, objects: objects, onClose: () => setScreenFlow(null), onComplete: () => { setScreenFlow(null); refresh(); } }), _jsx(EnvironmentEntitlementDialog, { state: entitlementDialog, apiBase: import.meta.env.VITE_SERVER_URL || '', onOpenChange: (open) => { if (!open)
|
|
567
|
+
setEntitlementDialog({ open: false }); } })] }));
|
|
541
568
|
return {
|
|
542
569
|
confirmHandler,
|
|
543
570
|
toastHandler,
|
|
@@ -548,6 +575,7 @@ export function useConsoleActionRuntime(opts) {
|
|
|
548
575
|
flowHandler,
|
|
549
576
|
serverActionHandler,
|
|
550
577
|
authFetch,
|
|
578
|
+
openEntitlementDialog,
|
|
551
579
|
actionProviderProps,
|
|
552
580
|
dialogs,
|
|
553
581
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -19,7 +19,9 @@ export type { AppShellProps, } from './types';
|
|
|
19
19
|
export type { MetadataState, MetadataContextValue, MetadataTypeStatus, } from './providers/MetadataProvider';
|
|
20
20
|
export type { ExpressionContextValue, } from './providers/ExpressionProvider';
|
|
21
21
|
export type { RecentItem, } from './hooks/useRecentItems';
|
|
22
|
-
export { ConsoleShell, ConnectedShell, RequireOrganization, AuthenticatedRoute, RootRedirect, SystemRedirect, LoadingFallback, } from './console/ConsoleShell';
|
|
22
|
+
export { ConsoleShell, ConnectedShell, RequireOrganization, RequireAiSurface, AuthenticatedRoute, RootRedirect, SystemRedirect, LoadingFallback, } from './console/ConsoleShell';
|
|
23
|
+
export { useAiSurfaceEnabled } from './hooks/useAiSurface';
|
|
24
|
+
export type { AiSurfaceState } from './hooks/useAiSurface';
|
|
23
25
|
export { ConsoleLayout, AppHeader, AppSidebar, UnifiedSidebar, AppSwitcher, ConnectionStatus, ActivityFeed, LocaleSwitcher, ModeToggle, AuthPageLayout, } from './layout';
|
|
24
26
|
export type { ActivityItem } from './layout';
|
|
25
27
|
export { CommandPalette, KeyboardShortcutsDialog, OnboardingWalkthrough, ConditionalAuthWrapper, ConsoleToaster, RouteFader, toastWithUndo, type ToastWithUndoOptions, ErrorBoundary, LoadingScreen, ThemeProvider, useTheme, } from './chrome';
|
package/dist/index.js
CHANGED
|
@@ -18,7 +18,11 @@ export { getPendingRequests, isIdle, subscribeSettle, whenIdle, withSettleSignal
|
|
|
18
18
|
export { useRecentItems } from './hooks/useRecentItems';
|
|
19
19
|
// Console building blocks — compose these in your App.tsx to build the console
|
|
20
20
|
// routing tree. See examples/console-starter/src/App.tsx for a minimal example.
|
|
21
|
-
export { ConsoleShell, ConnectedShell, RequireOrganization, AuthenticatedRoute, RootRedirect, SystemRedirect, LoadingFallback, } from './console/ConsoleShell';
|
|
21
|
+
export { ConsoleShell, ConnectedShell, RequireOrganization, RequireAiSurface, AuthenticatedRoute, RootRedirect, SystemRedirect, LoadingFallback, } from './console/ConsoleShell';
|
|
22
|
+
// Runtime AI-availability signal — the single source of truth every AI entry
|
|
23
|
+
// point gates on (FAB, /ai routes, designer "Ask AI"). Server-pushed, no
|
|
24
|
+
// build-time edition flag. See ./hooks/useAiSurface.
|
|
25
|
+
export { useAiSurfaceEnabled } from './hooks/useAiSurface';
|
|
22
26
|
// Layout chrome
|
|
23
27
|
export { ConsoleLayout, AppHeader, AppSidebar, UnifiedSidebar, AppSwitcher, ConnectionStatus, ActivityFeed, LocaleSwitcher, ModeToggle, AuthPageLayout, } from './layout';
|
|
24
28
|
// Top-level chrome (dialogs, providers, error boundaries)
|
package/dist/layout/AppHeader.js
CHANGED
|
@@ -20,11 +20,12 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
20
20
|
*/
|
|
21
21
|
import { useLocation, useParams, Link, useNavigate } from 'react-router-dom';
|
|
22
22
|
import { Button, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuGroup, Avatar, AvatarImage, AvatarFallback, cn, } from '@object-ui/components';
|
|
23
|
-
import { Search, HelpCircle, ChevronDown, Check, Lock, LogOut,
|
|
23
|
+
import { Search, HelpCircle, ChevronDown, Check, Lock, LogOut, Plus, Layers, Bot, User, BookOpen, ExternalLink, Keyboard, } from 'lucide-react';
|
|
24
24
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
25
25
|
import { useOffline } from '@object-ui/react';
|
|
26
26
|
import { PresenceAvatars, useTenantPresence } from '@object-ui/collaboration';
|
|
27
27
|
import { ModeToggle } from './ModeToggle';
|
|
28
|
+
import { WorkspaceSwitcher } from './WorkspaceSwitcher';
|
|
28
29
|
import { LocaleSwitcher } from './LocaleSwitcher';
|
|
29
30
|
import { ConnectionStatus } from './ConnectionStatus';
|
|
30
31
|
import { InboxPopover } from './InboxPopover';
|
|
@@ -39,6 +40,7 @@ import { useMobileViewSwitcher } from './MobileViewSwitcherContext';
|
|
|
39
40
|
import { useNavigationContext } from '../context/NavigationContext';
|
|
40
41
|
import { useCommandPalette } from '../context/CommandPaletteProvider';
|
|
41
42
|
import { useUrlOverlay } from '../hooks/useUrlOverlay';
|
|
43
|
+
import { useAiSurfaceEnabled } from '../hooks/useAiSurface';
|
|
42
44
|
import { getProductName } from '../runtime-config';
|
|
43
45
|
import { LocalizedSidebarTrigger } from './LocalizedSidebarTrigger';
|
|
44
46
|
function humanizeSlug(slug) {
|
|
@@ -64,8 +66,12 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
|
|
|
64
66
|
// Click-reachable entry for the keyboard-shortcuts dialog (was `?`-key only).
|
|
65
67
|
// Shares the `?shortcuts=1` URL param with KeyboardShortcutsDialog (C2/C3).
|
|
66
68
|
const { openOverlay: openShortcuts } = useUrlOverlay('shortcuts');
|
|
67
|
-
const { user, signOut, isAuthEnabled, organizations, activeOrganization, isOrganizationsLoading, } = useAuth();
|
|
69
|
+
const { user, signOut, isAuthEnabled, organizations, activeOrganization, isOrganizationsLoading, getAuthConfig, } = useAuth();
|
|
68
70
|
const dataSource = useAdapter();
|
|
71
|
+
// Runtime AI gating: hide the top-bar AI entry point when the server serves
|
|
72
|
+
// no AI (Community Edition) so it can't dead-end on a chat with no agent.
|
|
73
|
+
// Same signal as the FAB and the `/ai` route guard.
|
|
74
|
+
const { enabled: aiEnabled } = useAiSurfaceEnabled();
|
|
69
75
|
const { t } = useObjectTranslation();
|
|
70
76
|
const { objectLabel, dashboardLabel, pageLabel, reportLabel, viewLabel, appLabel } = useObjectLabel();
|
|
71
77
|
const { apps: metadataApps, dashboards: metadataDashboards, pages: metadataPages, reports: metadataReports } = useMetadata();
|
|
@@ -432,6 +438,25 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
|
|
|
432
438
|
const activeActivities = activities ?? apiActivities ?? [];
|
|
433
439
|
const orgList = organizations ?? [];
|
|
434
440
|
const hasOrgSection = isOrganizationsLoading || orgList.length > 0 || !!activeOrganization;
|
|
441
|
+
// Mirror the server's `beforeCreateOrganization` gate so the "Create
|
|
442
|
+
// workspace" entry only shows where multi-org self-service is enabled.
|
|
443
|
+
// Default to allowed until the config resolves (avoids hiding it on slow
|
|
444
|
+
// networks); the server still enforces.
|
|
445
|
+
const [multiOrgDisabled, setMultiOrgDisabled] = useState(false);
|
|
446
|
+
useEffect(() => {
|
|
447
|
+
let cancelled = false;
|
|
448
|
+
getAuthConfig?.()
|
|
449
|
+
.then((cfg) => {
|
|
450
|
+
if (!cancelled)
|
|
451
|
+
setMultiOrgDisabled(cfg?.features?.multiOrgEnabled === false);
|
|
452
|
+
})
|
|
453
|
+
.catch(() => {
|
|
454
|
+
/* leave default — server still enforces */
|
|
455
|
+
});
|
|
456
|
+
return () => {
|
|
457
|
+
cancelled = true;
|
|
458
|
+
};
|
|
459
|
+
}, [getAuthConfig]);
|
|
435
460
|
// Build path segments (only used in `app` variant)
|
|
436
461
|
const pathParts = location.pathname.split('/').filter(Boolean);
|
|
437
462
|
const appNameFromRoute = params.appName || pathParts[1];
|
|
@@ -534,7 +559,7 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
|
|
|
534
559
|
}
|
|
535
560
|
}
|
|
536
561
|
const lastSegmentLabel = extraSegments[extraSegments.length - 1]?.label || appName || '';
|
|
537
|
-
return (_jsxs("div", { className: "flex items-center justify-between w-full h-full", children: [_jsxs("div", { className: "flex items-center min-w-0 flex-1", children: [_jsx(Link, { to: "/home", className: cn("flex items-center justify-center h-7 w-7 shrink-0 rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors", isApp && "hidden sm:flex"), title: getProductName(), children: _jsx(Layers, { className: "h-4 w-4" }) }), resolvedVariant === 'home' && (_jsx("span", { className: "hidden sm:inline ml-2 text-sm font-semibold tracking-tight", children: getProductName() })), resolvedVariant === 'orgs' && (_jsxs(_Fragment, { children: [_jsx(PathSep, {}), _jsx("span", { className: "text-sm font-medium text-foreground/80 px-1.5", children: t('organizations.title', { defaultValue: 'Organizations' }) })] })), isApp && (_jsxs(_Fragment, { children: [_jsx(LocalizedSidebarTrigger, { className: "lg:hidden shrink-0 ml-1", "aria-label": t('common.toggleSidebar', { defaultValue: 'Toggle sidebar' }) }), activeAppName && onAppChange ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "hidden sm:flex items-center", children: _jsx(PathSep, {}) }), _jsx("div", { className: "hidden sm:flex items-center", children: _jsx(AppSwitcher, { activeAppName: activeAppName, onAppChange: onAppChange }) })] })) : appName ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "hidden sm:flex items-center", children: _jsx(PathSep, {}) }), _jsx("span", { className: "hidden sm:inline text-sm font-medium text-foreground/80 px-1.5", children: appName })] })) : null, extraSegments.map((seg, i) => {
|
|
562
|
+
return (_jsxs("div", { className: "flex items-center justify-between w-full h-full", children: [_jsxs("div", { className: "flex items-center min-w-0 flex-1", children: [_jsx(Link, { to: "/home", className: cn("flex items-center justify-center h-7 w-7 shrink-0 rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors", isApp && "hidden sm:flex"), title: getProductName(), children: _jsx(Layers, { className: "h-4 w-4" }) }), resolvedVariant === 'home' && (_jsx("span", { className: "hidden sm:inline ml-2 text-sm font-semibold tracking-tight", children: getProductName() })), _jsx(WorkspaceSwitcher, {}), resolvedVariant === 'orgs' && (_jsxs(_Fragment, { children: [_jsx(PathSep, {}), _jsx("span", { className: "text-sm font-medium text-foreground/80 px-1.5", children: t('organizations.title', { defaultValue: 'Organizations' }) })] })), isApp && (_jsxs(_Fragment, { children: [_jsx(LocalizedSidebarTrigger, { className: "lg:hidden shrink-0 ml-1", "aria-label": t('common.toggleSidebar', { defaultValue: 'Toggle sidebar' }) }), activeAppName && onAppChange ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "hidden sm:flex items-center", children: _jsx(PathSep, {}) }), _jsx("div", { className: "hidden sm:flex items-center", children: _jsx(AppSwitcher, { activeAppName: activeAppName, onAppChange: onAppChange }) })] })) : appName ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "hidden sm:flex items-center", children: _jsx(PathSep, {}) }), _jsx("span", { className: "hidden sm:inline text-sm font-medium text-foreground/80 px-1.5", children: appName })] })) : null, extraSegments.map((seg, i) => {
|
|
538
563
|
const isLast = i === extraSegments.length - 1;
|
|
539
564
|
return (_jsxs("span", { className: "hidden sm:flex items-center min-w-0", children: [_jsx(PathSep, {}), seg.siblings && seg.siblings.length > 1 ? (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { className: `flex items-center gap-1 rounded-md px-1.5 py-1 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring hover:bg-accent hover:text-foreground ${!isLast ? 'text-foreground/60' : 'text-foreground/80'}`, children: [seg.label, _jsx(ChevronDown, { className: "h-3.5 w-3.5 text-muted-foreground" })] }), _jsxs(DropdownMenuContent, { align: "start", sideOffset: 8, className: "w-56 max-h-72 overflow-y-auto", children: [_jsx(DropdownMenuLabel, { className: "text-xs text-muted-foreground font-normal", children: "Switch Object" }), _jsx(DropdownMenuSeparator, {}), seg.siblings.map((sibling) => (_jsx(DropdownMenuItem, { asChild: true, children: _jsx(Link, { to: sibling.href, className: "w-full", children: sibling.label }) }, sibling.href)))] })] })) : seg.href ? (_jsx(Link, { to: seg.href, className: `rounded-md px-1.5 py-1 text-sm font-medium transition-colors hover:bg-accent hover:text-foreground truncate max-w-[160px] ${isLast ? 'text-foreground/80' : 'text-foreground/60'}`, children: seg.label })) : (_jsx("span", { className: `px-1.5 py-1 text-sm font-medium truncate max-w-[160px] ${isLast ? 'text-foreground/80' : 'text-foreground/60'}`, children: seg.label }))] }, i));
|
|
540
565
|
}), mobileSwitcher && mobileSwitcher.views.length > 0 ? (mobileSwitcher.views.length > 1 ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: "sm:hidden flex items-center gap-0.5 min-w-0 ml-1 rounded-md px-1.5 py-1 text-sm font-medium hover:bg-accent active:bg-accent/80 transition-colors", "aria-label": t('topbar.switchView', { defaultValue: 'Switch view' }), children: [_jsx("span", { className: "truncate max-w-[180px]", children: mobileSwitcher.triggerLabel ??
|
|
@@ -545,10 +570,10 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
|
|
|
545
570
|
if (!isActive)
|
|
546
571
|
mobileSwitcher.onChange(v.id);
|
|
547
572
|
}, className: "gap-2", children: [v.icon ? (_jsx("span", { className: "shrink-0 text-muted-foreground [&>svg]:h-4 [&>svg]:w-4", children: v.icon })) : null, _jsx("span", { className: "flex-1 truncate", children: v.label }), v.locked ? (_jsx(Lock, { className: "h-3 w-3 shrink-0 text-muted-foreground", "aria-hidden": true })) : null, isActive ? (_jsx(Check, { className: "h-4 w-4 shrink-0 text-foreground", "aria-hidden": true })) : null] }, v.id));
|
|
548
|
-
}) })] })) : (_jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: mobileSwitcher.triggerLabel ?? mobileSwitcher.views[0].label }))) : (_jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: lastSegmentLabel }))] }))] }), _jsxs("div", { className: "flex items-center gap-0.5 sm:gap-1 shrink-0 [&>*+*[data-topbar-group]]:ml-1 [&>[data-topbar-group]+[data-topbar-group]]:border-l [&>[data-topbar-group]+[data-topbar-group]]:border-border/60 [&>[data-topbar-group]+[data-topbar-group]]:pl-1 sm:[&>[data-topbar-group]+[data-topbar-group]]:pl-2 sm:[&>[data-topbar-group]+[data-topbar-group]]:ml-2", children: [!isOnline && (_jsxs("div", { className: "flex items-center gap-1 px-2 py-1 rounded-full bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200 text-xs font-medium", children: [_jsx("span", { className: "h-2 w-2 rounded-full bg-yellow-500 animate-pulse" }), t('topbar.offline', { defaultValue: 'Offline' })] })), isApp && connectionState && _jsx(ConnectionStatus, { state: connectionState }), isApp && activeUsers.length > 0 && (_jsx("div", { className: "hidden md:flex items-center shrink-0", title: t('topbar.usersOnline', { defaultValue: 'Users currently online' }), children: _jsx(PresenceAvatars, { users: activeUsers, size: "sm", maxVisible: 3, showStatus: true }) })), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 sm:gap-1 shrink-0", children: [_jsxs("button", { type: "button", "data-testid": "action:command-palette:open", "aria-label": t('console.search', { defaultValue: 'Search…' }), "aria-keyshortcuts": "Meta+K Control+K", onClick: openCommandPalette, className: "hidden lg:flex relative items-center gap-2 w-48 xl:w-64 h-8 px-3 text-sm rounded-md border bg-muted/50 text-muted-foreground hover:bg-muted transition-colors", children: [_jsx(Search, { className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "flex-1 text-left text-xs", children: t('console.search', { defaultValue: 'Search…' }) }), _jsxs("kbd", { className: "pointer-events-none inline-flex h-5 items-center gap-0.5 rounded border bg-background px-1.5 text-[10px] font-medium text-muted-foreground", children: [_jsx("span", { className: "text-xs", children: "\u2318" }), "K"] })] }), _jsx(Button, { variant: "ghost", size: "icon", className: "lg:hidden h-8 w-8 shrink-0", "data-testid": "action:command-palette:open-mobile", onClick: openCommandPalette, "aria-label": t('console.search', { defaultValue: 'Search…' }), children: _jsx(Search, { className: "h-4 w-4" }) })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [_jsx(InboxPopover, { notifications: notifications, unreadCount: unreadCount, pendingApprovalsCount: pendingApprovalsCount, activities: activeActivities, onMarkAllRead: markAllRead, onMarkRead: markNotificationRead }), _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 shrink-0", asChild: true, "aria-label": t('topbar.aiAssistant', { defaultValue: 'AI Assistant' }), children: _jsx(Link, { to: "/ai", children: _jsx(Bot, { className: "h-4 w-4" }) }) }), _jsxs(DropdownMenu, { onOpenChange: (open) => { if (open)
|
|
573
|
+
}) })] })) : (_jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: mobileSwitcher.triggerLabel ?? mobileSwitcher.views[0].label }))) : (_jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: lastSegmentLabel }))] }))] }), _jsxs("div", { className: "flex items-center gap-0.5 sm:gap-1 shrink-0 [&>*+*[data-topbar-group]]:ml-1 [&>[data-topbar-group]+[data-topbar-group]]:border-l [&>[data-topbar-group]+[data-topbar-group]]:border-border/60 [&>[data-topbar-group]+[data-topbar-group]]:pl-1 sm:[&>[data-topbar-group]+[data-topbar-group]]:pl-2 sm:[&>[data-topbar-group]+[data-topbar-group]]:ml-2", children: [!isOnline && (_jsxs("div", { className: "flex items-center gap-1 px-2 py-1 rounded-full bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200 text-xs font-medium", children: [_jsx("span", { className: "h-2 w-2 rounded-full bg-yellow-500 animate-pulse" }), t('topbar.offline', { defaultValue: 'Offline' })] })), isApp && connectionState && _jsx(ConnectionStatus, { state: connectionState }), isApp && activeUsers.length > 0 && (_jsx("div", { className: "hidden md:flex items-center shrink-0", title: t('topbar.usersOnline', { defaultValue: 'Users currently online' }), children: _jsx(PresenceAvatars, { users: activeUsers, size: "sm", maxVisible: 3, showStatus: true }) })), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 sm:gap-1 shrink-0", children: [_jsxs("button", { type: "button", "data-testid": "action:command-palette:open", "aria-label": t('console.search', { defaultValue: 'Search…' }), "aria-keyshortcuts": "Meta+K Control+K", onClick: openCommandPalette, className: "hidden lg:flex relative items-center gap-2 w-48 xl:w-64 h-8 px-3 text-sm rounded-md border bg-muted/50 text-muted-foreground hover:bg-muted transition-colors", children: [_jsx(Search, { className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "flex-1 text-left text-xs", children: t('console.search', { defaultValue: 'Search…' }) }), _jsxs("kbd", { className: "pointer-events-none inline-flex h-5 items-center gap-0.5 rounded border bg-background px-1.5 text-[10px] font-medium text-muted-foreground", children: [_jsx("span", { className: "text-xs", children: "\u2318" }), "K"] })] }), _jsx(Button, { variant: "ghost", size: "icon", className: "lg:hidden h-8 w-8 shrink-0", "data-testid": "action:command-palette:open-mobile", onClick: openCommandPalette, "aria-label": t('console.search', { defaultValue: 'Search…' }), children: _jsx(Search, { className: "h-4 w-4" }) })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [_jsx(InboxPopover, { notifications: notifications, unreadCount: unreadCount, pendingApprovalsCount: pendingApprovalsCount, activities: activeActivities, onMarkAllRead: markAllRead, onMarkRead: markNotificationRead }), aiEnabled && (_jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 shrink-0", asChild: true, "aria-label": t('topbar.aiAssistant', { defaultValue: 'AI Assistant' }), children: _jsx(Link, { to: "/ai", children: _jsx(Bot, { className: "h-4 w-4" }) }) })), _jsxs(DropdownMenu, { onOpenChange: (open) => { if (open)
|
|
549
574
|
void loadHelpDocs(); }, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 hidden md:flex shrink-0", "aria-label": t('sidebar.helpTooltip', { defaultValue: 'Help & Documentation' }), children: _jsx(HelpCircle, { className: "h-4 w-4" }) }) }), _jsxs(DropdownMenuContent, { align: "end", className: "min-w-56 rounded-lg", sideOffset: 4, children: [currentAppDocs.length > 0 && currentAppPackageId ? (_jsxs(DropdownMenuItem, { className: "cursor-pointer", onClick: () => navigate(currentAppDocs.length === 1
|
|
550
575
|
? `/apps/${currentAppPackageId}/docs/${currentAppDocs[0].name}`
|
|
551
|
-
: `/apps/${currentAppPackageId}/docs`), children: [_jsx(BookOpen, { className: "mr-2 h-4 w-4" }), t('help.appDocs', { defaultValue: "This app's docs" })] })) : null, _jsxs(DropdownMenuItem, { className: "cursor-pointer", onClick: () => navigate('/docs'), children: [_jsx(Layers, { className: "mr-2 h-4 w-4" }), t('help.allDocs', { defaultValue: 'All documentation' })] }), isApp ? (_jsxs(DropdownMenuItem, { className: "cursor-pointer", "data-testid": "action:keyboard-shortcuts:open", onClick: openShortcuts, children: [_jsx(Keyboard, { className: "mr-2 h-4 w-4" }), t('help.keyboardShortcuts', { defaultValue: 'Keyboard shortcuts' })] })) : null, _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { asChild: true, className: "cursor-pointer", children: _jsxs("a", { href: "https://docs.objectstack.ai", target: "_blank", rel: "noopener noreferrer", children: [_jsx(ExternalLink, { className: "mr-2 h-4 w-4" }), t('help.onlineDocs', { defaultValue: 'Online documentation' })] }) })] })] })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [" ", _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 shrink-0 rounded-full", children: _jsxs(Avatar, { className: "h-7 w-7 rounded-full", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-full bg-primary text-primary-foreground text-xs", children: getUserInitials(user) })] }) }) }), _jsxs(DropdownMenuContent, { align: "end", className: "min-w-64 rounded-lg", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "p-0 font-normal", children: _jsxs("div", { className: "flex items-center gap-2 px-2 py-2", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground", children: getUserInitials(user) })] }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.name ?? 'User' }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email ?? '' })] })] }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuGroup, { children: [_jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/account/component/account/profile_card'), className: "cursor-pointer", children: [_jsx(User, { className: "mr-2 h-4 w-4" }), t('user.profile', { defaultValue: 'Profile' })] }), hasOrgSection && (_jsxs(DropdownMenuItem, { onClick: () => navigate('/organizations'), className: "cursor-pointer", children: [_jsx(
|
|
576
|
+
: `/apps/${currentAppPackageId}/docs`), children: [_jsx(BookOpen, { className: "mr-2 h-4 w-4" }), t('help.appDocs', { defaultValue: "This app's docs" })] })) : null, _jsxs(DropdownMenuItem, { className: "cursor-pointer", onClick: () => navigate('/docs'), children: [_jsx(Layers, { className: "mr-2 h-4 w-4" }), t('help.allDocs', { defaultValue: 'All documentation' })] }), isApp ? (_jsxs(DropdownMenuItem, { className: "cursor-pointer", "data-testid": "action:keyboard-shortcuts:open", onClick: openShortcuts, children: [_jsx(Keyboard, { className: "mr-2 h-4 w-4" }), t('help.keyboardShortcuts', { defaultValue: 'Keyboard shortcuts' })] })) : null, _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { asChild: true, className: "cursor-pointer", children: _jsxs("a", { href: "https://docs.objectstack.ai", target: "_blank", rel: "noopener noreferrer", children: [_jsx(ExternalLink, { className: "mr-2 h-4 w-4" }), t('help.onlineDocs', { defaultValue: 'Online documentation' })] }) })] })] })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [" ", _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 shrink-0 rounded-full", children: _jsxs(Avatar, { className: "h-7 w-7 rounded-full", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-full bg-primary text-primary-foreground text-xs", children: getUserInitials(user) })] }) }) }), _jsxs(DropdownMenuContent, { align: "end", className: "min-w-64 rounded-lg", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "p-0 font-normal", children: _jsxs("div", { className: "flex items-center gap-2 px-2 py-2", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground", children: getUserInitials(user) })] }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.name ?? 'User' }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email ?? '' })] })] }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuGroup, { children: [_jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/account/component/account/profile_card'), className: "cursor-pointer", children: [_jsx(User, { className: "mr-2 h-4 w-4" }), t('user.profile', { defaultValue: 'Profile' })] }), hasOrgSection && !multiOrgDisabled && (_jsxs(DropdownMenuItem, { onClick: () => navigate('/organizations?create=1'), className: "cursor-pointer", "data-testid": "header-create-workspace", children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), t('organizations.create', { defaultValue: 'Create workspace' })] })), (metadataApps || [])
|
|
552
577
|
.filter((a) => a.active !== false && a.hidden === true && a.name !== 'account')
|
|
553
578
|
.map((app) => {
|
|
554
579
|
const AppIcon = getIcon(app.icon);
|
|
@@ -13,7 +13,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
13
13
|
*/
|
|
14
14
|
import React from 'react';
|
|
15
15
|
import { useReconcileOnError } from '../hooks/useReconcileOnError';
|
|
16
|
-
import { FloatingChatbot, useObjectChat, useAgents, useHitlInChat, resolveDefaultAgentName, uiMessagesToChatMessages, publishHealthFromResponse, agentRouteName, } from '@object-ui/plugin-chatbot';
|
|
16
|
+
import { FloatingChatbot, useObjectChat, useAgents, useAiModels, useHitlInChat, resolveDefaultAgentName, uiMessagesToChatMessages, publishHealthFromResponse, agentRouteName, } from '@object-ui/plugin-chatbot';
|
|
17
17
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Button, ShareDialog, } from '@object-ui/components';
|
|
18
18
|
import { Share2, SquarePen } from 'lucide-react';
|
|
19
19
|
import { useObjectTranslation } from '@object-ui/i18n';
|
|
@@ -104,8 +104,12 @@ function buildChatLocale(language, appLabel, agentName, fallbackAgentLabel, obje
|
|
|
104
104
|
planApprove: '开始搭建',
|
|
105
105
|
planAdjust: '调整方案',
|
|
106
106
|
planBuilt: '已搭建',
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
// These messages the button SENDS must match the cloud confirm gate's
|
|
108
|
+
// APPROVAL_RE (service-ai-studio confirm-gate.ts) or the agent re-proposes
|
|
109
|
+
// and "开始搭建" looks inert — the gate anchors Chinese approval on 确认 /
|
|
110
|
+
// 直接搭建, so a bare "…搭建吧" does NOT match. Keep these 确认-anchored.
|
|
111
|
+
planApproveMessage: '确认,开始搭建。',
|
|
112
|
+
planApproveDefaultsMessage: '确认搭建,未决问题按你的合理假设和默认处理。',
|
|
109
113
|
planAnswer: (question, option) => `关于「${question}」,我选择「${option}」。`,
|
|
110
114
|
publishOk: '已发布,对象已生效。',
|
|
111
115
|
publishFailed: '发布失败',
|
|
@@ -257,6 +261,14 @@ function ChatbotInner({ appLabel, appName, objects, agents, agentsLoading, activ
|
|
|
257
261
|
// the agent context so "add a priority field" acts on the open object,
|
|
258
262
|
// and drives context-aware starter suggestions.
|
|
259
263
|
const { editor } = useAssistant();
|
|
264
|
+
// ADR-0028: the plan-filtered AI-model allowlist this env offers in the
|
|
265
|
+
// picker (free / single-model envs return one entry → the footer picker
|
|
266
|
+
// hides itself). The selected id is sent with each turn; the backend
|
|
267
|
+
// validates it against the same allowlist and weights the quota by the
|
|
268
|
+
// chosen model's cost_weight.
|
|
269
|
+
const { models: aiModels, defaultModelId } = useAiModels({ apiBase });
|
|
270
|
+
const [selectedModelId, setSelectedModelId] = React.useState(undefined);
|
|
271
|
+
const effectiveModelId = selectedModelId ?? defaultModelId;
|
|
260
272
|
// Replay persisted history when present.
|
|
261
273
|
const hydratedHistory = React.useMemo(() => {
|
|
262
274
|
if (!persistedMessages || persistedMessages.length === 0)
|
|
@@ -277,6 +289,9 @@ function ChatbotInner({ appLabel, appName, objects, agents, agentsLoading, activ
|
|
|
277
289
|
const { messages, isLoading, error, sendMessage, stop, reload, clear, setMessages, } = useObjectChat({
|
|
278
290
|
api: chatApi,
|
|
279
291
|
conversationId,
|
|
292
|
+
// ADR-0028: the user's picked model (or the env default) — sent in each
|
|
293
|
+
// request body; the agent route validates it + routes the turn to it.
|
|
294
|
+
model: effectiveModelId,
|
|
280
295
|
onError: handleChatError,
|
|
281
296
|
body: {
|
|
282
297
|
context: {
|
|
@@ -348,7 +363,10 @@ function ChatbotInner({ appLabel, appName, objects, agents, agentsLoading, activ
|
|
|
348
363
|
panelHeight: 560,
|
|
349
364
|
title: locale.title,
|
|
350
365
|
triggerSize: 56,
|
|
351
|
-
}, headerExtra: headerExtra, headerActions: headerActions, messages: messages, labels: locale.labels,
|
|
366
|
+
}, headerExtra: headerExtra, headerActions: headerActions, messages: messages, labels: locale.labels,
|
|
367
|
+
// ADR-0028: model picker — ChatbotEnhanced renders the footer <select>
|
|
368
|
+
// only when 2+ models are offered, so free / single-model envs see none.
|
|
369
|
+
models: aiModels, selectedModelId: effectiveModelId, onModelChange: setSelectedModelId, showAvatars: true, hideClearBar: true, assistantAvatarFallback: locale.agentLabel, suggestions: messages.length === 0 ? (editorSuggestions ?? locale.suggestions) : undefined, placeholder: activeAgent
|
|
352
370
|
? locale.placeholder
|
|
353
371
|
: agentsLoading
|
|
354
372
|
? locale.loadingPlaceholder
|
|
@@ -10,7 +10,6 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
10
10
|
*/
|
|
11
11
|
import { useEffect } from 'react';
|
|
12
12
|
import { AppShell } from '@object-ui/layout';
|
|
13
|
-
import { useDiscovery } from '@object-ui/react';
|
|
14
13
|
import { isBuildAgent } from '@object-ui/plugin-chatbot';
|
|
15
14
|
// Lightweight FAB stub — the heavy chat chunk graph (plugin-chatbot,
|
|
16
15
|
// shiki, streamdown, mermaid, @ai-sdk, ~20MB) only downloads on first
|
|
@@ -22,6 +21,7 @@ import { UnifiedSidebar } from './UnifiedSidebar';
|
|
|
22
21
|
import { AppHeader } from './AppHeader';
|
|
23
22
|
import { MobileViewSwitcherProvider } from './MobileViewSwitcherContext';
|
|
24
23
|
import { useResponsiveSidebar } from '../hooks/useResponsiveSidebar';
|
|
24
|
+
import { useAiSurfaceEnabled } from '../hooks/useAiSurface';
|
|
25
25
|
import { useNavigationContext } from '../context/NavigationContext';
|
|
26
26
|
import { CommandPaletteProvider } from '../context/CommandPaletteProvider';
|
|
27
27
|
import { resolveI18nLabel } from '../utils';
|
|
@@ -35,11 +35,10 @@ function ConsoleLayoutInner({ children }) {
|
|
|
35
35
|
// (moved to ./ConsoleFloatingChatbot.tsx for code-splitting)
|
|
36
36
|
export function ConsoleLayout({ children, activeAppName, activeApp, onAppChange, objects, connectionState, userId, }) {
|
|
37
37
|
const appLabel = resolveI18nLabel(activeApp?.label) || activeAppName;
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
const
|
|
42
|
-
const showChatbot = isAiEnabled || aiBaseUrlConfigured;
|
|
38
|
+
// Runtime, server-pushed AI gating (shared with the `/ai` route guard and the
|
|
39
|
+
// top-bar AI link): show the chatbot only when the server actually serves AI,
|
|
40
|
+
// or when an explicit `VITE_AI_BASE_URL` opt-in points at an external server.
|
|
41
|
+
const { enabled: showChatbot } = useAiSurfaceEnabled();
|
|
43
42
|
// AI Studio (AI-driven metadata authoring / "online development") can be
|
|
44
43
|
// turned off per deployment. When off, suppress the metadata-authoring
|
|
45
44
|
// assistant so the chatbot falls back to the generic data assistant — the
|
|
@@ -19,16 +19,6 @@ import { useLocation, useNavigate } from 'react-router-dom';
|
|
|
19
19
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@object-ui/components';
|
|
20
20
|
import { getIcon } from '../utils/getIcon';
|
|
21
21
|
import { resolveI18nLabel } from '../utils';
|
|
22
|
-
// Local/Custom scope sentinel — kept inline (not imported from metadata-admin)
|
|
23
|
-
// so this layout module never forms an import cycle with the metadata-admin
|
|
24
|
-
// views. Mirrors `LOCAL_PACKAGE_ID` in views/metadata-admin/package-scope.ts.
|
|
25
|
-
const LOCAL_SCOPE_ID = 'sys_metadata';
|
|
26
|
-
function localScopeLabel() {
|
|
27
|
-
const lang = (typeof document !== 'undefined' && document.documentElement?.lang) ||
|
|
28
|
-
(typeof navigator !== 'undefined' && navigator.language) ||
|
|
29
|
-
'';
|
|
30
|
-
return /^zh/i.test(lang) ? '本地 / 自定义(本环境)' : 'Local / Custom (this env)';
|
|
31
|
-
}
|
|
32
22
|
const ALL_SENTINEL = '__all__';
|
|
33
23
|
/** Read a (possibly dotted) property path off a row, e.g. `manifest.id`. */
|
|
34
24
|
function getByPath(row, key) {
|
|
@@ -117,15 +107,6 @@ function useSelectorOptions(def) {
|
|
|
117
107
|
opts.push({ value, label: typeof labelRaw === 'string' && labelRaw ? labelRaw : value });
|
|
118
108
|
}
|
|
119
109
|
opts.sort((a, b) => a.label.localeCompare(b.label));
|
|
120
|
-
// The package-scope selector gets a stable "Local / Custom (this env)"
|
|
121
|
-
// entry for this environment's runtime, DB-authored metadata — it is
|
|
122
|
-
// never a real package row (`package_id = null` / `sys_metadata`
|
|
123
|
-
// provenance) yet must always be selectable so org-authored items are
|
|
124
|
-
// discoverable and editable. The metadata list/get API already treats
|
|
125
|
-
// `?package=sys_metadata` as exactly this local scope.
|
|
126
|
-
if (/package/i.test(endpoint) && !opts.some((o) => o.value === LOCAL_SCOPE_ID)) {
|
|
127
|
-
opts.push({ value: LOCAL_SCOPE_ID, label: localScopeLabel() });
|
|
128
|
-
}
|
|
129
110
|
setOptions(opts);
|
|
130
111
|
}
|
|
131
112
|
catch {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkspaceSwitcher
|
|
3
|
+
*
|
|
4
|
+
* Header-left organization (workspace) switcher — the standard place users
|
|
5
|
+
* expect "which org am I in / switch org" to live (Linear/Vercel/GitHub style).
|
|
6
|
+
*
|
|
7
|
+
* - Single-org users (the vast majority): just the org name, NO dropdown. There
|
|
8
|
+
* is nothing to switch to, so a one-item menu would be pure friction.
|
|
9
|
+
* - Multi-org users: the active org name + a dropdown to switch orgs inline
|
|
10
|
+
* (full-page reload so the active-org context refreshes app-wide, mirroring
|
|
11
|
+
* OrganizationsPage), plus shortcuts to manage members / create a workspace.
|
|
12
|
+
* - No org context at all: renders nothing.
|
|
13
|
+
*/
|
|
14
|
+
export declare function WorkspaceSwitcher(): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* WorkspaceSwitcher
|
|
4
|
+
*
|
|
5
|
+
* Header-left organization (workspace) switcher — the standard place users
|
|
6
|
+
* expect "which org am I in / switch org" to live (Linear/Vercel/GitHub style).
|
|
7
|
+
*
|
|
8
|
+
* - Single-org users (the vast majority): just the org name, NO dropdown. There
|
|
9
|
+
* is nothing to switch to, so a one-item menu would be pure friction.
|
|
10
|
+
* - Multi-org users: the active org name + a dropdown to switch orgs inline
|
|
11
|
+
* (full-page reload so the active-org context refreshes app-wide, mirroring
|
|
12
|
+
* OrganizationsPage), plus shortcuts to manage members / create a workspace.
|
|
13
|
+
* - No org context at all: renders nothing.
|
|
14
|
+
*/
|
|
15
|
+
import { useEffect, useState } from 'react';
|
|
16
|
+
import { useNavigate } from 'react-router-dom';
|
|
17
|
+
import { useAuth } from '@object-ui/auth';
|
|
18
|
+
import { useObjectTranslation } from '@object-ui/i18n';
|
|
19
|
+
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, } from '@object-ui/components';
|
|
20
|
+
import { ChevronsUpDown, Check, Plus, Users } from 'lucide-react';
|
|
21
|
+
import { resolveHomeUrl } from '../console/organizations/resolveHomeUrl';
|
|
22
|
+
function getOrgInitials(name) {
|
|
23
|
+
return name
|
|
24
|
+
.split(/[\s_-]+/)
|
|
25
|
+
.map((w) => w[0])
|
|
26
|
+
.join('')
|
|
27
|
+
.toUpperCase()
|
|
28
|
+
.slice(0, 2);
|
|
29
|
+
}
|
|
30
|
+
function OrgBadge({ name }) {
|
|
31
|
+
return (_jsx("span", { className: "flex h-5 w-5 shrink-0 items-center justify-center rounded bg-muted text-[10px] font-semibold text-muted-foreground", children: getOrgInitials(name) }));
|
|
32
|
+
}
|
|
33
|
+
export function WorkspaceSwitcher() {
|
|
34
|
+
const { t } = useObjectTranslation();
|
|
35
|
+
const navigate = useNavigate();
|
|
36
|
+
const { organizations, activeOrganization, switchOrganization, getAuthConfig } = useAuth();
|
|
37
|
+
const [multiOrgDisabled, setMultiOrgDisabled] = useState(false);
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
let cancelled = false;
|
|
40
|
+
getAuthConfig?.()
|
|
41
|
+
.then((cfg) => {
|
|
42
|
+
if (!cancelled)
|
|
43
|
+
setMultiOrgDisabled(cfg?.features?.multiOrgEnabled === false);
|
|
44
|
+
})
|
|
45
|
+
.catch(() => {
|
|
46
|
+
/* leave default — create entry stays available */
|
|
47
|
+
});
|
|
48
|
+
return () => {
|
|
49
|
+
cancelled = true;
|
|
50
|
+
};
|
|
51
|
+
}, [getAuthConfig]);
|
|
52
|
+
const orgList = organizations ?? [];
|
|
53
|
+
const current = activeOrganization ?? orgList[0] ?? null;
|
|
54
|
+
// No organization context (e.g. a brand-new user before provisioning) — show
|
|
55
|
+
// nothing rather than an empty switcher.
|
|
56
|
+
if (!current)
|
|
57
|
+
return null;
|
|
58
|
+
// Single-org: static label, no dropdown.
|
|
59
|
+
if (orgList.length <= 1) {
|
|
60
|
+
return (_jsxs("span", { className: "ml-2 hidden max-w-[12rem] items-center gap-1.5 sm:inline-flex", "data-testid": "workspace-name", children: [_jsx(OrgBadge, { name: current.name }), _jsx("span", { className: "truncate text-sm font-medium text-foreground/80", children: current.name })] }));
|
|
61
|
+
}
|
|
62
|
+
const handleSwitch = async (org) => {
|
|
63
|
+
if (org.id === current.id)
|
|
64
|
+
return;
|
|
65
|
+
try {
|
|
66
|
+
await switchOrganization(org.id);
|
|
67
|
+
// switchOrganization only updates state; reload to home so the new active
|
|
68
|
+
// org propagates to every data scope app-wide (same as OrganizationsPage).
|
|
69
|
+
window.location.href = resolveHomeUrl();
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
console.error('[WorkspaceSwitcher] switch failed', err);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
return (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { className: "ml-2 inline-flex max-w-[14rem] items-center gap-1.5 rounded-md px-1.5 py-1 text-sm font-medium text-foreground/80 transition-colors hover:bg-accent hover:text-foreground", "data-testid": "workspace-switcher", children: [_jsx(OrgBadge, { name: current.name }), _jsx("span", { className: "hidden truncate sm:inline", children: current.name }), _jsx(ChevronsUpDown, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" })] }), _jsxs(DropdownMenuContent, { align: "start", className: "w-60", children: [_jsx(DropdownMenuLabel, { className: "text-xs text-muted-foreground", children: t('organization.switcher.label', { defaultValue: 'Switch organization' }) }), orgList.map((org) => (_jsxs(DropdownMenuItem, { onClick: () => handleSwitch(org), className: "cursor-pointer gap-2", children: [_jsx(OrgBadge, { name: org.name }), _jsx("span", { className: "flex-1 truncate", children: org.name }), org.id === current.id && _jsx(Check, { className: "h-4 w-4 shrink-0" })] }, org.id))), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onClick: () => navigate(`/organizations/${current.slug}/members`), className: "cursor-pointer gap-2", "data-testid": "workspace-manage-members", children: [_jsx(Users, { className: "h-4 w-4" }), t('organization.switcher.manageMembers', { defaultValue: 'Manage members' })] }), !multiOrgDisabled && (_jsxs(DropdownMenuItem, { onClick: () => navigate('/organizations?create=1'), className: "cursor-pointer gap-2", "data-testid": "workspace-create", children: [_jsx(Plus, { className: "h-4 w-4" }), t('organizations.create', { defaultValue: 'Create workspace' })] }))] })] }));
|
|
76
|
+
}
|