@object-ui/app-shell 11.3.0 → 11.5.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 +700 -0
- package/README.md +32 -0
- package/dist/chrome/KeyboardShortcutsDialog.js +2 -1
- package/dist/console/AppContent.js +145 -26
- package/dist/console/ConsoleShell.js +25 -3
- package/dist/console/home/CloudOnboardingNext.d.ts +9 -0
- package/dist/console/home/CloudOnboardingNext.js +14 -4
- package/dist/console/home/HomePage.js +34 -7
- package/dist/console/organizations/CreateWorkspaceDialog.js +33 -3
- package/dist/console/organizations/OrganizationsPage.js +16 -7
- package/dist/context/CommandPaletteProvider.js +2 -1
- package/dist/hooks/useConsoleActionRuntime.js +32 -3
- package/dist/hooks/useObjectActions.js +16 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/layout/AppHeader.js +13 -5
- package/dist/layout/AppSidebar.js +10 -4
- package/dist/observability/sentry.d.ts +5 -0
- package/dist/observability/sentry.js +6 -1
- package/dist/preview/DraftChangesPanel.d.ts +31 -1
- package/dist/preview/DraftChangesPanel.js +146 -18
- package/dist/urlParams.d.ts +68 -0
- package/dist/urlParams.js +76 -0
- package/dist/utils/appRoute.d.ts +15 -0
- package/dist/utils/appRoute.js +22 -0
- package/dist/utils/deriveRelatedLists.d.ts +20 -5
- package/dist/utils/deriveRelatedLists.js +31 -13
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/pageTabsUrlSync.d.ts +32 -0
- package/dist/utils/pageTabsUrlSync.js +43 -0
- package/dist/utils/recordFormNavigation.d.ts +40 -0
- package/dist/utils/recordFormNavigation.js +30 -0
- package/dist/utils/resolveViewId.d.ts +23 -0
- package/dist/utils/resolveViewId.js +37 -0
- package/dist/utils/warnSuppressedListNav.d.ts +10 -0
- package/dist/utils/warnSuppressedListNav.js +40 -0
- package/dist/views/InterfaceListPage.d.ts +1 -0
- package/dist/views/InterfaceListPage.js +7 -5
- package/dist/views/ObjectDataPage.d.ts +29 -0
- package/dist/views/ObjectDataPage.js +227 -0
- package/dist/views/ObjectView.js +65 -13
- package/dist/views/RecordDetailView.js +191 -123
- package/dist/views/RecordFormPage.js +7 -1
- package/dist/views/RelatedRecordActionsBridge.d.ts +33 -0
- package/dist/views/RelatedRecordActionsBridge.js +147 -0
- package/dist/views/metadata-admin/PackagesPage.js +18 -7
- package/dist/views/metadata-admin/PermissionMatrixEditor.d.ts +18 -1
- package/dist/views/metadata-admin/PermissionMatrixEditor.js +73 -14
- package/dist/views/metadata-admin/ResourceEditPage.js +39 -0
- package/dist/views/metadata-admin/i18n.d.ts +12 -21
- package/dist/views/metadata-admin/i18n.js +557 -6
- package/dist/views/metadata-admin/inspectors/AppNavInspector.d.ts +11 -4
- package/dist/views/metadata-admin/inspectors/AppNavInspector.js +141 -7
- package/dist/views/metadata-admin/inspectors/FlowReferenceField.d.ts +14 -0
- package/dist/views/metadata-admin/inspectors/FlowReferenceField.js +76 -5
- package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +60 -30
- package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +8 -1
- package/dist/views/metadata-admin/inspectors/flow-node-config.js +3 -2
- package/dist/views/metadata-admin/inspectors/nav-target.d.ts +52 -0
- package/dist/views/metadata-admin/inspectors/nav-target.js +149 -0
- package/dist/views/metadata-admin/nav-selection.d.ts +20 -0
- package/dist/views/metadata-admin/nav-selection.js +81 -0
- package/dist/views/metadata-admin/permission-slice.d.ts +66 -0
- package/dist/views/metadata-admin/permission-slice.js +70 -0
- package/dist/views/metadata-admin/previews/AppNavCanvas.js +19 -7
- package/dist/views/metadata-admin/previews/AppPreview.js +4 -2
- package/dist/views/metadata-admin/previews/FlowRunsPanel.d.ts +16 -7
- package/dist/views/metadata-admin/previews/FlowRunsPanel.js +18 -2
- package/dist/views/studio-design/BuilderLanding.d.ts +15 -0
- package/dist/views/studio-design/BuilderLanding.js +126 -0
- package/dist/views/studio-design/ObjectFormDesigner.d.ts +33 -0
- package/dist/views/studio-design/ObjectFormDesigner.js +231 -0
- package/dist/views/studio-design/ObjectSettingsPanel.d.ts +30 -0
- package/dist/views/studio-design/ObjectSettingsPanel.js +46 -0
- package/dist/views/studio-design/ObjectValidationsPanel.d.ts +30 -0
- package/dist/views/studio-design/ObjectValidationsPanel.js +80 -0
- package/dist/views/studio-design/PackageIdInput.d.ts +31 -0
- package/dist/views/studio-design/PackageIdInput.js +40 -0
- package/dist/views/studio-design/StudioDesignSurface.d.ts +13 -0
- package/dist/views/studio-design/StudioDesignSurface.js +962 -145
- package/dist/views/studio-design/metadataError.d.ts +23 -0
- package/dist/views/studio-design/metadataError.js +44 -0
- package/dist/views/studio-design/packageSurfaces.d.ts +49 -0
- package/dist/views/studio-design/packageSurfaces.js +34 -0
- package/dist/views/studio-design/packages-io.d.ts +38 -0
- package/dist/views/studio-design/packages-io.js +73 -0
- package/dist/views/studio-design/skeletons.d.ts +16 -0
- package/dist/views/studio-design/skeletons.js +51 -0
- package/package.json +42 -39
|
@@ -121,19 +121,28 @@ export function OrganizationsPage() {
|
|
|
121
121
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
122
122
|
}, [isOrganizationsLoading, orgList.length, manageMode, wantsCreate]);
|
|
123
123
|
// Open the create dialog when arriving via the header "Create workspace"
|
|
124
|
-
// entry (`?create=1`)
|
|
124
|
+
// entry (`?create=1`), OR for a brand-new user who has ZERO organizations:
|
|
125
|
+
// they have nothing to pick, so land them straight on the "name your
|
|
126
|
+
// workspace" form instead of an empty picker (one hop fewer on first run).
|
|
127
|
+
// The empty-state (with its own "New organization" button) remains the
|
|
128
|
+
// backdrop if they dismiss the dialog. Guarded so closing never re-opens it.
|
|
125
129
|
const createOpenedRef = useRef(false);
|
|
126
130
|
useEffect(() => {
|
|
127
|
-
if (
|
|
131
|
+
if (isOrganizationsLoading)
|
|
132
|
+
return;
|
|
133
|
+
const firstRunNoOrg = orgList.length === 0 && canCreateOrg;
|
|
134
|
+
if ((wantsCreate || firstRunNoOrg) && !createOpenedRef.current) {
|
|
128
135
|
createOpenedRef.current = true;
|
|
129
136
|
setIsCreateOpen(true);
|
|
130
137
|
}
|
|
131
|
-
}, [wantsCreate]);
|
|
132
|
-
// Show a spinner while we're either still loading,
|
|
133
|
-
// because there's only one org
|
|
134
|
-
//
|
|
138
|
+
}, [wantsCreate, orgList.length, canCreateOrg, isOrganizationsLoading]);
|
|
139
|
+
// Show a spinner while we're either still loading, about to auto-redirect
|
|
140
|
+
// because there's only one org, or about to auto-open the create dialog for a
|
|
141
|
+
// brand-new zero-org user. This prevents the picker / empty-state from
|
|
142
|
+
// briefly flashing on screen before the redirect or dialog.
|
|
135
143
|
const willAutoSelect = !wantsCreate && !isOrganizationsLoading && orgList.length === 1;
|
|
136
|
-
|
|
144
|
+
const willAutoCreate = !isOrganizationsLoading && orgList.length === 0 && canCreateOrg && !createOpenedRef.current;
|
|
145
|
+
if (isOrganizationsLoading || willAutoSelect || willAutoCreate) {
|
|
137
146
|
return (_jsx("div", { className: "flex flex-1 items-center justify-center py-20", children: _jsx(Loader2, { className: "h-5 w-5 animate-spin text-muted-foreground" }) }));
|
|
138
147
|
}
|
|
139
148
|
return (_jsxs("div", { className: "mx-auto w-full max-w-5xl px-4 sm:px-6 py-8 sm:py-12", children: [_jsxs("div", { className: "mb-8", children: [_jsx("h1", { className: "text-2xl sm:text-3xl font-bold tracking-tight", children: t('organizations.heading', { defaultValue: 'Your Organizations' }) }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: t('organizations.subtitle', {
|
|
@@ -19,9 +19,10 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
19
19
|
*/
|
|
20
20
|
import { createContext, useContext, useEffect, useMemo, } from 'react';
|
|
21
21
|
import { useUrlOverlay } from '../hooks/useUrlOverlay';
|
|
22
|
+
import { COMMAND_PALETTE_PARAM } from '../urlParams';
|
|
22
23
|
const CommandPaletteContext = createContext(null);
|
|
23
24
|
export function CommandPaletteProvider({ children }) {
|
|
24
|
-
const { open, setOpen, openOverlay, closeOverlay, toggleOverlay } = useUrlOverlay(
|
|
25
|
+
const { open, setOpen, openOverlay, closeOverlay, toggleOverlay } = useUrlOverlay(COMMAND_PALETTE_PARAM, {
|
|
25
26
|
alias: 'cmdk',
|
|
26
27
|
});
|
|
27
28
|
// ⌘K / Ctrl+K accelerator. Lives here (not in CommandPalette) so the command
|
|
@@ -37,6 +37,21 @@ import { EnvironmentEntitlementDialog } from '../environment/EnvironmentEntitlem
|
|
|
37
37
|
import { entitlementDialogFromError } from '../environment/entitlements';
|
|
38
38
|
import { resolvePageVarTokens } from '../utils/resolvePageVarTokens';
|
|
39
39
|
const FALLBACK_USER = { id: 'current-user', name: 'Demo User', isPlatformAdmin: false };
|
|
40
|
+
/**
|
|
41
|
+
* An action that also mounts on list rows (`list_item`) or record pages is
|
|
42
|
+
* designed to run against a single record. When such an action is launched
|
|
43
|
+
* from the list toolbar with nothing selected, there is no record context to
|
|
44
|
+
* resolve — block up front instead of triggering a run that fails at its
|
|
45
|
+
* first record-bound step (#2210: "Update requires an ID"). Actions declaring
|
|
46
|
+
* only object-level locations (e.g. `['list_toolbar']`) are left alone: they
|
|
47
|
+
* legitimately run without a record.
|
|
48
|
+
*/
|
|
49
|
+
function isRecordScoped(action) {
|
|
50
|
+
const locations = action.locations;
|
|
51
|
+
if (!Array.isArray(locations))
|
|
52
|
+
return false;
|
|
53
|
+
return locations.some((l) => l === 'list_item' || l === 'record_header' || l === 'record_more' || l === 'record_section');
|
|
54
|
+
}
|
|
40
55
|
export function useConsoleActionRuntime(opts) {
|
|
41
56
|
const { dataSource, objects, objectName, onRefresh } = opts;
|
|
42
57
|
const navigate = useNavigate();
|
|
@@ -297,6 +312,10 @@ export function useConsoleActionRuntime(opts) {
|
|
|
297
312
|
// context as `selectedRecords`. Flows take a single `recordId` input
|
|
298
313
|
// variable, so a multi-row selection is ambiguous: block with a message
|
|
299
314
|
// instead of triggering a run that fails at its first record-bound node.
|
|
315
|
+
// Zero selection is blocked too when the action is record-scoped (it
|
|
316
|
+
// also mounts on list rows) — otherwise the wizard opens, collects
|
|
317
|
+
// input, and dies at its first record-bound node ("Update requires an
|
|
318
|
+
// ID"). Pure object-level toolbar flows keep triggering with no record.
|
|
300
319
|
if (recordId == null) {
|
|
301
320
|
const selected = Array.isArray(context?.selectedRecords) ? context.selectedRecords : [];
|
|
302
321
|
if (selected.length === 1) {
|
|
@@ -305,6 +324,9 @@ export function useConsoleActionRuntime(opts) {
|
|
|
305
324
|
else if (selected.length > 1) {
|
|
306
325
|
return { success: false, error: 'This flow runs on a single record — select exactly one row.' };
|
|
307
326
|
}
|
|
327
|
+
else if (isRecordScoped(action)) {
|
|
328
|
+
return { success: false, error: 'This flow runs on a single record — select a row first.' };
|
|
329
|
+
}
|
|
308
330
|
}
|
|
309
331
|
if (recordId != null && params.recordId == null)
|
|
310
332
|
params.recordId = recordId;
|
|
@@ -358,7 +380,8 @@ export function useConsoleActionRuntime(opts) {
|
|
|
358
380
|
// Same list_toolbar fallback as flowHandler: no `_rowRecord` means the
|
|
359
381
|
// action came from the toolbar — resolve the recordId from the grid's
|
|
360
382
|
// checkbox selection (published as `selectedRecords`). Multi-select is
|
|
361
|
-
// ambiguous for a single-record action, so block with a message
|
|
383
|
+
// ambiguous for a single-record action, so block with a message; so is
|
|
384
|
+
// zero selection when the action is record-scoped (see isRecordScoped).
|
|
362
385
|
if (resolvedRecordId == null) {
|
|
363
386
|
const selected = Array.isArray(context?.selectedRecords) ? context.selectedRecords : [];
|
|
364
387
|
if (selected.length === 1) {
|
|
@@ -368,6 +391,9 @@ export function useConsoleActionRuntime(opts) {
|
|
|
368
391
|
// The runner's post-execution hook surfaces `error` as a toast.
|
|
369
392
|
return { success: false, error: 'This action runs on a single record — select exactly one row.' };
|
|
370
393
|
}
|
|
394
|
+
else if (isRecordScoped(action)) {
|
|
395
|
+
return { success: false, error: 'This action runs on a single record — select a row first.' };
|
|
396
|
+
}
|
|
371
397
|
}
|
|
372
398
|
// Re-entrancy guard.
|
|
373
399
|
const inflightKey = `${targetName}:${resolvedRecordId ?? ''}`;
|
|
@@ -464,7 +490,9 @@ export function useConsoleActionRuntime(opts) {
|
|
|
464
490
|
}
|
|
465
491
|
catch { /* ignore */ }
|
|
466
492
|
}
|
|
467
|
-
toast
|
|
493
|
+
// Don't toast here — the ActionRunner's post-execution hook surfaces
|
|
494
|
+
// `error` as a toast (see apiHandler/flowHandler, which likewise only
|
|
495
|
+
// return). Toasting here too double-fires the error (two identical toasts).
|
|
468
496
|
return { success: false, error: errMsg };
|
|
469
497
|
}
|
|
470
498
|
const shouldRefresh = action.refreshAfter !== false;
|
|
@@ -525,7 +553,8 @@ export function useConsoleActionRuntime(opts) {
|
|
|
525
553
|
catch { /* ignore */ }
|
|
526
554
|
}
|
|
527
555
|
const msg = error.message;
|
|
528
|
-
|
|
556
|
+
// The ActionRunner's post-execution hook toasts `error`; returning it here
|
|
557
|
+
// (without a local toast.error) avoids the double toast.
|
|
529
558
|
return { success: false, error: msg };
|
|
530
559
|
}
|
|
531
560
|
finally {
|
|
@@ -58,14 +58,21 @@ export function useObjectActions({ objectName, objectLabel, dataSource, onEdit,
|
|
|
58
58
|
label: objectLabel || objectName,
|
|
59
59
|
defaultValue: `Deleted ${succeeded} ${objectLabel || objectName} records`,
|
|
60
60
|
}));
|
|
61
|
-
|
|
61
|
+
// `silent`: the handler already toasted the localized summary above;
|
|
62
|
+
// without this the runner's post-execution hook adds a second, generic
|
|
63
|
+
// "Action completed successfully" toast (double success toast).
|
|
64
|
+
return { success: true, reload: true, silent: true };
|
|
62
65
|
}
|
|
63
66
|
toast.error(t('objectActions.bulkDeletePartial', {
|
|
64
67
|
succeeded,
|
|
65
68
|
failed,
|
|
66
69
|
defaultValue: `${succeeded} deleted, ${failed} failed`,
|
|
67
70
|
}));
|
|
68
|
-
|
|
71
|
+
// The toast above is the authoritative feedback (it carries the
|
|
72
|
+
// succeeded/failed summary the runner can't reconstruct). Return
|
|
73
|
+
// WITHOUT `error` so the ActionRunner post-execution hook — this runner
|
|
74
|
+
// has a toastHandler (onToast) — doesn't fire a second, duplicate toast.
|
|
75
|
+
return { success: false };
|
|
69
76
|
}
|
|
70
77
|
const recordId = action.params?.recordId ??
|
|
71
78
|
action.params?.record?.id ??
|
|
@@ -77,13 +84,18 @@ export function useObjectActions({ objectName, objectLabel, dataSource, onEdit,
|
|
|
77
84
|
await dataSource.delete(objectName, recordId);
|
|
78
85
|
onRefresh?.();
|
|
79
86
|
toast.success(t('objectActions.deleteSuccess', { label: objectLabel || objectName }));
|
|
80
|
-
|
|
87
|
+
// `silent`: handler owns the localized success toast above — suppress the
|
|
88
|
+
// runner's generic duplicate (see the bulk branch).
|
|
89
|
+
return { success: true, reload: true, silent: true };
|
|
81
90
|
}
|
|
82
91
|
catch (err) {
|
|
83
92
|
toast.error(t('objectActions.deleteFailed', { label: objectLabel || objectName }), {
|
|
84
93
|
description: err.message,
|
|
85
94
|
});
|
|
86
|
-
|
|
95
|
+
// Keep the richer toast above (label + error description) and return
|
|
96
|
+
// WITHOUT `error` so the ActionRunner post-execution hook doesn't toast
|
|
97
|
+
// the raw message a second time. See the bulk branch for the rationale.
|
|
98
|
+
return { success: false };
|
|
87
99
|
}
|
|
88
100
|
});
|
|
89
101
|
// Handler: navigate
|
package/dist/index.d.ts
CHANGED
|
@@ -62,6 +62,7 @@ import './console/home/CloudOnboardingNext';
|
|
|
62
62
|
export { MetadataDirectoryPage, MetadataResourceRouter, MetadataResourceListPage, MetadataResourceEditPage, MetadataResourceHistoryPage, MetadataDiagnosticsPage, MetadataQuickFind, MetadataPageShell, SchemaForm, LayeredDiff, registerMetadataResource, getMetadataResource, listMetadataResources, resolveResourceConfig, useMetadataClient, useMetadataTypes, useTypesIndex, useGlobalDiagnostics, matchesQuery, registerMetadataPreview, getMetadataPreview, listMetadataPreviewTypes, registerMetadataInspector, getMetadataInspector, listMetadataInspectorTypes, } from './views/metadata-admin';
|
|
63
63
|
export type { MetadataResourceConfig, MetadataDomain, RichMetadataTypeEntry, MetadataPreview, MetadataPreviewProps, MetadataSelection, MetadataInspector, MetadataInspectorProps, } from './views/metadata-admin';
|
|
64
64
|
export { StudioDesignSurface, type StudioDesignSurfaceProps, } from './views/studio-design/StudioDesignSurface';
|
|
65
|
+
export { BuilderLanding } from './views/studio-design/BuilderLanding';
|
|
65
66
|
export { assistantBus, useAssistant, useRegisterAssistantEditor, requestAssistantOpen, } from './assistant/assistantBus';
|
|
66
67
|
export type { AssistantSnapshot, AssistantEditorContext, AssistantEditorField, } from './assistant/assistantBus';
|
|
67
68
|
export { RemediationOverlay } from './console/RemediationOverlay';
|
package/dist/index.js
CHANGED
|
@@ -82,6 +82,9 @@ export { MetadataDirectoryPage, MetadataResourceRouter, MetadataResourceListPage
|
|
|
82
82
|
// Studio WYSIWYG design surface (ADR-0080) — the open-source design surface.
|
|
83
83
|
// The left AI copilot is an injected `aiSlot`; OSS renders three zones.
|
|
84
84
|
export { StudioDesignSurface, } from './views/studio-design/StudioDesignSurface';
|
|
85
|
+
// The builder's front door: pick/create a writable package → pillar builder.
|
|
86
|
+
// Standalone at `/studio` and embedded via the `studio:builder` component ref.
|
|
87
|
+
export { BuilderLanding } from './views/studio-design/BuilderLanding';
|
|
85
88
|
// AI assistant bus — connects the metadata designers to the global chat.
|
|
86
89
|
export { assistantBus, useAssistant, useRegisterAssistantEditor, requestAssistantOpen, } from './assistant/assistantBus';
|
|
87
90
|
export { RemediationOverlay } from './console/RemediationOverlay';
|
package/dist/layout/AppHeader.js
CHANGED
|
@@ -20,7 +20,7 @@ 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, Plus, Layers, Bot, User, BookOpen, ExternalLink, Keyboard, } from 'lucide-react';
|
|
23
|
+
import { Search, HelpCircle, ChevronDown, Check, Lock, LogOut, Plus, Layers, Bot, User, BookOpen, ExternalLink, Keyboard, Hammer, } 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';
|
|
@@ -32,14 +32,15 @@ import { InboxPopover } from './InboxPopover';
|
|
|
32
32
|
import { AppSwitcher } from './AppSwitcher';
|
|
33
33
|
import { useAdapter } from '../providers/AdapterProvider';
|
|
34
34
|
import { useObjectTranslation, useObjectLabel } from '@object-ui/i18n';
|
|
35
|
-
import { useAuth, getUserInitials } from '@object-ui/auth';
|
|
35
|
+
import { useAuth, getUserInitials, useIsWorkspaceAdmin } from '@object-ui/auth';
|
|
36
36
|
import { useMetadata } from '../providers/MetadataProvider';
|
|
37
|
-
import { resolveI18nLabel, preferLocal, matchAppBySegment, appRouteSegment } from '../utils';
|
|
37
|
+
import { resolveI18nLabel, preferLocal, matchAppBySegment, appRouteSegment, appStudioDesignPath } from '../utils';
|
|
38
38
|
import { getIcon } from '../utils/getIcon';
|
|
39
39
|
import { useMobileViewSwitcher } from './MobileViewSwitcherContext';
|
|
40
40
|
import { useNavigationContext } from '../context/NavigationContext';
|
|
41
41
|
import { useCommandPalette } from '../context/CommandPaletteProvider';
|
|
42
42
|
import { useUrlOverlay } from '../hooks/useUrlOverlay';
|
|
43
|
+
import { KEYBOARD_SHORTCUTS_PARAM } from '../urlParams';
|
|
43
44
|
import { useAiSurfaceEnabled } from '../hooks/useAiSurface';
|
|
44
45
|
import { getProductName } from '../runtime-config';
|
|
45
46
|
import { LocalizedSidebarTrigger } from './LocalizedSidebarTrigger';
|
|
@@ -65,13 +66,16 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
|
|
|
65
66
|
const { openCommandPalette } = useCommandPalette();
|
|
66
67
|
// Click-reachable entry for the keyboard-shortcuts dialog (was `?`-key only).
|
|
67
68
|
// Shares the `?shortcuts=1` URL param with KeyboardShortcutsDialog (C2/C3).
|
|
68
|
-
const { openOverlay: openShortcuts } = useUrlOverlay(
|
|
69
|
+
const { openOverlay: openShortcuts } = useUrlOverlay(KEYBOARD_SHORTCUTS_PARAM);
|
|
69
70
|
const { user, signOut, isAuthEnabled, organizations, activeOrganization, isOrganizationsLoading, getAuthConfig, } = useAuth();
|
|
70
71
|
const dataSource = useAdapter();
|
|
71
72
|
// Runtime AI gating: hide the top-bar AI entry point when the server serves
|
|
72
73
|
// no AI (Community Edition) so it can't dead-end on a chat with no agent.
|
|
73
74
|
// Same signal as the FAB and the `/ai` route guard.
|
|
74
75
|
const { enabled: aiEnabled } = useAiSurfaceEnabled();
|
|
76
|
+
// Design entry points mutate shared package metadata, so the app → Studio
|
|
77
|
+
// bridge below is admin-only (mirrors the runtime view/page editors).
|
|
78
|
+
const isWorkspaceAdmin = useIsWorkspaceAdmin();
|
|
75
79
|
const { t } = useObjectTranslation();
|
|
76
80
|
const { objectLabel, dashboardLabel, pageLabel, reportLabel, viewLabel, appLabel } = useObjectLabel();
|
|
77
81
|
const { apps: metadataApps, dashboards: metadataDashboards, pages: metadataPages, reports: metadataReports } = useMetadata();
|
|
@@ -489,6 +493,10 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
|
|
|
489
493
|
const currentAppDocs = currentAppPackageId
|
|
490
494
|
? (helpDocs ?? []).filter((d) => d._packageId === currentAppPackageId)
|
|
491
495
|
: [];
|
|
496
|
+
// App → Studio reverse bridge (ADR-0080): admins jump from the running app
|
|
497
|
+
// to its owning package's design surface. Null when there is nothing to
|
|
498
|
+
// open (non-admin, or no owning package).
|
|
499
|
+
const studioDesignPath = isApp ? appStudioDesignPath(currentApp, isWorkspaceAdmin) : null;
|
|
492
500
|
const objectSiblings = appObjects.map((o) => ({
|
|
493
501
|
label: objectLabel(o),
|
|
494
502
|
href: `${baseHref}/${o.name}`,
|
|
@@ -570,7 +578,7 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
|
|
|
570
578
|
if (!isActive)
|
|
571
579
|
mobileSwitcher.onChange(v.id);
|
|
572
580
|
}, 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));
|
|
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)
|
|
581
|
+
}) })] })) : (_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 }), studioDesignPath && (_jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 shrink-0", asChild: true, "data-testid": "app-design-in-studio-button", "aria-label": t('topbar.designInStudio', { defaultValue: 'Design in Studio' }), title: t('topbar.designInStudio', { defaultValue: 'Design in Studio' }), children: _jsx(Link, { to: studioDesignPath, children: _jsx(Hammer, { className: "h-4 w-4" }) }) })), 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)
|
|
574
582
|
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
|
|
575
583
|
? `/apps/${currentAppPackageId}/docs/${currentAppDocs[0].name}`
|
|
576
584
|
: `/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 || [])
|
|
@@ -10,12 +10,12 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
10
10
|
* @module
|
|
11
11
|
*/
|
|
12
12
|
import * as React from 'react';
|
|
13
|
-
import { useNavigate, Link } from 'react-router-dom';
|
|
13
|
+
import { useNavigate, useLocation, Link } from 'react-router-dom';
|
|
14
14
|
import { Layers } from 'lucide-react';
|
|
15
15
|
import { getIcon as resolveIcon } from '../utils/getIcon';
|
|
16
16
|
import { Sidebar, SidebarHeader, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupLabel, SidebarGroupContent, SidebarMenu, SidebarMenuItem, SidebarMenuButton, SidebarMenuAction, SidebarInput, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuGroup, Avatar, AvatarImage, AvatarFallback, useSidebar, } from '@object-ui/components';
|
|
17
|
-
import { ChevronsUpDown, Plus, Settings, LogOut, Database, Clock, Star, StarOff, Search, Pencil, ChevronRight, Home, } from 'lucide-react';
|
|
18
|
-
import { NavigationRenderer, resolveHref } from '@object-ui/layout';
|
|
17
|
+
import { ChevronsUpDown, Plus, Settings, LogOut, Database, Clock, Star, StarOff, Search, Pencil, ChevronRight, Home, ListTree, } from 'lucide-react';
|
|
18
|
+
import { NavigationRenderer, resolveHref, resolveActiveNavItem } from '@object-ui/layout';
|
|
19
19
|
import { useMetadata } from '../providers/MetadataProvider';
|
|
20
20
|
import { useExpressionContext, evaluateVisibility } from '../providers/ExpressionProvider';
|
|
21
21
|
import { useAuth, useIsWorkspaceAdmin, getUserInitials } from '@object-ui/auth';
|
|
@@ -95,6 +95,7 @@ export function AppSidebar({ activeAppName, onAppChange }) {
|
|
|
95
95
|
const { user, signOut, isAuthEnabled } = useAuth();
|
|
96
96
|
const isWorkspaceAdmin = useIsWorkspaceAdmin();
|
|
97
97
|
const navigate = useNavigate();
|
|
98
|
+
const location = useLocation();
|
|
98
99
|
const { t } = useObjectTranslation();
|
|
99
100
|
const { objectLabel: resolveNavObjectLabel, viewLabel: resolveNavViewLabel } = useObjectLabel();
|
|
100
101
|
// Swipe-from-left-edge gesture to open sidebar on mobile
|
|
@@ -208,7 +209,12 @@ export function AppSidebar({ activeAppName, onAppChange }) {
|
|
|
208
209
|
items.push({ id: 'sys-objects', label: t('layout.systemNav.objectManager', { defaultValue: 'Object Manager' }), type: 'url', url: '/apps/setup/system/metadata/object', icon: 'database' }, { id: 'sys-datasources', label: t('layout.systemNav.datasources', { defaultValue: 'Datasources' }), type: 'url', url: '/apps/setup/component/metadata/resource?type=datasource', icon: 'database' }, { id: 'sys-users', label: t('layout.systemNav.users', { defaultValue: 'Users' }), type: 'url', url: '/apps/setup/system/users', icon: 'users' }, { id: 'sys-orgs', label: t('layout.systemNav.organizations', { defaultValue: 'Organizations' }), type: 'url', url: '/apps/setup/system/organizations', icon: 'building-2' }, { id: 'sys-roles', label: t('layout.systemNav.roles', { defaultValue: 'Roles' }), type: 'url', url: '/apps/setup/system/roles', icon: 'shield' }, { id: 'sys-config', label: t('layout.systemNav.configuration', { defaultValue: 'Configuration' }), type: 'url', url: '/apps/setup/system/settings', icon: 'sliders-horizontal' });
|
|
209
210
|
return items;
|
|
210
211
|
}, [isWorkspaceAdmin, t]);
|
|
211
|
-
return (_jsxs(_Fragment, { children: [_jsxs(Sidebar, { collapsible: "icon", children: [_jsx(SidebarHeader, { children: _jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: activeApp ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(SidebarMenuButton, { size: "lg", className: "data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground", children: [_jsx("div", { className: "flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground", style: primaryColor ? { backgroundColor: primaryColor } : undefined, children: logo ? (_jsx("img", { src: logo, alt: resolveI18nLabel(activeApp.label, t), className: "size-6 object-contain" })) : (React.createElement(getIcon(activeApp.icon), { className: "size-4" })) }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: resolveI18nLabel(activeApp.label, t) }), _jsx("span", { className: "truncate text-xs", children: resolveI18nLabel(activeApp.description, t) || t('layout.appSwitcher.appsAvailable', { defaultValue: '{{count}} apps available', count: activeApps.length }) })] }), _jsx(ChevronsUpDown, { className: "ml-auto" })] }) }), _jsxs(DropdownMenuContent, { className: "w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg", align: "start", side: isMobile ? "bottom" : "right", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "text-xs text-muted-foreground", children: t('layout.appSwitcher.switchApplication', { defaultValue: 'Switch Application' }) }), activeApps.map((app) => (_jsxs(DropdownMenuItem, { onClick: () => onAppChange(appRouteSegment(app) ?? app.name), className: "gap-2 p-2", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-sm border", children: app.icon ? React.createElement(getIcon(app.icon), { className: "size-3" }) : _jsx(Database, { className: "size-3" }) }), resolveI18nLabel(app.label, t), activeApp.name === app.name && _jsx("span", { className: "ml-auto text-xs", children: "\u2713" })] }, app.name))), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate('/home'), "data-testid": "home-link-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Home, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: t('layout.appSwitcher.home') })] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate(`/apps/${appRouteSegment(activeApp) ?? activeAppName}/create-app`), "data-testid": "add-app-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Plus, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: t('layout.appSwitcher.addApp') })] }), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate(`/apps/${appRouteSegment(activeApp) ?? activeAppName}/edit-app/${activeAppName}`), "data-testid": "edit-app-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Pencil, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: t('layout.appSwitcher.editApp') })] }), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () =>
|
|
212
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Sidebar, { collapsible: "icon", children: [_jsx(SidebarHeader, { children: _jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: activeApp ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(SidebarMenuButton, { size: "lg", className: "data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground", children: [_jsx("div", { className: "flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground", style: primaryColor ? { backgroundColor: primaryColor } : undefined, children: logo ? (_jsx("img", { src: logo, alt: resolveI18nLabel(activeApp.label, t), className: "size-6 object-contain" })) : (React.createElement(getIcon(activeApp.icon), { className: "size-4" })) }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: resolveI18nLabel(activeApp.label, t) }), _jsx("span", { className: "truncate text-xs", children: resolveI18nLabel(activeApp.description, t) || t('layout.appSwitcher.appsAvailable', { defaultValue: '{{count}} apps available', count: activeApps.length }) })] }), _jsx(ChevronsUpDown, { className: "ml-auto" })] }) }), _jsxs(DropdownMenuContent, { className: "w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg", align: "start", side: isMobile ? "bottom" : "right", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "text-xs text-muted-foreground", children: t('layout.appSwitcher.switchApplication', { defaultValue: 'Switch Application' }) }), activeApps.map((app) => (_jsxs(DropdownMenuItem, { onClick: () => onAppChange(appRouteSegment(app) ?? app.name), className: "gap-2 p-2", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-sm border", children: app.icon ? React.createElement(getIcon(app.icon), { className: "size-3" }) : _jsx(Database, { className: "size-3" }) }), resolveI18nLabel(app.label, t), activeApp.name === app.name && _jsx("span", { className: "ml-auto text-xs", children: "\u2713" })] }, app.name))), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate('/home'), "data-testid": "home-link-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Home, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: t('layout.appSwitcher.home') })] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate(`/apps/${appRouteSegment(activeApp) ?? activeAppName}/create-app`), "data-testid": "add-app-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Plus, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: t('layout.appSwitcher.addApp') })] }), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate(`/apps/${appRouteSegment(activeApp) ?? activeAppName}/edit-app/${activeAppName}`), "data-testid": "edit-app-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Pencil, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: t('layout.appSwitcher.editApp') })] }), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => {
|
|
213
|
+
const seg = appRouteSegment(activeApp) ?? activeAppName;
|
|
214
|
+
const active = resolveActiveNavItem(processedNavigation, location.pathname, location.search, basePath, { currentUserId: user?.id ?? null, contextValues });
|
|
215
|
+
const sel = active ? `?sel=${encodeURIComponent(`nav:${active.id}`)}` : '';
|
|
216
|
+
navigate(`/apps/${seg}/metadata/app/${activeAppName}${sel}`);
|
|
217
|
+
}, "data-testid": "edit-navigation-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(ListTree, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: t('layout.appSwitcher.editNavigation', { defaultValue: 'Edit Navigation' }) })] }), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate('/apps/setup/system/apps'), "data-testid": "manage-all-apps-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Settings, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: t('layout.appSwitcher.manageAllApps') })] })] })] })) : (_jsxs(SidebarMenuButton, { size: "lg", onClick: () => navigate('/apps/setup'), "data-testid": "system-sidebar-header", children: [_jsx("div", { className: "flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground", children: _jsx(Settings, { className: "size-4" }) }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: t('layout.appSwitcher.systemConsole') }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: t('layout.appSwitcher.noAppsConfigured') })] })] })) }) }) }), _jsx(SidebarContent, { children: activeApp ? (_jsxs(_Fragment, { children: [areas.length > 1 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Layers, { className: "h-3.5 w-3.5" }), t('sidebar.area', { defaultValue: 'Area' })] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: areas.map((area) => {
|
|
212
218
|
const AreaIcon = getIcon(area.icon);
|
|
213
219
|
const isActiveArea = area.id === activeAreaId;
|
|
214
220
|
return (_jsx(SidebarMenuItem, { children: _jsxs(SidebarMenuButton, { isActive: isActiveArea, tooltip: area.label, onClick: () => setActiveAreaId(area.id), children: [_jsx(AreaIcon, { className: "h-4 w-4" }), _jsx("span", { children: area.label })] }) }, area.id));
|
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Env vars consumed (all optional):
|
|
13
13
|
* - `VITE_SENTRY_DSN` — DSN; absent disables the integration entirely
|
|
14
|
+
* - `VITE_SENTRY_ENABLED` — set to `"false"` to force-disable reporting
|
|
15
|
+
* even when a DSN is configured (e.g. a fork that keeps the upstream DSN
|
|
16
|
+
* in `.env.production` but doesn't want to report to it). Default: enabled
|
|
17
|
+
* whenever a DSN is present — this is an *additional* off switch, not a
|
|
18
|
+
* change to that default.
|
|
14
19
|
* - `VITE_SENTRY_ENVIRONMENT` — defaults to `MODE` (production/development)
|
|
15
20
|
* - `VITE_SENTRY_RELEASE` — defaults to `VITE_APP_VERSION` or `unknown`
|
|
16
21
|
* - `VITE_SENTRY_TRACES_SAMPLE_RATE` — defaults to `0.1`
|
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Env vars consumed (all optional):
|
|
13
13
|
* - `VITE_SENTRY_DSN` — DSN; absent disables the integration entirely
|
|
14
|
+
* - `VITE_SENTRY_ENABLED` — set to `"false"` to force-disable reporting
|
|
15
|
+
* even when a DSN is configured (e.g. a fork that keeps the upstream DSN
|
|
16
|
+
* in `.env.production` but doesn't want to report to it). Default: enabled
|
|
17
|
+
* whenever a DSN is present — this is an *additional* off switch, not a
|
|
18
|
+
* change to that default.
|
|
14
19
|
* - `VITE_SENTRY_ENVIRONMENT` — defaults to `MODE` (production/development)
|
|
15
20
|
* - `VITE_SENTRY_RELEASE` — defaults to `VITE_APP_VERSION` or `unknown`
|
|
16
21
|
* - `VITE_SENTRY_TRACES_SAMPLE_RATE` — defaults to `0.1`
|
|
@@ -38,7 +43,7 @@ export function initSentry() {
|
|
|
38
43
|
initPromise = (async () => {
|
|
39
44
|
const env = import.meta.env ?? {};
|
|
40
45
|
const dsn = env.VITE_SENTRY_DSN;
|
|
41
|
-
if (!dsn)
|
|
46
|
+
if (!dsn || env.VITE_SENTRY_ENABLED === 'false')
|
|
42
47
|
return false;
|
|
43
48
|
try {
|
|
44
49
|
const Sentry = (await import('@sentry/react'));
|
|
@@ -12,8 +12,38 @@ export interface DraftChangeEntry {
|
|
|
12
12
|
/** `new` = no published version; `update` = overwrites one; undefined = probing. */
|
|
13
13
|
kind?: 'new' | 'update';
|
|
14
14
|
}
|
|
15
|
+
export interface EntryChangeDetail {
|
|
16
|
+
/** Field-level diff — present when either side carries a `fields` map. */
|
|
17
|
+
fields: {
|
|
18
|
+
added: string[];
|
|
19
|
+
changed: Array<{
|
|
20
|
+
name: string;
|
|
21
|
+
keys: string[];
|
|
22
|
+
}>;
|
|
23
|
+
removed: string[];
|
|
24
|
+
} | null;
|
|
25
|
+
/** Top-level keys (other than `fields`) whose values differ. */
|
|
26
|
+
changedKeys: string[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* What publishing this draft actually changes, computed client-side from the
|
|
30
|
+
* published body (null when the item is NEW) and the pending draft body.
|
|
31
|
+
* `fields` gets the dedicated designer diff; every other top-level key is
|
|
32
|
+
* compared wholesale — enough to answer "which parts of this item move".
|
|
33
|
+
*/
|
|
34
|
+
export declare function computeChangeDetail(published: Record<string, unknown> | null, draft: Record<string, unknown> | null): EntryChangeDetail;
|
|
15
35
|
export interface DraftChangesPanelProps {
|
|
16
36
|
open: boolean;
|
|
17
37
|
onOpenChange: (open: boolean) => void;
|
|
38
|
+
/** When set, list only pending drafts belonging to this package (Studio is package-scoped). */
|
|
39
|
+
packageId?: string | null;
|
|
40
|
+
/**
|
|
41
|
+
* When provided, the panel renders a confirm footer whose button invokes
|
|
42
|
+
* this — turning the panel into the review-then-publish step. The caller
|
|
43
|
+
* still owns the actual publish request and closing the panel on success.
|
|
44
|
+
*/
|
|
45
|
+
onPublish?: () => void | Promise<void>;
|
|
46
|
+
/** Disables the confirm button and shows a spinner while the caller publishes. */
|
|
47
|
+
publishing?: boolean;
|
|
18
48
|
}
|
|
19
|
-
export declare function DraftChangesPanel({ open, onOpenChange }: DraftChangesPanelProps): import("react").JSX.Element;
|
|
49
|
+
export declare function DraftChangesPanel({ open, onOpenChange, packageId, onPublish, publishing, }: DraftChangesPanelProps): import("react").JSX.Element;
|