@object-ui/app-shell 5.0.2 → 5.2.1

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 (40) hide show
  1. package/CHANGELOG.md +401 -0
  2. package/dist/chrome/CommandPalette.d.ts +6 -1
  3. package/dist/chrome/CommandPalette.js +40 -4
  4. package/dist/chrome/ConsoleToaster.js +8 -1
  5. package/dist/chrome/RouteFader.d.ts +32 -0
  6. package/dist/chrome/RouteFader.js +52 -0
  7. package/dist/chrome/index.d.ts +2 -0
  8. package/dist/chrome/index.js +2 -0
  9. package/dist/chrome/toast-helpers.d.ts +48 -0
  10. package/dist/chrome/toast-helpers.js +59 -0
  11. package/dist/console/AppContent.js +108 -2
  12. package/dist/console/home/HomePage.js +5 -4
  13. package/dist/console/marketplace/MarketplaceInstalledPage.d.ts +17 -0
  14. package/dist/console/marketplace/MarketplaceInstalledPage.js +64 -0
  15. package/dist/console/marketplace/MarketplacePackagePage.d.ts +7 -0
  16. package/dist/console/marketplace/MarketplacePackagePage.js +199 -0
  17. package/dist/console/marketplace/MarketplacePage.d.ts +8 -0
  18. package/dist/console/marketplace/MarketplacePage.js +94 -0
  19. package/dist/console/marketplace/PackageIcon.d.ts +19 -0
  20. package/dist/console/marketplace/PackageIcon.js +17 -0
  21. package/dist/console/marketplace/marketplaceApi.d.ts +122 -0
  22. package/dist/console/marketplace/marketplaceApi.js +207 -0
  23. package/dist/index.d.ts +5 -6
  24. package/dist/index.js +4 -5
  25. package/dist/layout/AppHeader.js +25 -21
  26. package/dist/layout/AppSidebar.js +15 -11
  27. package/dist/layout/InboxPopover.js +43 -3
  28. package/dist/types.d.ts +0 -46
  29. package/dist/views/ObjectView.js +20 -7
  30. package/dist/views/RecordDetailView.js +213 -45
  31. package/package.json +25 -25
  32. package/src/styles.css +49 -0
  33. package/dist/components/DashboardRenderer.d.ts +0 -8
  34. package/dist/components/DashboardRenderer.js +0 -16
  35. package/dist/components/FormRenderer.d.ts +0 -8
  36. package/dist/components/FormRenderer.js +0 -31
  37. package/dist/components/ObjectRenderer.d.ts +0 -8
  38. package/dist/components/ObjectRenderer.js +0 -74
  39. package/dist/components/PageRenderer.d.ts +0 -7
  40. package/dist/components/PageRenderer.js +0 -14
