@object-ui/app-shell 5.0.1 → 5.0.2

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 CHANGED
@@ -1,5 +1,46 @@
1
1
  # @object-ui/app-shell — Changelog
2
2
 
3
+ ## 5.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - cab6a93: **plugin-grid:** column summary footer now formats values using the
8
+ column's type metadata. Currency columns render `Sum: $1,760,000.00`
9
+ instead of bare `Sum: 1,760,000`; percent columns honor `0–1` vs
10
+ `0–100` value ranges; avg uses two fraction digits. `useColumnSummary`
11
+ accepts an optional `fieldMetadata` map (typically `objectSchema.fields`)
12
+ so per-field `type`, `currency`, `defaultCurrency`, `precision` are
13
+ respected.
14
+
15
+ **plugin-gantt:** added safe-fallback `useGanttTranslation` hook. All
16
+ hardcoded toolbar `aria-label`s and the `Task Name` / `Start` / `End` /
17
+ `Today` column-header strings now flow through `t('gantt.*')`. A new
18
+ `gantt.*` section is exported from the en/zh/ja/ko/de/fr/es/pt/ru/ar
19
+ locales.
20
+
21
+ **app-shell:** `ReportView` no longer hardcodes the `Edit` button label
22
+ or the `Loading report…` fallback — they now use `common.edit` and
23
+ `common.loading`.
24
+
25
+ **i18n:** added top-level `gantt` section (with English fallbacks in
26
+ non-en/zh locales) and the `common.addToFavorites` /
27
+ `common.removeFromFavorites` keys across all ten built-in locales so
28
+ the `builtInLocales` parity tests pass.
29
+
30
+ - Updated dependencies [cab6a93]
31
+ - @object-ui/i18n@5.0.2
32
+ - @object-ui/components@5.0.2
33
+ - @object-ui/fields@5.0.2
34
+ - @object-ui/react@5.0.2
35
+ - @object-ui/layout@5.0.2
36
+ - @object-ui/types@5.0.2
37
+ - @object-ui/core@5.0.2
38
+ - @object-ui/data-objectstack@5.0.2
39
+ - @object-ui/auth@5.0.2
40
+ - @object-ui/permissions@5.0.2
41
+ - @object-ui/collaboration@5.0.2
42
+ - @object-ui/providers@5.0.2
43
+
3
44
  ## 5.0.1
4
45
 
5
46
  ### Patch Changes
@@ -9,8 +9,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
9
9
  import { useNavigate } from 'react-router-dom';
10
10
  import { useObjectTranslation } from '@object-ui/i18n';
11
11
  import { Card, CardContent, cn } from '@object-ui/components';
12
- import { Clock, ArrowUpRight } from 'lucide-react';
13
- import { getIcon } from '../../utils/getIcon';
12
+ import { Clock, ArrowUpRight, Database, FileText, LayoutDashboard, File } from 'lucide-react';
14
13
  import { capitalizeFirst } from '../../utils';
15
14
  const TYPE_TONES = {
16
15
  object: 'bg-blue-500/10 text-blue-600 dark:text-blue-400 ring-blue-500/20',
@@ -18,13 +17,21 @@ const TYPE_TONES = {
18
17
  page: 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 ring-emerald-500/20',
19
18
  record: 'bg-amber-500/10 text-amber-600 dark:text-amber-400 ring-amber-500/20',
20
19
  };
20
+ // Per-type icon so the four kinds are visually distinguishable — see
21
+ // StarredApps.tsx for the rationale.
22
+ const TYPE_ICONS = {
23
+ object: Database,
24
+ record: FileText,
25
+ dashboard: LayoutDashboard,
26
+ page: File,
27
+ };
21
28
  export function RecentApps({ items }) {
22
29
  const navigate = useNavigate();
23
30
  const { t } = useObjectTranslation();
24
31
  if (items.length === 0)
25
32
  return null;
26
33
  return (_jsxs("section", { children: [_jsxs("div", { className: "flex items-center gap-2 mb-5", children: [_jsx("span", { className: "inline-flex h-8 w-8 items-center justify-center rounded-lg bg-sky-500/10 ring-1 ring-sky-500/20 text-sky-600 dark:text-sky-400", children: _jsx(Clock, { className: "h-4 w-4" }) }), _jsx("h2", { className: "text-2xl font-semibold tracking-tight", children: t('home.recentApps.title', { defaultValue: 'Recently Accessed' }) })] }), _jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3", children: items.map((item) => {
27
- const Icon = getIcon(item.type);
34
+ const Icon = TYPE_ICONS[item.type] || Database;
28
35
  const typeLabel = t(`home.recentApps.itemType.${item.type}`, {
29
36
  defaultValue: capitalizeFirst(item.type),
30
37
  });
@@ -9,8 +9,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
9
9
  import { useNavigate } from 'react-router-dom';
10
10
  import { useObjectTranslation } from '@object-ui/i18n';
11
11
  import { Card, CardContent, cn } from '@object-ui/components';
12
- import { Star, ArrowUpRight } from 'lucide-react';
13
- import { getIcon } from '../../utils/getIcon';
12
+ import { Star, ArrowUpRight, Database, FileText, LayoutDashboard, File } from 'lucide-react';
14
13
  import { capitalizeFirst } from '../../utils';
15
14
  const TYPE_TONES = {
16
15
  object: 'bg-blue-500/10 text-blue-600 dark:text-blue-400 ring-blue-500/20',
@@ -18,19 +17,35 @@ const TYPE_TONES = {
18
17
  page: 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 ring-emerald-500/20',
19
18
  record: 'bg-amber-500/10 text-amber-600 dark:text-amber-400 ring-amber-500/20',
20
19
  };
20
+ // Per-type icon so the four kinds (object / record / dashboard / page) are
21
+ // visually distinguishable at a glance. `getIcon` falls back to the same
22
+ // Database glyph for every unknown name which made all cards look identical.
23
+ const TYPE_ICONS = {
24
+ object: Database,
25
+ record: FileText,
26
+ dashboard: LayoutDashboard,
27
+ page: File,
28
+ };
21
29
  export function StarredApps({ items }) {
22
30
  const navigate = useNavigate();
23
31
  const { t } = useObjectTranslation();
24
32
  if (items.length === 0)
25
33
  return null;
26
34
  return (_jsxs("section", { children: [_jsxs("div", { className: "flex items-center gap-2 mb-5", children: [_jsx("span", { className: "inline-flex h-8 w-8 items-center justify-center rounded-lg bg-amber-500/10 ring-1 ring-amber-500/20 text-amber-600 dark:text-amber-400", children: _jsx(Star, { className: "h-4 w-4 fill-current" }) }), _jsx("h2", { className: "text-2xl font-semibold tracking-tight", children: t('home.starredApps.title', { defaultValue: 'Starred' }) })] }), _jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3", children: items.map((item) => {
27
- const Icon = getIcon(item.type);
35
+ const Icon = TYPE_ICONS[item.type] || Database;
28
36
  const tone = TYPE_TONES[item.type] || TYPE_TONES.object;
37
+ // Reuse the recentApps.itemType.* keys so Starred and Recently
38
+ // Accessed surface the same localized labels (e.g. "记录" vs
39
+ // "Record"). Falls back to the capitalized english type so
40
+ // unknown types still render readably.
41
+ const typeLabel = t(`home.recentApps.itemType.${item.type}`, {
42
+ defaultValue: capitalizeFirst(item.type),
43
+ });
29
44
  return (_jsx(Card, { className: "group cursor-pointer border border-border/70 bg-card/80 backdrop-blur-sm transition-[transform,box-shadow,border-color] duration-200 hover:-translate-y-0.5 hover:shadow-md hover:border-foreground/20 motion-reduce:transition-none motion-reduce:hover:transform-none", onClick: () => navigate(item.href), "data-testid": `starred-item-${item.id}`, role: "link", tabIndex: 0, onKeyDown: (e) => {
30
45
  if (e.key === 'Enter' || e.key === ' ') {
31
46
  e.preventDefault();
32
47
  navigate(item.href);
33
48
  }
34
- }, children: _jsx(CardContent, { className: "p-3.5", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: cn('inline-flex h-10 w-10 items-center justify-center rounded-lg ring-1 shrink-0', tone), children: _jsx(Icon, { className: "h-5 w-5" }) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("h3", { className: "font-medium text-sm truncate", children: item.label }), _jsx("p", { className: "text-xs text-muted-foreground", children: capitalizeFirst(item.type) })] }), _jsx(ArrowUpRight, { className: "h-4 w-4 text-muted-foreground opacity-0 -translate-x-1 transition-[opacity,transform] duration-200 group-hover:opacity-100 group-hover:translate-x-0" })] }) }) }, item.id));
49
+ }, children: _jsx(CardContent, { className: "p-3.5", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: cn('inline-flex h-10 w-10 items-center justify-center rounded-lg ring-1 shrink-0', tone), children: _jsx(Icon, { className: "h-5 w-5" }) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("h3", { className: "font-medium text-sm truncate", children: item.label }), _jsx("p", { className: "text-xs text-muted-foreground", children: typeLabel })] }), _jsx(ArrowUpRight, { className: "h-4 w-4 text-muted-foreground opacity-0 -translate-x-1 transition-[opacity,transform] duration-200 group-hover:opacity-100 group-hover:translate-x-0" })] }) }) }, item.id));
35
50
  }) })] }));
