@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.
Files changed (90) hide show
  1. package/CHANGELOG.md +700 -0
  2. package/README.md +32 -0
  3. package/dist/chrome/KeyboardShortcutsDialog.js +2 -1
  4. package/dist/console/AppContent.js +145 -26
  5. package/dist/console/ConsoleShell.js +25 -3
  6. package/dist/console/home/CloudOnboardingNext.d.ts +9 -0
  7. package/dist/console/home/CloudOnboardingNext.js +14 -4
  8. package/dist/console/home/HomePage.js +34 -7
  9. package/dist/console/organizations/CreateWorkspaceDialog.js +33 -3
  10. package/dist/console/organizations/OrganizationsPage.js +16 -7
  11. package/dist/context/CommandPaletteProvider.js +2 -1
  12. package/dist/hooks/useConsoleActionRuntime.js +32 -3
  13. package/dist/hooks/useObjectActions.js +16 -4
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.js +3 -0
  16. package/dist/layout/AppHeader.js +13 -5
  17. package/dist/layout/AppSidebar.js +10 -4
  18. package/dist/observability/sentry.d.ts +5 -0
  19. package/dist/observability/sentry.js +6 -1
  20. package/dist/preview/DraftChangesPanel.d.ts +31 -1
  21. package/dist/preview/DraftChangesPanel.js +146 -18
  22. package/dist/urlParams.d.ts +68 -0
  23. package/dist/urlParams.js +76 -0
  24. package/dist/utils/appRoute.d.ts +15 -0
  25. package/dist/utils/appRoute.js +22 -0
  26. package/dist/utils/deriveRelatedLists.d.ts +20 -5
  27. package/dist/utils/deriveRelatedLists.js +31 -13
  28. package/dist/utils/index.d.ts +1 -1
  29. package/dist/utils/index.js +1 -1
  30. package/dist/utils/pageTabsUrlSync.d.ts +32 -0
  31. package/dist/utils/pageTabsUrlSync.js +43 -0
  32. package/dist/utils/recordFormNavigation.d.ts +40 -0
  33. package/dist/utils/recordFormNavigation.js +30 -0
  34. package/dist/utils/resolveViewId.d.ts +23 -0
  35. package/dist/utils/resolveViewId.js +37 -0
  36. package/dist/utils/warnSuppressedListNav.d.ts +10 -0
  37. package/dist/utils/warnSuppressedListNav.js +40 -0
  38. package/dist/views/InterfaceListPage.d.ts +1 -0
  39. package/dist/views/InterfaceListPage.js +7 -5
  40. package/dist/views/ObjectDataPage.d.ts +29 -0
  41. package/dist/views/ObjectDataPage.js +227 -0
  42. package/dist/views/ObjectView.js +65 -13
  43. package/dist/views/RecordDetailView.js +191 -123
  44. package/dist/views/RecordFormPage.js +7 -1
  45. package/dist/views/RelatedRecordActionsBridge.d.ts +33 -0
  46. package/dist/views/RelatedRecordActionsBridge.js +147 -0
  47. package/dist/views/metadata-admin/PackagesPage.js +18 -7
  48. package/dist/views/metadata-admin/PermissionMatrixEditor.d.ts +18 -1
  49. package/dist/views/metadata-admin/PermissionMatrixEditor.js +73 -14
  50. package/dist/views/metadata-admin/ResourceEditPage.js +39 -0
  51. package/dist/views/metadata-admin/i18n.d.ts +12 -21
  52. package/dist/views/metadata-admin/i18n.js +557 -6
  53. package/dist/views/metadata-admin/inspectors/AppNavInspector.d.ts +11 -4
  54. package/dist/views/metadata-admin/inspectors/AppNavInspector.js +141 -7
  55. package/dist/views/metadata-admin/inspectors/FlowReferenceField.d.ts +14 -0
  56. package/dist/views/metadata-admin/inspectors/FlowReferenceField.js +76 -5
  57. package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +60 -30
  58. package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +8 -1
  59. package/dist/views/metadata-admin/inspectors/flow-node-config.js +3 -2
  60. package/dist/views/metadata-admin/inspectors/nav-target.d.ts +52 -0
  61. package/dist/views/metadata-admin/inspectors/nav-target.js +149 -0
  62. package/dist/views/metadata-admin/nav-selection.d.ts +20 -0
  63. package/dist/views/metadata-admin/nav-selection.js +81 -0
  64. package/dist/views/metadata-admin/permission-slice.d.ts +66 -0
  65. package/dist/views/metadata-admin/permission-slice.js +70 -0
  66. package/dist/views/metadata-admin/previews/AppNavCanvas.js +19 -7
  67. package/dist/views/metadata-admin/previews/AppPreview.js +4 -2
  68. package/dist/views/metadata-admin/previews/FlowRunsPanel.d.ts +16 -7
  69. package/dist/views/metadata-admin/previews/FlowRunsPanel.js +18 -2
  70. package/dist/views/studio-design/BuilderLanding.d.ts +15 -0
  71. package/dist/views/studio-design/BuilderLanding.js +126 -0
  72. package/dist/views/studio-design/ObjectFormDesigner.d.ts +33 -0
  73. package/dist/views/studio-design/ObjectFormDesigner.js +231 -0
  74. package/dist/views/studio-design/ObjectSettingsPanel.d.ts +30 -0
  75. package/dist/views/studio-design/ObjectSettingsPanel.js +46 -0
  76. package/dist/views/studio-design/ObjectValidationsPanel.d.ts +30 -0
  77. package/dist/views/studio-design/ObjectValidationsPanel.js +80 -0
  78. package/dist/views/studio-design/PackageIdInput.d.ts +31 -0
  79. package/dist/views/studio-design/PackageIdInput.js +40 -0
  80. package/dist/views/studio-design/StudioDesignSurface.d.ts +13 -0
  81. package/dist/views/studio-design/StudioDesignSurface.js +962 -145
  82. package/dist/views/studio-design/metadataError.d.ts +23 -0
  83. package/dist/views/studio-design/metadataError.js +44 -0
  84. package/dist/views/studio-design/packageSurfaces.d.ts +49 -0
  85. package/dist/views/studio-design/packageSurfaces.js +34 -0
  86. package/dist/views/studio-design/packages-io.d.ts +38 -0
  87. package/dist/views/studio-design/packages-io.js +73 -0
  88. package/dist/views/studio-design/skeletons.d.ts +16 -0
  89. package/dist/views/studio-design/skeletons.js +51 -0
  90. 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`). Guarded so closing the dialog doesn't re-open it.
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 (wantsCreate && !createOpenedRef.current) {
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, or about to auto-redirect
133
- // because there's only one org. This prevents the picker from briefly
134
- // flashing on screen for single-org users.
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
- if (isOrganizationsLoading || willAutoSelect) {
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('palette', {
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.error(errMsg);
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
- toast.error(msg);
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
- return { success: true, reload: true };
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
- return { success: false, error: `${failed} failed` };
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
- return { success: true, reload: true };
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
- return { success: false, error: err.message };
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';
@@ -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('shortcuts');
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: () => 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
+ 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;