@@ -18,7 +18,7 @@ import { ChevronsUpDown, Plus, Settings, LogOut, Database, Clock, Star, StarOff,
18
18
  import { NavigationRenderer } from '@object-ui/layout';
19
19
  import { useMetadata } from '../providers/MetadataProvider';
20
20
  import { useExpressionContext, evaluateVisibility } from '../providers/ExpressionProvider';
21
- import { useAuth, getUserInitials } from '@object-ui/auth';
21
+ import { useAuth, useIsWorkspaceAdmin, getUserInitials } from '@object-ui/auth';
22
22
  import { usePermissions } from '@object-ui/permissions';
23
23
  import { useRecentItems } from '../hooks/useRecentItems';
24
24
  import { useFavorites } from '../hooks/useFavorites';
@@ -92,6 +92,7 @@ const getIcon = resolveIcon;
92
92
  export function AppSidebar({ activeAppName, onAppChange }) {
93
93
  const { isMobile } = useSidebar();
94
94
  const { user, signOut, isAuthEnabled } = useAuth();
95
+ const isWorkspaceAdmin = useIsWorkspaceAdmin();
95
96
  const navigate = useNavigate();
96
97
  const { t } = useObjectTranslation();
97
98
  const { objectLabel: resolveNavObjectLabel, viewLabel: resolveNavViewLabel } = useObjectLabel();
@@ -186,16 +187,19 @@ export function AppSidebar({ activeAppName, onAppChange }) {
186
187
  }, [registeredObjectNames]);
187
188
  const basePath = activeApp ? `/apps/${activeAppName}` : '';
188
189
  // Fallback system navigation when no active app exists — routes into the Setup app.
189
- const systemFallbackNavigation = React.useMemo(() => [
190
- { id: 'sys-settings', label: 'System Settings', type: 'url', url: '/apps/setup', icon: 'settings' },
191
- { id: 'sys-apps', label: 'Applications', type: 'url', url: '/apps/setup/system/apps', icon: 'layout-grid' },
192
- { id: 'sys-objects', label: 'Object Manager', type: 'url', url: '/apps/setup/system/metadata/object', icon: 'database' },
193
- { id: 'sys-users', label: 'Users', type: 'url', url: '/apps/setup/system/users', icon: 'users' },
194
- { id: 'sys-orgs', label: 'Organizations', type: 'url', url: '/apps/setup/system/organizations', icon: 'building-2' },
195
- { id: 'sys-roles', label: 'Roles', type: 'url', url: '/apps/setup/system/roles', icon: 'shield' },
196
- { id: 'sys-config', label: 'Configuration', type: 'url', url: '/apps/setup/system/settings', icon: 'sliders-horizontal' },
197
- { id: 'sys-create-app', label: 'Create App', type: 'url', url: '/create-app', icon: 'plus' },
198
- ], []);
190
+ // The marketplace entry is hidden from non-admin members (install is gated to
191
+ // owner/admin on the server, so non-admins have no reason to see it).
192
+ const systemFallbackNavigation = React.useMemo(() => {
193
+ const items = [
194
+ { id: 'sys-settings', label: 'System Settings', type: 'url', url: '/apps/setup', icon: 'settings' },
195
+ { id: 'sys-apps', label: 'Applications', type: 'url', url: '/apps/setup/system/apps', icon: 'layout-grid' },
196
+ ];
197
+ if (isWorkspaceAdmin) {
198
+ items.push({ id: 'sys-marketplace', label: 'App Marketplace', type: 'url', url: '/apps/setup/system/marketplace', icon: 'store' });
199
+ }
200
+ items.push({ id: 'sys-objects', label: 'Object Manager', type: 'url', url: '/apps/setup/system/metadata/object', icon: 'database' }, { id: 'sys-users', label: 'Users', type: 'url', url: '/apps/setup/system/users', icon: 'users' }, { id: 'sys-orgs', label: 'Organizations', type: 'url', url: '/apps/setup/system/organizations', icon: 'building-2' }, { id: 'sys-roles', label: 'Roles', type: 'url', url: '/apps/setup/system/roles', icon: 'shield' }, { id: 'sys-config', label: 'Configuration', type: 'url', url: '/apps/setup/system/settings', icon: 'sliders-horizontal' }, { id: 'sys-create-app', label: 'Create App', type: 'url', url: '/create-app', icon: 'plus' });
201
+ return items;
202
+ }, [isWorkspaceAdmin]);
199
203
  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) || `${activeApps.length} Apps Available` })] }), _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: "Switch Application" }), activeApps.map((app) => (_jsxs(DropdownMenuItem, { onClick: () => onAppChange(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/${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/${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" }), "Area"] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: areas.map((area) => {
200
204
  const AreaIcon = getIcon(area.icon);
201
205
  const isActiveArea = area.id === activeAreaId;
@@ -14,7 +14,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
14
14
  *
15
15
  * @module
16
16
  */
17
- import { useState } from 'react';
17
+ import { useState, useEffect, useRef } from 'react';
18
18
  import { useNavigate, useParams } from 'react-router-dom';
19
19
  import { Button, Popover, PopoverContent, PopoverTrigger, Tabs, TabsList, TabsTrigger, TabsContent, } from '@object-ui/components';
20
20
  import { Bell, CheckSquare, Activity as ActivityIcon } from 'lucide-react';
@@ -42,8 +42,34 @@ export function InboxPopover({ notifications, unreadCount, pendingApprovalsCount
42
42
  const { currentAppName } = useNavigationContext();
43
43
  const [open, setOpen] = useState(false);
44
44
  const [tab, setTab] = useState('notifications');
45
+ // Sub-filter inside Notifications: default to Unread so users see what
46
+ // actually needs their attention first. The popover caps at 20 rows from
47
+ // the server (`?view=mine` already scopes to current user), so we filter
48
+ // client-side — switching tabs never re-fetches.
49
+ const [notifFilter, setNotifFilter] = useState('unread');
45
50
  const totalBadge = unreadCount + pendingApprovalsCount;
46
51
  const ariaLabel = t('sidebar.inboxAriaLabel', { defaultValue: 'Open inbox' });
52
+ // Pulse the bell once whenever the unread/approval pressure increases.
53
+ // We track the previous total in a ref so the very first render (when
54
+ // the counts arrive from the server) doesn't trigger a spurious pulse.
55
+ const prevTotalRef = useRef(null);
56
+ const [pulse, setPulse] = useState(false);
57
+ useEffect(() => {
58
+ const prev = prevTotalRef.current;
59
+ if (prev !== null && totalBadge > prev) {
60
+ setPulse(true);
61
+ const timeout = window.setTimeout(() => setPulse(false), 1200);
62
+ return () => window.clearTimeout(timeout);
63
+ }
64
+ prevTotalRef.current = totalBadge;
65
+ return undefined;
66
+ }, [totalBadge]);
67
+ // Keep prev in sync after the pulse window so a subsequent increase
68
+ // (e.g. 3 → 5 right after 5 settled) re-triggers the animation.
69
+ useEffect(() => {
70
+ if (!pulse)
71
+ prevTotalRef.current = totalBadge;
72
+ }, [pulse, totalBadge]);
47
73
  const goToApprovals = () => {
48
74
  setOpen(false);
49
75
  const app = currentAppName ?? params.appName;
@@ -75,10 +101,24 @@ export function InboxPopover({ notifications, unreadCount, pendingApprovalsCount
75
101
  navigate(target);
76
102
  }
77
103
  };
78
- return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 relative shrink-0", "aria-label": ariaLabel, title: t('sidebar.inbox', { defaultValue: 'Inbox' }), children: [_jsx(Bell, { className: "h-4 w-4" }), totalBadge > 0 && (_jsx("span", { className: "absolute -top-0.5 -right-0.5 h-4 min-w-[16px] rounded-full bg-red-500 text-[10px] leading-4 text-white text-center px-1", children: totalBadge > 9 ? '9+' : totalBadge }))] }) }), _jsxs(PopoverContent, { align: "end", sideOffset: 8, className: "w-96 p-0", children: [_jsxs("div", { className: "flex items-center justify-between px-3 pt-3 pb-1", children: [_jsx("div", { className: "text-sm font-semibold", children: t('sidebar.inbox', { defaultValue: 'Inbox' }) }), tab === 'notifications' && unreadCount > 0 && (_jsx("button", { type: "button", onClick: onMarkAllRead, className: "text-xs text-muted-foreground hover:text-foreground", children: t('notifications.markAllRead', { defaultValue: 'Mark all read' }) }))] }), _jsxs(Tabs, { value: tab, onValueChange: (v) => setTab(v), className: "w-full", children: [_jsxs(TabsList, { className: "w-full justify-start rounded-none border-b bg-transparent px-1 h-9", children: [_jsxs(TabsTrigger, { value: "notifications", className: "text-xs gap-1.5 data-[state=active]:bg-transparent", children: [_jsx(Bell, { className: "h-3.5 w-3.5" }), t('sidebar.notifications', { defaultValue: 'Notifications' }), unreadCount > 0 && (_jsx("span", { className: "ml-0.5 inline-flex h-4 min-w-[16px] items-center justify-center rounded-full bg-red-500 px-1 text-[10px] text-white", children: unreadCount > 9 ? '9+' : unreadCount }))] }), _jsxs(TabsTrigger, { value: "approvals", className: "text-xs gap-1.5 data-[state=active]:bg-transparent", children: [_jsx(CheckSquare, { className: "h-3.5 w-3.5" }), t('sidebar.approvals', { defaultValue: 'Approvals' }), pendingApprovalsCount > 0 && (_jsx("span", { className: "ml-0.5 inline-flex h-4 min-w-[16px] items-center justify-center rounded-full bg-amber-500 px-1 text-[10px] text-white", children: pendingApprovalsCount > 9 ? '9+' : pendingApprovalsCount }))] }), _jsxs(TabsTrigger, { value: "activity", className: "text-xs gap-1.5 data-[state=active]:bg-transparent", children: [_jsx(ActivityIcon, { className: "h-3.5 w-3.5" }), t('sidebar.activityFeed', { defaultValue: 'Activity' })] })] }), _jsxs(TabsContent, { value: "notifications", className: "m-0 max-h-80 overflow-auto", children: [notifications.length === 0 ? (_jsx("div", { className: "px-3 py-8 text-sm text-muted-foreground text-center", children: t('notifications.empty', { defaultValue: 'No notifications' }) })) : (_jsx("ul", { className: "divide-y", children: notifications.map((n) => (_jsx("li", { children: _jsx("button", { type: "button", onClick: () => handleNotificationClick(n), className: `w-full text-left px-3 py-2.5 hover:bg-accent transition-colors ${n.is_read ? '' : 'bg-accent/40'}`, children: _jsxs("div", { className: "flex items-start gap-2", children: [!n.is_read && (_jsx("span", { className: "mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-primary", "aria-hidden": true })), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "text-sm font-medium leading-tight truncate", children: n.title }), n.body && (_jsx("div", { className: "text-xs text-muted-foreground line-clamp-2 mt-0.5", children: n.body })), _jsx("div", { className: "text-[10px] text-muted-foreground mt-1", children: timeAgo(n.created_at) })] })] }) }) }, n.id))) })), _jsx("div", { className: "border-t px-3 py-2 text-center", children: _jsx("button", { type: "button", onClick: goToAllNotifications, className: "text-xs text-primary hover:underline", children: t('notifications.viewAll', { defaultValue: 'View all notifications' }) }) })] }), _jsx(TabsContent, { value: "approvals", className: "m-0 max-h-80 overflow-auto", children: _jsx("div", { className: "px-3 py-6 text-center", children: pendingApprovalsCount > 0 ? (_jsxs(_Fragment, { children: [_jsx(CheckSquare, { className: "mx-auto h-6 w-6 text-amber-500" }), _jsx("div", { className: "mt-2 text-sm font-medium", children: t('notifications.approvalsPending', {
104
+ return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "icon", className: `h-8 w-8 relative shrink-0 ${pulse ? 'motion-safe:animate-bounce' : ''}`, "aria-label": ariaLabel, title: t('sidebar.inbox', { defaultValue: 'Inbox' }), children: [_jsx(Bell, { className: "h-4 w-4" }), totalBadge > 0 && (_jsx("span", { className: "absolute -top-0.5 -right-0.5 h-4 min-w-[16px] rounded-full bg-red-500 text-[10px] leading-4 text-white text-center px-1 motion-safe:animate-in motion-safe:zoom-in-50 motion-safe:fade-in-0 motion-safe:duration-200", children: totalBadge > 9 ? '9+' : totalBadge }, totalBadge))] }) }), _jsxs(PopoverContent, { align: "end", sideOffset: 8, className: "w-96 p-0", children: [_jsxs("div", { className: "flex items-center justify-between px-3 pt-3 pb-1", children: [_jsx("div", { className: "text-sm font-semibold", children: t('sidebar.inbox', { defaultValue: 'Inbox' }) }), tab === 'notifications' && unreadCount > 0 && (_jsx("button", { type: "button", onClick: onMarkAllRead, className: "text-xs text-muted-foreground hover:text-foreground", children: t('notifications.markAllRead', { defaultValue: 'Mark all read' }) }))] }), _jsxs(Tabs, { value: tab, onValueChange: (v) => setTab(v), className: "w-full", children: [_jsxs(TabsList, { className: "w-full justify-start rounded-none border-b bg-transparent px-1 h-9", children: [_jsxs(TabsTrigger, { value: "notifications", className: "text-xs gap-1.5 data-[state=active]:bg-transparent", children: [_jsx(Bell, { className: "h-3.5 w-3.5" }), t('sidebar.notifications', { defaultValue: 'Notifications' }), unreadCount > 0 && (_jsx("span", { className: "ml-0.5 inline-flex h-4 min-w-[16px] items-center justify-center rounded-full bg-red-500 px-1 text-[10px] text-white motion-safe:animate-in motion-safe:zoom-in-75 motion-safe:duration-200", children: unreadCount > 9 ? '9+' : unreadCount }, `notif-${unreadCount}`))] }), _jsxs(TabsTrigger, { value: "approvals", className: "text-xs gap-1.5 data-[state=active]:bg-transparent", children: [_jsx(CheckSquare, { className: "h-3.5 w-3.5" }), t('sidebar.approvals', { defaultValue: 'Approvals' }), pendingApprovalsCount > 0 && (_jsx("span", { className: "ml-0.5 inline-flex h-4 min-w-[16px] items-center justify-center rounded-full bg-amber-500 px-1 text-[10px] text-white motion-safe:animate-in motion-safe:zoom-in-75 motion-safe:duration-200", children: pendingApprovalsCount > 9 ? '9+' : pendingApprovalsCount }, `appr-${pendingApprovalsCount}`))] }), _jsxs(TabsTrigger, { value: "activity", className: "text-xs gap-1.5 data-[state=active]:bg-transparent", children: [_jsx(ActivityIcon, { className: "h-3.5 w-3.5" }), t('sidebar.activityFeed', { defaultValue: 'Activity' })] })] }), _jsxs(TabsContent, { value: "notifications", className: "m-0 max-h-80 overflow-auto", children: [_jsxs("div", { className: "flex items-center gap-1 border-b px-2 py-1.5", children: [_jsxs("button", { type: "button", onClick: () => setNotifFilter('unread'), className: `text-xs px-2 py-1 rounded-md transition-colors ${notifFilter === 'unread'
105
+ ? 'bg-accent text-foreground font-medium'
106
+ : 'text-muted-foreground hover:text-foreground'}`, children: [t('notifications.filterUnread', { defaultValue: 'Unread' }), unreadCount > 0 && (_jsx("span", { className: "ml-1 text-[10px] opacity-70", children: unreadCount }))] }), _jsx("button", { type: "button", onClick: () => setNotifFilter('all'), className: `text-xs px-2 py-1 rounded-md transition-colors ${notifFilter === 'all'
107
+ ? 'bg-accent text-foreground font-medium'
108
+ : 'text-muted-foreground hover:text-foreground'}`, children: t('notifications.filterAll', { defaultValue: 'All' }) })] }), (() => {
109
+ const visible = notifFilter === 'unread'
110
+ ? notifications.filter((n) => !n.is_read)
111
+ : notifications;
112
+ if (visible.length === 0) {
113
+ return (_jsx("div", { className: "px-3 py-8 text-sm text-muted-foreground text-center motion-safe:animate-in motion-safe:fade-in-0 motion-safe:duration-300", children: notifFilter === 'unread'
114
+ ? t('notifications.emptyUnread', { defaultValue: "You're all caught up" })
115
+ : t('notifications.empty', { defaultValue: 'No notifications' }) }));
116
+ }
117
+ return (_jsx("ul", { className: "divide-y", children: visible.map((n, idx) => (_jsx("li", { className: "motion-safe:animate-in motion-safe:fade-in-0 motion-safe:slide-in-from-top-1 motion-safe:duration-200", style: { animationDelay: `${Math.min(idx, 6) * 20}ms` }, children: _jsx("button", { type: "button", onClick: () => handleNotificationClick(n), className: `w-full text-left px-3 py-2.5 hover:bg-accent transition-colors duration-150 ${n.is_read ? '' : 'bg-accent/40'}`, children: _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: `mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-primary transition-opacity duration-200 ${n.is_read ? 'opacity-0' : 'opacity-100'}`, "aria-hidden": true }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "text-sm font-medium leading-tight truncate", children: n.title }), n.body && (_jsx("div", { className: "text-xs text-muted-foreground line-clamp-2 mt-0.5", children: n.body })), _jsx("div", { className: "text-[10px] text-muted-foreground mt-1", children: timeAgo(n.created_at) })] })] }) }) }, n.id))) }));
118
+ })(), _jsx("div", { className: "border-t px-3 py-2 text-center", children: _jsx("button", { type: "button", onClick: goToAllNotifications, className: "text-xs text-primary hover:underline", children: t('notifications.viewAll', { defaultValue: 'View all notifications' }) }) })] }), _jsx(TabsContent, { value: "approvals", className: "m-0 max-h-80 overflow-auto", children: _jsx("div", { className: "px-3 py-6 text-center motion-safe:animate-in motion-safe:fade-in-0 motion-safe:duration-300", children: pendingApprovalsCount > 0 ? (_jsxs(_Fragment, { children: [_jsx(CheckSquare, { className: "mx-auto h-6 w-6 text-amber-500 motion-safe:animate-in motion-safe:zoom-in-50 motion-safe:duration-300" }), _jsx("div", { className: "mt-2 text-sm font-medium", children: t('notifications.approvalsPending', {
79
119
  defaultValue: '{{count}} pending approvals',
80
120
  count: pendingApprovalsCount,
81
121
  }) }), _jsx(Button, { size: "sm", className: "mt-3", onClick: goToApprovals, children: t('notifications.viewApprovals', { defaultValue: 'View approvals' }) })] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "text-sm text-muted-foreground", children: t('notifications.noPendingApprovals', {
82
122
  defaultValue: 'No pending approvals',
83
- }) }), _jsx("button", { type: "button", onClick: goToApprovals, className: "mt-2 text-xs text-primary hover:underline", children: t('notifications.openApprovalsInbox', { defaultValue: 'Open Approvals Inbox' }) })] })) }) }), _jsxs(TabsContent, { value: "activity", className: "m-0 max-h-80 overflow-auto", children: [activities.length === 0 ? (_jsx("div", { className: "px-3 py-8 text-sm text-muted-foreground text-center", children: t('layout.activityFeed.empty', { defaultValue: 'No recent activity' }) })) : (_jsx("ul", { className: "divide-y", children: activities.slice(0, 20).map((a) => (_jsxs("li", { className: "px-3 py-2.5", children: [_jsxs("div", { className: "text-sm leading-tight truncate", children: [_jsx("span", { className: "font-medium", children: a.user }), ' ', _jsx("span", { className: "text-muted-foreground", children: a.description })] }), _jsxs("div", { className: "text-[10px] text-muted-foreground mt-0.5", children: [timeAgo(a.timestamp), " \u00B7 ", a.objectName] })] }, a.id))) })), _jsx("div", { className: "border-t px-3 py-2 text-center", children: _jsx("button", { type: "button", onClick: goToAllActivity, className: "text-xs text-primary hover:underline", children: t('layout.activityFeed.viewAll', { defaultValue: 'View all activity' }) }) })] })] })] })] }));
123
+ }) }), _jsx("button", { type: "button", onClick: goToApprovals, className: "mt-2 text-xs text-primary hover:underline", children: t('notifications.openApprovalsInbox', { defaultValue: 'Open Approvals Inbox' }) })] })) }) }), _jsxs(TabsContent, { value: "activity", className: "m-0 max-h-80 overflow-auto", children: [activities.length === 0 ? (_jsx("div", { className: "px-3 py-8 text-sm text-muted-foreground text-center motion-safe:animate-in motion-safe:fade-in-0 motion-safe:duration-300", children: t('layout.activityFeed.empty', { defaultValue: 'No recent activity' }) })) : (_jsx("ul", { className: "divide-y", children: activities.slice(0, 20).map((a, idx) => (_jsxs("li", { className: "px-3 py-2.5 motion-safe:animate-in motion-safe:fade-in-0 motion-safe:slide-in-from-top-1 motion-safe:duration-200", style: { animationDelay: `${Math.min(idx, 6) * 20}ms` }, children: [_jsxs("div", { className: "text-sm leading-tight truncate", children: [_jsx("span", { className: "font-medium", children: a.user }), ' ', _jsx("span", { className: "text-muted-foreground", children: a.description })] }), _jsxs("div", { className: "text-[10px] text-muted-foreground mt-0.5", children: [timeAgo(a.timestamp), " \u00B7 ", a.objectName] })] }, a.id))) })), _jsx("div", { className: "border-t px-3 py-2 text-center", children: _jsx("button", { type: "button", onClick: goToAllActivity, className: "text-xs text-primary hover:underline", children: t('layout.activityFeed.viewAll', { defaultValue: 'View all activity' }) }) })] })] })] })] }));
84
124
  }