36
51
  }
@@ -33,6 +33,13 @@ interface FavoritesContextValue {
33
33
  toggleFavorite: (item: Omit<FavoriteItem, 'favoritedAt'>) => void;
34
34
  isFavorite: (id: string) => boolean;
35
35
  clearFavorites: () => void;
36
+ /**
37
+ * Self-heal a stored label for an existing favorite without re-ordering
38
+ * or resetting `favoritedAt`. Used by record pages once the human-readable
39
+ * title resolves so stale "raw id" labels (e.g. saved before the title
40
+ * loaded) get rewritten transparently on the next visit.
41
+ */
42
+ refreshLabel: (id: string, label: string) => void;
36
43
  }
37
44
  interface FavoritesProviderProps {
38
45
  children: ReactNode;
@@ -138,6 +138,24 @@ export function FavoritesProvider({ children }) {
138
138
  setFavorites([]);
139
139
  commit([]);
140
140
  }, [commit]);
141
+ const refreshLabel = useCallback((id, label) => {
142
+ if (!id || !label)
143
+ return;
144
+ setFavorites(prev => {
145
+ let changed = false;
146
+ const updated = prev.map(f => {
147
+ if (f.id === id && f.label !== label) {
148
+ changed = true;
149
+ return { ...f, label };
150
+ }
151
+ return f;
152
+ });
153
+ if (!changed)
154
+ return prev;
155
+ commit(updated);
156
+ return updated;
157
+ });
158
+ }, [commit]);
141
159
  const value = useMemo(() => ({
142
160
  favorites,
143
161
  addFavorite,
@@ -145,7 +163,8 @@ export function FavoritesProvider({ children }) {
145
163
  toggleFavorite,
146
164
  isFavorite: (id) => favorites.some(f => f.id === id),
147
165
  clearFavorites,
148
- }), [favorites, addFavorite, removeFavorite, toggleFavorite, clearFavorites]);
166
+ refreshLabel,
167
+ }), [favorites, addFavorite, removeFavorite, toggleFavorite, clearFavorites, refreshLabel]);
149
168
  return (_jsx(FavoritesContext.Provider, { value: value, children: children }));
150
169
  }
151
170
  // ---------------------------------------------------------------------------
@@ -169,6 +188,7 @@ export function useFavorites() {
169
188
  toggleFavorite: () => { },
170
189
  isFavorite: () => false,
171
190
  clearFavorites: () => { },
191
+ refreshLabel: () => { },
172
192
  };
173
193
  }
174
194
  return ctx;
