@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 +41 -0
- package/dist/console/home/RecentApps.js +10 -3
- package/dist/console/home/StarredApps.js +19 -4
- package/dist/context/FavoritesProvider.d.ts +7 -0
- package/dist/context/FavoritesProvider.js +21 -1
- package/dist/layout/AppSidebar.js +2 -2
- package/dist/layout/UnifiedSidebar.js +2 -2
- package/dist/views/ObjectView.js +4 -4
- package/dist/views/RecordDetailView.js +89 -40
- package/dist/views/ReportView.js +1 -1
- package/package.json +25 -25
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 =
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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));
|
package/dist/views/ObjectView.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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: [
|
|
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: [
|
|
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
|
package/dist/views/ReportView.js
CHANGED
|
@@ -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" }),
|
|
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.
|
|
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.
|
|
31
|
-
"@object-ui/collaboration": "5.0.
|
|
32
|
-
"@object-ui/components": "5.0.
|
|
33
|
-
"@object-ui/core": "5.0.
|
|
34
|
-
"@object-ui/data-objectstack": "5.0.
|
|
35
|
-
"@object-ui/fields": "5.0.
|
|
36
|
-
"@object-ui/i18n": "5.0.
|
|
37
|
-
"@object-ui/layout": "5.0.
|
|
38
|
-
"@object-ui/permissions": "5.0.
|
|
39
|
-
"@object-ui/providers": "5.0.
|
|
40
|
-
"@object-ui/react": "5.0.
|
|
41
|
-
"@object-ui/types": "5.0.
|
|
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.
|
|
48
|
-
"@object-ui/plugin-charts": "^5.0.
|
|
49
|
-
"@object-ui/plugin-chatbot": "^5.0.
|
|
50
|
-
"@object-ui/plugin-dashboard": "^5.0.
|
|
51
|
-
"@object-ui/plugin-designer": "^5.0.
|
|
52
|
-
"@object-ui/plugin-detail": "^5.0.
|
|
53
|
-
"@object-ui/plugin-form": "^5.0.
|
|
54
|
-
"@object-ui/plugin-grid": "^5.0.
|
|
55
|
-
"@object-ui/plugin-kanban": "^5.0.
|
|
56
|
-
"@object-ui/plugin-list": "^5.0.
|
|
57
|
-
"@object-ui/plugin-report": "^5.0.
|
|
58
|
-
"@object-ui/plugin-view": "^5.0.
|
|
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",
|