package/dist/types.d.ts CHANGED
@@ -24,49 +24,3 @@ export interface AppShellProps {
24
24
  /** Custom className */
25
25
  className?: string;
26
26
  }
27
- export interface ObjectRendererProps {
28
- /** Object API name */
29
- objectName: string;
30
- /** View ID (optional) */
31
- viewId?: string;
32
- /** Data source for CRUD operations */
33
- dataSource: DataSource;
34
- /** Callback when a record is clicked */
35
- onRecordClick?: (record: any) => void;
36
- /** Callback when edit is triggered */
37
- onEdit?: (record: any) => void;
38
- /** Object metadata (optional, will fetch if not provided) */
39
- objectDef?: any;
40
- /** Refresh key to force re-render */
41
- refreshKey?: number;
42
- }
43
- export interface DashboardRendererProps {
44
- /** Dashboard schema */
45
- schema: any;
46
- /** Data source for widgets */
47
- dataSource: DataSource;
48
- /** Dashboard name */
49
- dashboardName?: string;
50
- }
51
- export interface PageRendererProps {
52
- /** Page schema */
53
- schema: any;
54
- /** Page name */
55
- pageName?: string;
56
- }
57
- export interface FormRendererProps {
58
- /** Form schema */
59
- schema: any;
60
- /** Data source for form submission */
61
- dataSource: DataSource;
62
- /** Form mode */
63
- mode?: 'create' | 'edit';
64
- /** Record ID (for edit mode) */
65
- recordId?: string;
66
- /** Success callback */
67
- onSuccess?: (result: any) => void;
68
- /** Cancel callback */
69
- onCancel?: () => void;
70
- /** Object definition */
71
- objectDef?: any;
72
- }
@@ -10,7 +10,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
10
10
  * - ListView delegation for non-grid view types (kanban, calendar, chart, etc.)