@@ -94,7 +94,7 @@ export function AppSidebar({ activeAppName, onAppChange }) {
94
94
  const { user, signOut, isAuthEnabled } = useAuth();
95
95
  const navigate = useNavigate();
96
96
  const { t } = useObjectTranslation();
97
- const { objectLabel: resolveNavObjectLabel } = useObjectLabel();
97
+ const { objectLabel: resolveNavObjectLabel, viewLabel: resolveNavViewLabel } = useObjectLabel();
98
98
  // Swipe-from-left-edge gesture to open sidebar on mobile
99
99
  React.useEffect(() => {
100
100
  const EDGE_THRESHOLD = 30;
@@ -200,7 +200,7 @@ export function AppSidebar({ activeAppName, onAppChange }) {
200
200
  const AreaIcon = getIcon(area.icon);
201
201
  const isActiveArea = area.id === activeAreaId;
202
202
  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));
203
- }) }) })] })), _jsx(SidebarGroup, { className: "py-0", children: _jsxs(SidebarGroupContent, { className: "relative", children: [_jsx(Search, { className: "pointer-events-none absolute left-2 top-1/2 size-4 -translate-y-1/2 select-none opacity-70" }), _jsx(SidebarInput, { placeholder: "Search navigation...", value: navSearchQuery, onChange: (e) => setNavSearchQuery(e.target.value), className: "pl-8" })] }) }), _jsx(NavigationRenderer, { items: processedNavigation, basePath: basePath, evaluateVisibility: evalVis, checkPermission: checkPerm, checkCapability: checkCap, searchQuery: navSearchQuery, enablePinning: true, onPinToggle: togglePin, enableReorder: true, onReorder: handleReorder, resolveObjectLabel: (objectName, fallback) => resolveNavObjectLabel({ name: objectName, label: fallback }), t: t }), recentItems.length > 0 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5 cursor-pointer select-none", onClick: () => setRecentExpanded(prev => !prev), children: [_jsx(ChevronRight, { className: `h-3 w-3 transition-transform duration-150 ${recentExpanded ? 'rotate-90' : ''}` }), _jsx(Clock, { className: "h-3.5 w-3.5" }), "Recent"] }), recentExpanded && (_jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: recentItems.slice(0, 5).map(item => (_jsx(SidebarMenuItem, { children: _jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : '📄' }), _jsx("span", { className: "truncate", children: item.label })] }) }) }, item.id))) }) }))] })), favorites.length > 0 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Star, { className: "h-3.5 w-3.5" }), "Favorites"] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: favorites.slice(0, 8).map(item => (_jsxs(SidebarMenuItem, { children: [_jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : item.type === 'page' ? '📄' : '📋' }), _jsx("span", { className: "truncate", children: item.label })] }) }), _jsx(SidebarMenuAction, { showOnHover: true, onClick: (e) => { e.stopPropagation(); removeFavorite(item.id); }, "aria-label": `Remove ${item.label} from favorites`, children: _jsx(StarOff, { className: "h-3 w-3" }) })] }, item.id))) }) })] }))] })) : (_jsxs(SidebarGroup, { "data-testid": "system-fallback-nav", children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Settings, { className: "h-3.5 w-3.5" }), "System"] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: systemFallbackNavigation.map((item) => {
203
+ }) }) })] })), _jsx(SidebarGroup, { className: "py-0", children: _jsxs(SidebarGroupContent, { className: "relative", children: [_jsx(Search, { className: "pointer-events-none absolute left-2 top-1/2 size-4 -translate-y-1/2 select-none opacity-70" }), _jsx(SidebarInput, { placeholder: "Search navigation...", value: navSearchQuery, onChange: (e) => setNavSearchQuery(e.target.value), className: "pl-8" })] }) }), _jsx(NavigationRenderer, { items: processedNavigation, basePath: basePath, evaluateVisibility: evalVis, checkPermission: checkPerm, checkCapability: checkCap, searchQuery: navSearchQuery, enablePinning: true, onPinToggle: togglePin, enableReorder: true, onReorder: handleReorder, resolveObjectLabel: (objectName, fallback) => resolveNavObjectLabel({ name: objectName, label: fallback }), resolveViewLabel: (objectName, viewName, fallback) => resolveNavViewLabel(objectName, viewName, fallback), t: t }), recentItems.length > 0 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5 cursor-pointer select-none", onClick: () => setRecentExpanded(prev => !prev), children: [_jsx(ChevronRight, { className: `h-3 w-3 transition-transform duration-150 ${recentExpanded ? 'rotate-90' : ''}` }), _jsx(Clock, { className: "h-3.5 w-3.5" }), "Recent"] }), recentExpanded && (_jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: recentItems.slice(0, 5).map(item => (_jsx(SidebarMenuItem, { children: _jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : '📄' }), _jsx("span", { className: "truncate", children: item.label })] }) }) }, item.id))) }) }))] })), favorites.length > 0 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Star, { className: "h-3.5 w-3.5" }), "Favorites"] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: favorites.slice(0, 8).map(item => (_jsxs(SidebarMenuItem, { children: [_jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : item.type === 'page' ? '📄' : '📋' }), _jsx("span", { className: "truncate", children: item.label })] }) }), _jsx(SidebarMenuAction, { showOnHover: true, onClick: (e) => { e.stopPropagation(); removeFavorite(item.id); }, "aria-label": `Remove ${item.label} from favorites`, children: _jsx(StarOff, { className: "h-3 w-3" }) })] }, item.id))) }) })] }))] })) : (_jsxs(SidebarGroup, { "data-testid": "system-fallback-nav", children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Settings, { className: "h-3.5 w-3.5" }), "System"] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: systemFallbackNavigation.map((item) => {
204
204
  const NavIcon = getIcon(item.icon);
205
205
  return (_jsx(SidebarMenuItem, { children: _jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.url || '/system', children: [_jsx(NavIcon, { className: "h-4 w-4" }), _jsx("span", { children: item.label })] }) }) }, item.id));