11
11
  */
12
12
  import { useMemo, useState, useCallback, useEffect, useRef, lazy, Suspense } from 'react';
13
- import { useParams, useSearchParams, useNavigate } from 'react-router-dom';
13
+ import { useParams, useSearchParams, useNavigate, useLocation } from 'react-router-dom';
14
14
  const ObjectChart = lazy(() => import('@object-ui/plugin-charts').then((m) => ({ default: m.ObjectChart })));
15
15
  const ImportWizard = lazy(() => import('@object-ui/plugin-grid').then((m) => ({ default: m.ImportWizard })));
16
16
  import { ListView } from '@object-ui/plugin-list';
@@ -197,6 +197,7 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
197
197
  const navigate = useNavigate();
198
198
  const { appName, objectName, viewId } = useParams();
199
199
  const [searchParams, setSearchParams] = useSearchParams();
200
+ const location = useLocation();
200
201
  const { showDebug } = useMetadataInspector();
201
202
  const { t } = useObjectTranslation();
202
203
  const { objectLabel, objectDescription: objectDesc, viewLabel, viewEmptyState, actionLabel, actionConfirm, actionSuccess, fieldLabel, fieldOptionLabel } = useObjectLabel();
@@ -1239,13 +1240,19 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
1239
1240
  // forwarded from `navigation.view` (e.g. 'detail_form'). The view