206
206
  }) }) })] })) }), _jsx(SidebarFooter, { children: _jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: _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: [_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(ChevronsUpDown, { className: "ml-auto size-4" })] }) }), _jsxs(DropdownMenuContent, { className: "w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg", side: isMobile ? "bottom" : "right", align: "end", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "p-0 font-normal", children: _jsxs("div", { className: "flex items-center gap-2 px-1 py-1.5 text-left text-sm", 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, {}), _jsx(DropdownMenuGroup, { children: _jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/setup'), children: [_jsx(Settings, { className: "mr-2 h-4 w-4" }), t('user.settings', { defaultValue: 'Settings' })] }) }), isAuthEnabled && (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "text-destructive focus:text-destructive", onClick: () => signOut(), children: [_jsx(LogOut, { className: "mr-2 h-4 w-4" }), t('user.logout', { defaultValue: 'Log out' })] })] }))] })] }) }) }) })] }), isMobile && (_jsx("div", { className: "fixed bottom-0 left-0 right-0 z-50 flex items-center justify-around border-t bg-background/95 backdrop-blur-sm px-2 py-1 sm:hidden safe-area-bottom", children: (() => {
@@ -88,7 +88,7 @@ export function UnifiedSidebar({ activeAppName }) {
88
88
  const { isMobile, setOpenMobile } = useSidebar();
89
89
  const location = useLocation();
90
90
  const { t } = useObjectTranslation();
91
- const { objectLabel: resolveNavObjectLabel, dashboardLabel: resolveNavDashboardLabel, navGroupLabel: resolveNavGroupLabel } = useObjectLabel();
91
+ const { objectLabel: resolveNavObjectLabel, dashboardLabel: resolveNavDashboardLabel, navGroupLabel: resolveNavGroupLabel, viewLabel: resolveNavViewLabel } = useObjectLabel();
92
92
  const { context, currentAppName } = useNavigationContext();
93
93
  // Swipe-from-left-edge gesture to open sidebar on mobile
94
94
  React.useEffect(() => {
@@ -175,7 +175,7 @@ export function UnifiedSidebar({ activeAppName }) {
175
175
  const AreaIcon = getIcon(area.icon);
176
176
  const isActiveArea = area.id === activeAreaId;
177
177
  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));
178
- }) }) })] })), _jsx(NavigationRenderer, { items: processedNavigation, basePath: basePath, evaluateVisibility: evalVis, checkPermission: checkPerm, checkCapability: checkCap, enablePinning: !isMobile, onPinToggle: togglePin, enableReorder: !isMobile, onReorder: handleReorder, resolveObjectLabel: (objectName, fallback) => resolveNavObjectLabel({ name: objectName, label: fallback }), resolveDashboardLabel: (dashboardName, fallback) => resolveNavDashboardLabel({ name: dashboardName, label: fallback }), resolveGroupLabel: activeApp ? (groupId, fallback) => resolveNavGroupLabel(activeApp.name, groupId, fallback) : undefined, t: t }), recentItems.length > 0 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5 cursor-pointer select-none", onClick: () => setRecentExpanded(prev => !prev), children: [_jsx(ChevronRight, { className: `h-3 w-3 transition-transform duration-150 ${recentExpanded ? 'rotate-90' : ''}` }), _jsx(Clock, { className: "h-3.5 w-3.5" }), t('sidebar.recent', { defaultValue: 'Recent' })] }), recentExpanded && (_jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: recentItems.slice(0, 5).map(item => (_jsx(SidebarMenuItem, { children: _jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : '📄' }), _jsx("span", { className: "truncate", children: item.label })] }) }) }, item.id))) }) }))] })), favorites.length > 0 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Star, { className: "h-3.5 w-3.5" }), t('sidebar.favorites', { defaultValue: 'Favorites' })] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: favorites.slice(0, 8).map(item => (_jsxs(SidebarMenuItem, { children: [_jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : item.type === 'page' ? '📄' : '📋' }), _jsx("span", { className: "truncate", children: item.label })] }) }), _jsx(SidebarMenuAction, { showOnHover: true, onClick: (e) => { e.stopPropagation(); removeFavorite(item.id); }, "aria-label": t('sidebar.removeFromFavorites', { defaultValue: 'Remove {{name}} from favorites', name: item.label }), children: _jsx(StarOff, { className: "h-3 w-3" }) })] }, item.id))) }) })] }))] })) : (_jsxs(_Fragment, { children: [_jsx(SidebarGroup, { children: _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: homeNavigation.map((item) => {
178
+ }) }) })] })), _jsx(NavigationRenderer, { items: processedNavigation, basePath: basePath, evaluateVisibility: evalVis, checkPermission: checkPerm, checkCapability: checkCap, enablePinning: !isMobile, onPinToggle: togglePin, enableReorder: !isMobile, onReorder: handleReorder, resolveObjectLabel: (objectName, fallback) => resolveNavObjectLabel({ name: objectName, label: fallback }), resolveDashboardLabel: (dashboardName, fallback) => resolveNavDashboardLabel({ name: dashboardName, label: fallback }), resolveGroupLabel: activeApp ? (groupId, fallback) => resolveNavGroupLabel(activeApp.name, groupId, fallback) : undefined, resolveViewLabel: (objectName, viewName, fallback) => resolveNavViewLabel(objectName, viewName, fallback), t: t }), recentItems.length > 0 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5 cursor-pointer select-none", onClick: () => setRecentExpanded(prev => !prev), children: [_jsx(ChevronRight, { className: `h-3 w-3 transition-transform duration-150 ${recentExpanded ? 'rotate-90' : ''}` }), _jsx(Clock, { className: "h-3.5 w-3.5" }), t('sidebar.recent', { defaultValue: 'Recent' })] }), recentExpanded && (_jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: recentItems.slice(0, 5).map(item => (_jsx(SidebarMenuItem, { children: _jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : '📄' }), _jsx("span", { className: "truncate", children: item.label })] }) }) }, item.id))) }) }))] })), favorites.length > 0 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Star, { className: "h-3.5 w-3.5" }), t('sidebar.favorites', { defaultValue: 'Favorites' })] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: favorites.slice(0, 8).map(item => (_jsxs(SidebarMenuItem, { children: [_jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : item.type === 'page' ? '📄' : '📋' }), _jsx("span", { className: "truncate", children: item.label })] }) }), _jsx(SidebarMenuAction, { showOnHover: true, onClick: (e) => { e.stopPropagation(); removeFavorite(item.id); }, "aria-label": t('sidebar.removeFromFavorites', { defaultValue: 'Remove {{name}} from favorites', name: item.label }), children: _jsx(StarOff, { className: "h-3 w-3" }) })] }, item.id))) }) })] }))] })) : (_jsxs(_Fragment, { children: [_jsx(SidebarGroup, { children: _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: homeNavigation.map((item) => {
179
179
  const NavIcon = getIcon(item.icon);
180
180
  const isActive = location.pathname === item.url;
181
181
  return (_jsx(SidebarMenuItem, { children: _jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, isActive: isActive, children: _jsxs(Link, { to: item.url || '/home', children: [_jsx(NavIcon, { className: "h-4 w-4" }), _jsx("span", { children: item.label })] }) }) }, item.id));
@@ -199,7 +199,7 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
199
199
  const [searchParams, setSearchParams] = useSearchParams();
200
200
  const { showDebug } = useMetadataInspector();
201
201
  const { t } = useObjectTranslation();
202
- const { objectLabel, objectDescription: objectDesc, viewLabel, actionLabel, actionConfirm, actionSuccess, fieldLabel, fieldOptionLabel } = useObjectLabel();
202
+ const { objectLabel, objectDescription: objectDesc, viewLabel, viewEmptyState, actionLabel, actionConfirm, actionSuccess, fieldLabel, fieldOptionLabel } = useObjectLabel();
203
203
  const { isFavorite, toggleFavorite } = useFavorites();
204
204
  // Inline view config panel state (Airtable-style right sidebar)
205
205
  const [showViewConfigPanel, setShowViewConfigPanel] = useState(false);
@@ -1433,9 +1433,9 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
1433
1433
  showRecordCount: viewDef.showRecordCount ?? listSchema.showRecordCount,
1434
1434
  allowPrinting: viewDef.allowPrinting ?? listSchema.allowPrinting,
1435
1435
  virtualScroll: viewDef.virtualScroll ?? listSchema.virtualScroll,
1436
- emptyState: viewDef.emptyState
1436
+ emptyState: viewEmptyState(objectDef.name, viewDef.name || viewDef.id || '', viewDef.emptyState
1437
1437
  ?? listSchema.emptyState
1438
- ?? resolveManagedByEmptyState(objectDef?.managedBy),
1438
+ ?? resolveManagedByEmptyState(objectDef?.managedBy)),
1439
1439
  aria: viewDef.aria ?? listSchema.aria,
1440
1440
  tabs: listSchema.tabs,
1441
1441
  // Propagate filter/sort as default filters/sort for data flow
@@ -1668,7 +1668,7 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
1668
1668
  { title: 'View Configuration', data: activeView },
1669
1669
  { title: 'Object Definition', data: objectDef },