1240
1241
  // variant is resolved by RecordDetailView from its own config, so
1241
1242
  // any non-`new_window` action lands on the record detail route.
1243
+ const originState = {
1244
+ from: {
1245
+ pathname: location.pathname + (location.search || ''),
1246
+ label: viewLabel(objectDef.name, activeView?.name ?? '', activeView?.label ?? '') || objectLabel(objectDef),
1247
+ },
1248
+ };
1242
1249
  if (viewId) {
1243
- navigate(`../../record/${encodeURIComponent(String(recordId))}`, { relative: 'path' });
1250
+ navigate(`../../record/${encodeURIComponent(String(recordId))}`, { relative: 'path', state: originState });
1244
1251
  }
1245
1252
  else {
1246
- navigate(`record/${encodeURIComponent(String(recordId))}`);
1253
+ navigate(`record/${encodeURIComponent(String(recordId))}`, { state: originState });
1247
1254
  }
1248
- }, [navigate, viewId]);
1255
+ }, [navigate, viewId, location.pathname, location.search, objectDef, activeView?.name, activeView?.label, viewLabel, objectLabel]);
1249
1256
  const navOverlay = useNavigationOverlay({
1250
1257
  navigation: detailNavigation,
1251
1258
  objectName: objectDef.name,
@@ -1566,15 +1573,21 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
1566
1573
  onEdit?.({ id: recordId });
1567
1574
  }
1568
1575
  else if (mode === 'view') {
1576
+ const originState = {
1577
+ from: {
1578
+ pathname: location.pathname + (location.search || ''),
1579
+ label: viewLabel(objectDef.name, activeView?.name ?? '', activeView?.label ?? '') || objectLabel(objectDef),
1580
+ },
1581
+ };
1569
1582
  if (viewId) {
1570
- navigate(`../../record/${encodeURIComponent(String(recordId))}`, { relative: 'path' });
1583
+ navigate(`../../record/${encodeURIComponent(String(recordId))}`, { relative: 'path', state: originState });
1571
1584
  }
1572
1585
  else {
1573
- navigate(`record/${encodeURIComponent(String(recordId))}`);
1586
+ navigate(`record/${encodeURIComponent(String(recordId))}`, { state: originState });
1574
1587
  }
1575
1588
  }
1576
1589
  },
1577
- }), [objectDef.name, onEdit, activeView?.showSearch, activeView?.showFilters, activeView?.showSort, navigate, viewId, isAdmin]);
1590
+ }), [objectDef, onEdit, activeView?.showSearch, activeView?.showFilters, activeView?.showSort, activeView?.name, activeView?.label, navigate, viewId, isAdmin, location.pathname, location.search, viewLabel, objectLabel]);
1578
1591
  return (_jsxs(ActionProvider, { context: {
1579
1592
  objectName: objectDef.name,
1580
1593
  user: currentUser,