1670
1670
  ] }), _jsx("div", { "data-testid": "view-config-panel-wrapper", className: `transition-[max-width,opacity] duration-300 ease-in-out overflow-hidden ${showViewConfigPanel && isAdmin ? 'max-w-[280px] opacity-100' : 'max-w-0 opacity-0'}`, children: _jsx(ViewConfigPanel, { open: showViewConfigPanel && isAdmin, onClose: () => { setShowViewConfigPanel(false); setViewConfigPanelMode('edit'); }, mode: viewConfigPanelMode, activeView: activeView, objectDef: objectDef, recordCount: recordCount, onSave: handleViewConfigSave, onViewUpdate: handleViewUpdate, onCreate: handleViewCreate }) }), _jsx(CreateViewDialog, { open: showCreateViewDialog && isAdmin, onOpenChange: setShowCreateViewDialog, existingLabels: views.map((v) => v.label).filter(Boolean), objectDef: objectDef, onCreate: (cfg) => handleViewCreate(cfg) })] }), navOverlay.mode !== 'split' && (_jsx(NavigationOverlay, { ...navOverlay, setIsOpen: (open) => { if (!open)
1671
- handleDrawerClose(); }, title: objectDef.label, onExpand: handleExpandDrawer, expandLabel: t('console.objectView.expandToPage', { defaultValue: 'Open as full page' }), storageKey: `drawer-width:${objectDef.name}`, children: (record) => {
1671
+ handleDrawerClose(); }, title: objectLabel(objectDef), onExpand: handleExpandDrawer, expandLabel: t('console.objectView.expandToPage', { defaultValue: 'Open as full page' }), storageKey: `drawer-width:${objectDef.name}`, children: (record) => {
1672
1672
  const recordId = (record.id || record._id);
1673
1673
  return (_jsx(RecordDetailView, { dataSource: dataSource, objects: objects, onEdit: onEdit, objectNameOverride: objectDef.name, recordIdOverride: recordId, embedded: true }));
1674
1674
  } }))] }), _jsx(ActionConfirmDialog, { state: confirmState, onOpenChange: (open) => {
@@ -9,13 +9,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
9
9
  import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
10
10
  import { useParams, useNavigate } from 'react-router-dom';
11
11
  import { DetailView, RecordChatterPanel, buildDefaultPageSchema } from '@object-ui/plugin-detail';
12
- import { Empty, EmptyTitle, EmptyDescription, Button } from '@object-ui/components';
12
+ import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
13
13
  import { PresenceAvatars } from '@object-ui/collaboration';
14
14
  import { useAuth, createAuthenticatedFetch } from '@object-ui/auth';
15
15
  import { ActionProvider, useObjectTranslation, useObjectLabel, usePageAssignment, RecordContextProvider, SchemaRenderer, DiscussionContextProvider, HighlightFieldsProvider } from '@object-ui/react';
16
16
  import { buildExpandFields } from '@object-ui/core';
17
17
  import { toast } from 'sonner';
18
- import { Database, Users, Star, StarOff } from 'lucide-react';
18
+ import { Database, Users } from 'lucide-react';
19
19
  import { MetadataPanel, useMetadataInspector } from './MetadataInspector';
20
20
  import { SkeletonDetail } from '../skeletons';
21
21
  import { ManagedByBadge } from '../components/ManagedByBadge';
@@ -28,6 +28,7 @@ import { useRecordBreadcrumbTitle } from '../context/NavigationContext';
28
28
  import { useRecordApprovals } from '../hooks/useRecordApprovals';
29
29
  import { getRecordDisplayName } from '../utils';
30
30
  import { useFavorites } from '../hooks/useFavorites';
31
+ import { useRecentItems } from '../hooks/useRecentItems';
31
32
  const FALLBACK_USER = { id: 'current-user', name: 'Demo User' };
32
33
  /**
33
34
  * Audit field names auto-injected by the framework's `applySystemFields`.
@@ -59,7 +60,8 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
59
60
  const navigate = useNavigate();
60
61
  const { t } = useObjectTranslation();
61
62
  const { objectLabel, viewLabel: _vLabel, sectionLabel, actionLabel, actionConfirm, actionSuccess, fieldLabel, fieldOptionLabel } = useObjectLabel();
62
- const { isFavorite, toggleFavorite } = useFavorites();
63
+ const { isFavorite, toggleFavorite, refreshLabel: refreshFavoriteLabel } = useFavorites();
64
+ const { addRecentItem } = useRecentItems();
63
65
  const [isLoading, setIsLoading] = useState(true);
64
66
  const [feedItems, setFeedItems] = useState([]);
65
67
  const [recordViewers, setRecordViewers] = useState([]);
@@ -77,6 +79,21 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
77
79
  // Navigation code passes `record.id || record._id` directly into the URL
78
80
  // without adding any prefix, so no stripping is needed.
79
81
  const pureRecordId = recordId;
82
+ const favoriteRecord = useMemo(() => {
83
+ if (!objectName || !pureRecordId)
84
+ return null;
85
+ return {
86
+ id: `record:${objectName}:${pureRecordId}`,
87
+ label: recordTitle || pureRecordId || '',
88
+ href: `/apps/${appName}/${objectName}/record/${pureRecordId}`,
89
+ type: 'record',
90
+ };
91
+ }, [appName, objectName, pureRecordId, recordTitle]);
92
+ const isRecordFavorite = favoriteRecord ? isFavorite(favoriteRecord.id) : false;
93
+ const handleToggleRecordFavorite = useCallback(() => {
94
+ if (favoriteRecord)
95
+ toggleFavorite(favoriteRecord);
96
+ }, [favoriteRecord, toggleFavorite]);
80
97
  // ─── Page Assignment (Salesforce Lightning-style record Pages) ──────
81
98
  // If a PageSchema(pageType='record') is authored for this object, render
82
99
  // it via SchemaRenderer (which dispatches to the registered 'record'
@@ -134,22 +151,63 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
134
151
  // page subtitle interpolation and record:* renderers depend on this.
135
152
  const expandFields = buildExpandFields(objectDef?.fields);
136
153
  const params = expandFields.length > 0 ? { $expand: expandFields } : undefined;
137
- const findOnePromise = params
138
- ? dataSource.findOne(objectName, pureRecordId, params)
139
- : dataSource.findOne(objectName, pureRecordId);
140
- findOnePromise
141
- .then((rec) => {
142
- if (!cancelled)
143
- setPageRecord(rec);
144
- })
145
- .catch(() => {
146
- if (!cancelled)
147
- setPageRecord(null);
148
- });
154
+ const loadRecord = () => {
155
+ const findOnePromise = params
156
+ ? dataSource.findOne(objectName, pureRecordId, params)
157
+ : dataSource.findOne(objectName, pureRecordId);
158
+ findOnePromise
159
+ .then((rec) => {
160
+ if (!cancelled)
161
+ setPageRecord(rec);
162
+ })
163
+ .catch(() => {
164
+ if (!cancelled)
165
+ setPageRecord(null);
166
+ });
167
+ };
168
+ loadRecord();
169
+ // Re-sync when any descendant signals the record changed (e.g.
170
+ // DetailView recalling a pending approval). Without this listener,
171
+ // the cached `pageRecord` would stay stale and propagate `pending`
172
+ // back into nested DetailViews via context.
173
+ const onChanged = (ev) => {
174
+ const detail = ev.detail || {};
175
+ if (detail.objectName !== objectName || String(detail.recordId) !== String(pureRecordId))
176
+ return;
177
+ loadRecord();
178
+ };
179
+ window.addEventListener('objectui:record-changed', onChanged);
149
180
  return () => {
150
181
  cancelled = true;
182
+ window.removeEventListener('objectui:record-changed', onChanged);
151
183
  };
152
184
  }, [effectivePage, objectName, pureRecordId, dataSource, objectDef]);
185
+ // Schema-driven path: derive a human-readable record title from the
186
+ // loaded `pageRecord` so favourites (record:*) and the breadcrumb show
187
+ // e.g. "Acme Corporation" instead of the raw record id. The legacy
188
+ // `DetailView` path keeps using its own `onDataLoaded` callback below.
189
+ useEffect(() => {
190
+ if (!pageRecord || typeof pageRecord !== 'object' || !objectDef)
191
+ return;
192
+ const resolved = getRecordDisplayName(objectDef, pageRecord);
193
+ if (resolved && resolved !== 'Untitled' && resolved !== recordTitle) {
194
+ setRecordTitle(resolved);
195
+ }
196
+ }, [pageRecord, objectDef, recordTitle]);
197
+ // Once we have a human-readable title, (a) record this visit into the
198
+ // "Recently Accessed" rail on the home page and (b) self-heal any
199
+ // previously-favorited entry whose label was saved as the raw record id
200
+ // (because the title hadn't loaded yet at the time of the toggle).
201
+ useEffect(() => {
202
+ if (!objectName || !pureRecordId || !appName)
203
+ return;
204
+ if (!recordTitle)
205
+ return;
206
+ const favId = `record:${objectName}:${pureRecordId}`;
207
+ const href = `/apps/${appName}/${objectName}/record/${pureRecordId}`;
208
+ addRecentItem({ id: favId, label: recordTitle, href, type: 'record' });
209
+ refreshFavoriteLabel(favId, recordTitle);
210
+ }, [appName, objectName, pureRecordId, recordTitle, addRecentItem, refreshFavoriteLabel]);
153
211
  // ─── Action Provider Handlers ───────────────────────────────────────
154
212
  // Confirm dialog state (promise-based)
155
213
  const [confirmState, setConfirmState] = useState({ open: false, message: '' });
@@ -1107,14 +1165,23 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
1107
1165
  title: document.title,
1108
1166
  url: window.location.href,
1109
1167
  });
1110
- }
1111
- else {
1112
- await navigator.clipboard.writeText(window.location.href);
1113
- toast.success(t('detail.linkCopied', { defaultValue: 'Link copied' }));
1168
+ return;
1114
1169
  }
1115
1170
  }
1116
1171
  catch {
1117
- // user dismissed share sheet — no-op
1172
+ // user dismissed the native share sheet — no-op
1173
+ return;
1174
+ }
1175
+ // Fallback path: clipboard. Surface failure to the user so we
1176
+ // never silently no-op (e.g. when clipboard access is denied
1177
+ // because the page is not focused or running over http://).
1178
+ try {
1179
+ await navigator.clipboard.writeText(window.location.href);
1180
+ toast.success(t('detail.linkCopied', { defaultValue: 'Link copied' }));
1181
+ }
1182
+ catch (err) {
1183
+ toast.error(t('detail.linkCopyFailed', { defaultValue: 'Failed to copy link' }) +
1184
+ (err?.message ? `: ${err.message}` : ''));
1118
1185
  }
1119
1186
  },
1120
1187
  });
@@ -1185,16 +1252,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
1185
1252
  history: synthHistory,
1186
1253
  ...(assignedSlots ? { slots: assignedSlots } : {}),
1187
1254
  });
1188
- return (_jsxs("div", { className: "h-full bg-background overflow-hidden flex flex-col relative", children: [_jsxs("div", { className: "absolute top-2 sm:top-4 right-2 sm:right-4 z-50 flex items-center gap-2", children: [objectName && pureRecordId && (_jsx(Button, { size: "sm", variant: "ghost", className: "h-8 w-8 p-0", onClick: () => toggleFavorite({
1189
- id: `record:${objectName}:${pureRecordId}`,
1190
- label: recordTitle || pureRecordId || '',
1191
- href: `/apps/${appName}/${objectName}/record/${pureRecordId}`,
1192
- type: 'record',
1193
- }), "aria-pressed": isFavorite(`record:${objectName}:${pureRecordId}`), "aria-label": isFavorite(`record:${objectName}:${pureRecordId}`)
1194
- ? t('common.removeFromFavorites', { defaultValue: 'Remove from favorites' })
1195
- : t('common.addToFavorites', { defaultValue: 'Add to favorites' }), "data-testid": `record-favorite-btn-${pureRecordId}`, children: isFavorite(`record:${objectName}:${pureRecordId}`)
1196
- ? _jsx(Star, { className: "h-4 w-4 fill-amber-400 text-amber-400" })
1197
- : _jsx(StarOff, { className: "h-4 w-4" }) })), _jsx(ManagedByBadge, { managedBy: objectDef?.managedBy }), recordViewers.length > 0 && (_jsxs("div", { className: "flex items-center gap-1.5", title: t('recordDetail.viewersTooltip'), children: [_jsx(Users, { className: "h-3.5 w-3.5 text-muted-foreground" }), _jsx(PresenceAvatars, { users: recordViewers, size: "sm", maxVisible: 4, showStatus: true })] }))] }), _jsx(RecordContextProvider, { objectName: objectName, recordId: pureRecordId, data: pageRecord, objectSchema: objectDef, dataSource: dataSource, embedded: embedded, headerSystemActions: synthSystemActions, children: _jsx(HighlightFieldsProvider, { children: _jsx(DiscussionContextProvider, { items: feedItems, onAddComment: handleAddComment, onAddReply: handleAddReply, onToggleReaction: handleToggleReaction, children: _jsx(ActionProvider, { context: { record: pageRecord || {}, objectName, user: currentUser }, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, modal: serverActionHandler, approval: approvalHandler }, children: _jsxs("div", { className: "flex-1 overflow-hidden flex flex-row", children: [_jsxs("div", { className: "flex-1 overflow-auto p-3 sm:p-4 lg:p-6 scroll-pb-48", children: [_jsx(SchemaRenderer, { schema: renderedPage }), showAutoDiscussion && (_jsx("div", { className: "mt-6", children: _jsx(RecordChatterPanel, { config: {
1255
+ return (_jsxs("div", { className: "h-full bg-background overflow-hidden flex flex-col relative", children: [_jsxs("div", { className: "absolute top-2 sm:top-4 right-2 sm:right-4 z-50 flex items-center gap-2", children: [_jsx(ManagedByBadge, { managedBy: objectDef?.managedBy }), recordViewers.length > 0 && (_jsxs("div", { className: "flex items-center gap-1.5", title: t('recordDetail.viewersTooltip'), children: [_jsx(Users, { className: "h-3.5 w-3.5 text-muted-foreground" }), _jsx(PresenceAvatars, { users: recordViewers, size: "sm", maxVisible: 4, showStatus: true })] }))] }), _jsx(RecordContextProvider, { objectName: objectName, recordId: pureRecordId, data: pageRecord, objectSchema: objectDef, dataSource: dataSource, embedded: embedded, headerSystemActions: synthSystemActions, isFavorite: isRecordFavorite, onToggleFavorite: favoriteRecord ? handleToggleRecordFavorite : undefined, children: _jsx(HighlightFieldsProvider, { children: _jsx(DiscussionContextProvider, { items: feedItems, onAddComment: handleAddComment, onAddReply: handleAddReply, onToggleReaction: handleToggleReaction, children: _jsx(ActionProvider, { context: { record: pageRecord || {}, objectName, user: currentUser }, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, modal: serverActionHandler, approval: approvalHandler }, children: _jsxs("div", { className: "flex-1 overflow-hidden flex flex-row", children: [_jsxs("div", { className: "flex-1 overflow-auto p-3 sm:p-4 lg:p-6 scroll-pb-48", children: [_jsx(SchemaRenderer, { schema: renderedPage }), showAutoDiscussion && (_jsx("div", { className: "mt-6", children: _jsx(RecordChatterPanel, { config: {
1198
1256
  position: 'bottom',
1199
1257
  collapsible: false,
1200
1258
  feed: {
@@ -1210,16 +1268,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
1210
1268
  setParamState(s => ({ ...s, open: false }));
1211
1269
  } })] }));
1212
1270
  }
1213
- return (_jsxs("div", { className: "h-full bg-background overflow-hidden flex flex-col relative", children: [_jsxs("div", { className: "absolute top-2 sm:top-4 right-2 sm:right-4 z-50 flex items-center gap-2", children: [objectName && pureRecordId && (_jsx(Button, { size: "sm", variant: "ghost", className: "h-8 w-8 p-0", onClick: () => toggleFavorite({
1214
- id: `record:${objectName}:${pureRecordId}`,
1215
- label: recordTitle || pureRecordId || '',
1216
- href: `/apps/${appName}/${objectName}/record/${pureRecordId}`,
1217
- type: 'record',
1218
- }), "aria-pressed": isFavorite(`record:${objectName}:${pureRecordId}`), "aria-label": isFavorite(`record:${objectName}:${pureRecordId}`)
1219
- ? t('common.removeFromFavorites', { defaultValue: 'Remove from favorites' })
1220
- : t('common.addToFavorites', { defaultValue: 'Add to favorites' }), "data-testid": `record-favorite-btn-${pureRecordId}`, children: isFavorite(`record:${objectName}:${pureRecordId}`)
1221
- ? _jsx(Star, { className: "h-4 w-4 fill-amber-400 text-amber-400" })
1222
- : _jsx(StarOff, { className: "h-4 w-4" }) })), _jsx(ManagedByBadge, { managedBy: objectDef?.managedBy }), recordViewers.length > 0 && (_jsxs("div", { className: "flex items-center gap-1.5", title: t('recordDetail.viewersTooltip'), children: [_jsx(Users, { className: "h-3.5 w-3.5 text-muted-foreground" }), _jsx(PresenceAvatars, { users: recordViewers, size: "sm", maxVisible: 4, showStatus: true })] }))] }), _jsxs("div", { className: "flex-1 overflow-hidden flex flex-row", children: [_jsx("div", { className: "flex-1 overflow-auto p-3 sm:p-4 lg:p-6 scroll-pb-48", children: _jsx(ActionProvider, { context: { record: {}, objectName, user: currentUser }, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, modal: serverActionHandler, approval: approvalHandler }, children: _jsx(DetailView, { schema: detailSchema, dataSource: dataSource, objectLabel: objectLabel({ name: objectDef.name, label: objectDef.label }), onDataLoaded: (record) => {
1271
+ return (_jsxs("div", { className: "h-full bg-background overflow-hidden flex flex-col relative", children: [_jsxs("div", { className: "absolute top-2 sm:top-4 right-2 sm:right-4 z-50 flex items-center gap-2", children: [_jsx(ManagedByBadge, { managedBy: objectDef?.managedBy }), recordViewers.length > 0 && (_jsxs("div", { className: "flex items-center gap-1.5", title: t('recordDetail.viewersTooltip'), children: [_jsx(Users, { className: "h-3.5 w-3.5 text-muted-foreground" }), _jsx(PresenceAvatars, { users: recordViewers, size: "sm", maxVisible: 4, showStatus: true })] }))] }), _jsxs("div", { className: "flex-1 overflow-hidden flex flex-row", children: [_jsx("div", { className: "flex-1 overflow-auto p-3 sm:p-4 lg:p-6 scroll-pb-48", children: _jsx(ActionProvider, { context: { record: {}, objectName, user: currentUser }, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, modal: serverActionHandler, approval: approvalHandler }, children: _jsx(DetailView, { schema: detailSchema, dataSource: dataSource, objectLabel: objectLabel({ name: objectDef.name, label: objectDef.label }), isFavorite: isRecordFavorite, onToggleFavorite: favoriteRecord ? handleToggleRecordFavorite : undefined, onDataLoaded: (record) => {
1223
1272
  if (!record || typeof record !== 'object')
1224
1273
  return;
1225
1274
  // Resolve the same way DetailView's header does, so the
@@ -352,5 +352,5 @@ export function ReportView({ dataSource }) {
352
352
  allowExport: true,
353
353
  loading: dataLoading, // Loading state for data fetching
354
354
  };
355
- return (_jsxs("div", { className: "flex flex-col h-full overflow-hidden bg-background", children: [_jsxs("div", { className: "flex flex-col sm:flex-row justify-between sm:items-center gap-3 sm:gap-4 p-4 sm:p-6 border-b shrink-0", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("h1", { className: "text-lg sm:text-xl md:text-2xl font-bold tracking-tight truncate", children: previewReport.title || previewReport.label || 'Report Viewer' }), previewReport.description && (_jsx("p", { className: "text-sm text-muted-foreground mt-1 line-clamp-2", children: previewReport.description }))] }), _jsx("div", { className: "shrink-0 flex items-center gap-1.5", children: _jsxs("button", { type: "button", onClick: handleOpenConfigPanel, className: "inline-flex items-center gap-1.5 rounded-md border border-input bg-background px-2.5 py-1.5 text-xs font-medium text-muted-foreground shadow-sm hover:bg-accent hover:text-accent-foreground", "data-testid": "report-edit-button", children: [_jsx(Pencil, { className: "h-3.5 w-3.5" }), "Edit"] }) })] }), _jsxs("div", { className: "flex-1 overflow-hidden flex flex-col sm:flex-row relative", children: [_jsx("div", { className: "flex-1 min-w-0 overflow-auto p-4 sm:p-6 lg:p-8 bg-muted/5", children: _jsx("div", { className: "w-full shadow-sm border rounded-lg sm:rounded-xl bg-background overflow-hidden min-h-150", children: _jsx(Suspense, { fallback: _jsx("div", { className: "p-8 text-sm text-muted-foreground", children: "Loading report\u2026" }), children: useSpecRenderer ? (_jsx("div", { className: "p-4 sm:p-6", children: _jsx(ReportRenderer, { schema: previewReport, dataSource: dataSource }) })) : (_jsx(ReportViewer, { schema: viewerSchema })) }) }) }), _jsx(Suspense, { fallback: null, children: _jsx(ReportConfigPanel, { open: configPanelOpen, onClose: handleCloseConfigPanel, config: reportConfig, onSave: handleReportConfigSave, onFieldChange: handleReportFieldChange, availableFields: availableFields, getFieldsForObject: getFieldsForObject }) }), _jsx(MetadataPanel, { open: showDebug, sections: [{ title: 'Report Configuration', data: previewReport }] })] })] }));
355
+ return (_jsxs("div", { className: "flex flex-col h-full overflow-hidden bg-background", children: [_jsxs("div", { className: "flex flex-col sm:flex-row justify-between sm:items-center gap-3 sm:gap-4 p-4 sm:p-6 border-b shrink-0", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("h1", { className: "text-lg sm:text-xl md:text-2xl font-bold tracking-tight truncate", children: previewReport.title || previewReport.label || 'Report Viewer' }), previewReport.description && (_jsx("p", { className: "text-sm text-muted-foreground mt-1 line-clamp-2", children: previewReport.description }))] }), _jsx("div", { className: "shrink-0 flex items-center gap-1.5", children: _jsxs("button", { type: "button", onClick: handleOpenConfigPanel, className: "inline-flex items-center gap-1.5 rounded-md border border-input bg-background px-2.5 py-1.5 text-xs font-medium text-muted-foreground shadow-sm hover:bg-accent hover:text-accent-foreground", "data-testid": "report-edit-button", children: [_jsx(Pencil, { className: "h-3.5 w-3.5" }), t('common.edit')] }) })] }), _jsxs("div", { className: "flex-1 overflow-hidden flex flex-col sm:flex-row relative", children: [_jsx("div", { className: "flex-1 min-w-0 overflow-auto p-4 sm:p-6 lg:p-8 bg-muted/5", children: _jsx("div", { className: "w-full shadow-sm border rounded-lg sm:rounded-xl bg-background overflow-hidden min-h-150", children: _jsx(Suspense, { fallback: _jsx("div", { className: "p-8 text-sm text-muted-foreground", children: t('common.loading', { defaultValue: 'Loading…' }) }), children: useSpecRenderer ? (_jsx("div", { className: "p-4 sm:p-6", children: _jsx(ReportRenderer, { schema: previewReport, dataSource: dataSource }) })) : (_jsx(ReportViewer, { schema: viewerSchema })) }) }) }), _jsx(Suspense, { fallback: null, children: _jsx(ReportConfigPanel, { open: configPanelOpen, onClose: handleCloseConfigPanel, config: reportConfig, onSave: handleReportConfigSave, onFieldChange: handleReportFieldChange, availableFields: availableFields, getFieldsForObject: getFieldsForObject }) }), _jsx(MetadataPanel, { open: showDebug, sections: [{ title: 'Report Configuration', data: previewReport }] })] })] }));
356
356
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/app-shell",
3
- "version": "5.0.1",
3
+ "version": "5.0.2",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Minimal application shell for ObjectUI - framework-agnostic rendering engine",
@@ -27,35 +27,35 @@
27
27
  "dependencies": {
28
28
  "lucide-react": "^1.16.0",
29
29
  "sonner": "^2.0.7",
30
- "@object-ui/auth": "5.0.1",
31
- "@object-ui/collaboration": "5.0.1",
32
- "@object-ui/components": "5.0.1",
33
- "@object-ui/core": "5.0.1",
34
- "@object-ui/data-objectstack": "5.0.1",
35
- "@object-ui/fields": "5.0.1",
36
- "@object-ui/i18n": "5.0.1",
37
- "@object-ui/layout": "5.0.1",
38
- "@object-ui/permissions": "5.0.1",
39
- "@object-ui/providers": "5.0.1",
40
- "@object-ui/react": "5.0.1",
41
- "@object-ui/types": "5.0.1"
30
+ "@object-ui/auth": "5.0.2",
31
+ "@object-ui/collaboration": "5.0.2",
32
+ "@object-ui/components": "5.0.2",
33
+ "@object-ui/core": "5.0.2",
34
+ "@object-ui/data-objectstack": "5.0.2",
35
+ "@object-ui/fields": "5.0.2",
36
+ "@object-ui/i18n": "5.0.2",
37
+ "@object-ui/layout": "5.0.2",
38
+ "@object-ui/permissions": "5.0.2",
39
+ "@object-ui/providers": "5.0.2",
40
+ "@object-ui/react": "5.0.2",
41
+ "@object-ui/types": "5.0.2"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "react": "^18.0.0 || ^19.0.0",
45
45
  "react-dom": "^18.0.0 || ^19.0.0",
46
46
  "react-router-dom": "^6.0.0 || ^7.0.0",
47
- "@object-ui/plugin-calendar": "^5.0.1",
48
- "@object-ui/plugin-charts": "^5.0.1",
49
- "@object-ui/plugin-chatbot": "^5.0.1",
50
- "@object-ui/plugin-dashboard": "^5.0.1",
51
- "@object-ui/plugin-designer": "^5.0.1",
52
- "@object-ui/plugin-detail": "^5.0.1",
53
- "@object-ui/plugin-form": "^5.0.1",
54
- "@object-ui/plugin-grid": "^5.0.1",
55
- "@object-ui/plugin-kanban": "^5.0.1",
56
- "@object-ui/plugin-list": "^5.0.1",
57
- "@object-ui/plugin-report": "^5.0.1",
58
- "@object-ui/plugin-view": "^5.0.1"
47
+ "@object-ui/plugin-calendar": "^5.0.2",
48
+ "@object-ui/plugin-charts": "^5.0.2",
49
+ "@object-ui/plugin-chatbot": "^5.0.2",
50
+ "@object-ui/plugin-dashboard": "^5.0.2",
51
+ "@object-ui/plugin-designer": "^5.0.2",
52
+ "@object-ui/plugin-detail": "^5.0.2",
53
+ "@object-ui/plugin-form": "^5.0.2",
54
+ "@object-ui/plugin-grid": "^5.0.2",
55
+ "@object-ui/plugin-kanban": "^5.0.2",
56
+ "@object-ui/plugin-list": "^5.0.2",
57
+ "@object-ui/plugin-report": "^5.0.2",
58
+ "@object-ui/plugin-view": "^5.0.2"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@types/node": "^25.9.0",