@object-ui/app-shell 5.0.0 → 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 +59 -0
- package/dist/components/ObjectRenderer.js +1 -1
- package/dist/console/home/AppCard.js +6 -1
- package/dist/console/home/HomePage.js +4 -4
- package/dist/console/home/QuickActions.js +2 -2
- package/dist/console/home/RecentApps.js +12 -5
- package/dist/console/home/StarredApps.js +20 -5
- package/dist/console/organizations/manage/SettingsPage.js +34 -4
- package/dist/context/FavoritesProvider.d.ts +8 -1
- package/dist/context/FavoritesProvider.js +21 -1
- package/dist/layout/AppHeader.js +2 -2
- package/dist/layout/AppSidebar.js +2 -2
- package/dist/layout/UnifiedSidebar.js +2 -2
- package/dist/views/ObjectView.js +18 -7
- package/dist/views/RecordDetailView.js +88 -19
- package/dist/views/ReportView.js +1 -1
- package/package.json +25 -24
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,64 @@
|
|
|
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
|
+
|
|
44
|
+
## 5.0.1
|
|
45
|
+
|
|
46
|
+
### Patch Changes
|
|
47
|
+
|
|
48
|
+
- cb4879e: form
|
|
49
|
+
- @object-ui/types@5.0.1
|
|
50
|
+
- @object-ui/core@5.0.1
|
|
51
|
+
- @object-ui/i18n@5.0.1
|
|
52
|
+
- @object-ui/react@5.0.1
|
|
53
|
+
- @object-ui/components@5.0.1
|
|
54
|
+
- @object-ui/fields@5.0.1
|
|
55
|
+
- @object-ui/layout@5.0.1
|
|
56
|
+
- @object-ui/data-objectstack@5.0.1
|
|
57
|
+
- @object-ui/auth@5.0.1
|
|
58
|
+
- @object-ui/permissions@5.0.1
|
|
59
|
+
- @object-ui/collaboration@5.0.1
|
|
60
|
+
- @object-ui/providers@5.0.1
|
|
61
|
+
|
|
3
62
|
## 5.0.0
|
|
4
63
|
|
|
5
64
|
### Minor Changes
|
|
@@ -45,7 +45,7 @@ export function ObjectRenderer({ objectName, viewId, dataSource, onRecordClick:
|
|
|
45
45
|
}
|
|
46
46
|
}, [objectName, dataSource, externalObjectDef]);
|
|
47
47
|
if (loading) {
|
|
48
|
-
return (_jsx("div", { className: "flex h-full items-center justify-center", children: _jsx("div", { className: "text-muted-foreground", children: "Loading
|
|
48
|
+
return (_jsx("div", { className: "flex h-full items-center justify-center", children: _jsx("div", { className: "text-muted-foreground", children: "Loading\u2026" }) }));
|
|
49
49
|
}
|
|
50
50
|
if (error) {
|
|
51
51
|
return (_jsx("div", { className: "flex h-full items-center justify-center", children: _jsxs("div", { className: "text-destructive", children: ["Error: ", error] }) }));
|
|
@@ -47,7 +47,12 @@ export function AppCard({ app, onClick, isFavorite, index = 0 }) {
|
|
|
47
47
|
type: 'object',
|
|
48
48
|
});
|
|
49
49
|
};
|
|
50
|
-
return (_jsxs(Card, { className: cn('group relative cursor-pointer overflow-hidden border border-border/70 bg-card/80 backdrop-blur-sm', 'transition-
|
|
50
|
+
return (_jsxs(Card, { role: "button", tabIndex: 0, "aria-label": label, className: cn('group relative cursor-pointer overflow-hidden 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-lg', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2', 'motion-reduce:transition-none motion-reduce:hover:transform-none', !primaryColor && accent.ring), onClick: onClick, onKeyDown: (e) => {
|
|
51
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
52
|
+
e.preventDefault();
|
|
53
|
+
onClick();
|
|
54
|
+
}
|
|
55
|
+
}, "data-testid": `app-card-${app.name}`, style: primaryColor ? { borderColor: undefined } : undefined, children: [_jsx("div", { "aria-hidden": true, className: cn('absolute inset-x-0 top-0 h-1', primaryColor ? '' : accent.solid), style: primaryColor ? { backgroundColor: primaryColor } : undefined }), !primaryColor && (_jsx("div", { "aria-hidden": true, className: cn('absolute inset-0 bg-gradient-to-br opacity-0 transition-opacity duration-300 group-hover:opacity-100', accent.from, accent.to) })), _jsxs(CardContent, { className: "relative p-5", children: [_jsx(Button, { variant: "ghost", size: "sm", className: "absolute top-2 right-2 h-8 w-8 p-0 opacity-0 group-hover:opacity-100 focus-visible:opacity-100 transition-opacity", onClick: handleToggleFavorite, "aria-label": isFavorite
|
|
51
56
|
? t('common.removeFromFavorites', { defaultValue: 'Remove from favorites' }) + ` — ${label}`
|
|
52
57
|
: t('common.addToFavorites', { defaultValue: 'Add to favorites' }) + ` — ${label}`, "aria-pressed": isFavorite, "data-testid": `favorite-btn-${app.name}`, children: isFavorite ? (_jsx(Star, { className: "h-4 w-4 fill-amber-400 text-amber-400" })) : (_jsx(StarOff, { className: "h-4 w-4" })) }), _jsx("div", { className: cn('inline-flex h-14 w-14 items-center justify-center rounded-xl mb-4 ring-1 ring-inset', primaryColor ? '' : cn('bg-gradient-to-br', accent.from, accent.to, 'ring-border/40')), style: primaryColor
|
|
53
58
|
? { backgroundColor: `${primaryColor}1f`, boxShadow: `inset 0 0 0 1px ${primaryColor}33` }
|
|
@@ -58,20 +58,20 @@ export function HomePage() {
|
|
|
58
58
|
const { user } = useAuth();
|
|
59
59
|
const activeApps = apps.filter((a) => a.active !== false);
|
|
60
60
|
const recentApps = recentItems
|
|
61
|
-
.filter(item => item.type === 'object' || item.type === 'dashboard' || item.type === 'page')
|
|
61
|
+
.filter(item => item.type === 'object' || item.type === 'dashboard' || item.type === 'page' || item.type === 'record')
|
|
62
62
|
.slice(0, 6);
|
|
63
63
|
const starredApps = favorites
|
|
64
|
-
.filter(item => item.type === 'object' || item.type === 'dashboard' || item.type === 'page')
|
|
64
|
+
.filter(item => item.type === 'object' || item.type === 'dashboard' || item.type === 'page' || item.type === 'record')
|
|
65
65
|
.slice(0, 8);
|
|
66
66
|
const greeting = useMemo(() => t(pickGreetingKey(new Date().getHours()), { defaultValue: 'Welcome' }), [t]);
|
|
67
67
|
const displayName = (user?.name?.trim() || user?.email?.split('@')[0] || '').trim();
|
|
68
68
|
if (loading) {
|
|
69
|
-
return (_jsx("div", { className: "flex flex-1 items-center justify-center py-20", children: _jsx("div", { className: "text-muted-foreground", children: t('home.loading', { defaultValue: 'Loading workspace
|
|
69
|
+
return (_jsx("div", { className: "flex flex-1 items-center justify-center py-20", children: _jsx("div", { className: "text-muted-foreground", children: t('home.loading', { defaultValue: 'Loading workspace…' }) }) }));
|
|
70
70
|
}
|
|
71
71
|
if (activeApps.length === 0) {
|
|
72
72
|
return (_jsx("div", { className: "flex flex-1 items-center justify-center p-6", children: _jsxs(Empty, { children: [_jsx(EmptyTitle, { children: t('home.welcome', { defaultValue: 'Welcome to ObjectUI' }) }), _jsx(EmptyDescription, { children: t('home.welcomeDescription', {
|
|
73
73
|
defaultValue: 'Get started by creating your first application or configure your system settings.',
|
|
74
74
|
}) }), _jsxs("div", { className: "mt-6 flex flex-col sm:flex-row items-center gap-3", children: [_jsxs(Button, { onClick: () => navigate('/create-app'), "data-testid": "create-first-app-btn", children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), t('home.createFirstApp', { defaultValue: 'Create Your First App' })] }), _jsxs(Button, { variant: "outline", onClick: () => navigate('/apps/setup'), "data-testid": "go-to-settings-btn", children: [_jsx(Settings, { className: "mr-2 h-4 w-4" }), t('home.systemSettings', { defaultValue: 'System Settings' })] })] })] }) }));
|
|
75
75
|
}
|
|
76
|
-
return (_jsxs("div", { className: "relative isolate min-h-full bg-gradient-to-b from-background via-background to-muted/40", children: [_jsxs("div", { "aria-hidden": true, className: "pointer-events-none absolute inset-x-0 top-0 -z-10 h-[28rem] overflow-hidden", children: [_jsx("div", { className: "absolute -top-32 -left-24 h-[28rem] w-[28rem] rounded-full bg-primary/30 blur-3xl opacity-70 dark:opacity-40" }), _jsx("div", { className: "absolute -top-20 right-[-6rem] h-[26rem] w-[36rem] rounded-full bg-sky-400/30 blur-3xl opacity-70 dark:opacity-35" }), _jsx("div", { className: "absolute top-32 left-1/3 h-[18rem] w-[24rem] rounded-full bg-fuchsia-400/25 blur-3xl opacity-60 dark:opacity-25" }), _jsx("div", { className: "absolute inset-0 bg-gradient-to-b from-transparent via-background/40 to-background" })] }), _jsx("section", { className: "px-4 sm:px-6 lg:px-8 pt-10 pb-6", children: _jsxs("div", { className: "max-w-7xl mx-auto", children: [_jsxs("div", { className: "flex items-center gap-2 text-xs font-medium text-muted-foreground mb-3", children: [_jsx(Sparkles, { className: "h-3.5 w-3.5 text-primary" }), _jsx("span", { className: "uppercase tracking-wider", children: t('home.title', { defaultValue: 'Home' }) })] }), _jsxs("h1", { className: "text-3xl sm:text-4xl lg:text-5xl font-bold tracking-tight", children: [_jsxs("span", { className: "bg-gradient-to-r from-foreground via-foreground to-foreground/70 bg-clip-text text-transparent", children: [greeting, displayName ? `, ${displayName}` : ''] }), _jsx("span", { className: "text-foreground/40", children: "." })] }), _jsx("p", { className: "text-base sm:text-lg text-muted-foreground mt-2 max-w-2xl", children: t('home.heroTagline', { defaultValue: 'Pick up where you left off, or explore something new.' }) })] }) }), _jsx("div", { className: "px-4 sm:px-6 lg:px-8 pb-16", children: _jsxs("div", { className: "max-w-7xl mx-auto space-y-10", children: [starredApps.length === 0 && recentApps.length === 0 && (_jsx(GettingStartedHint, { t: t })), starredApps.length > 0 && _jsx(StarredApps, { items: starredApps }), recentApps.length > 0 && _jsx(RecentApps, { items: recentApps }), _jsxs("section", { children: [_jsx("div", { className: "flex items-end justify-between mb-5", children: _jsxs("div", { children: [_jsx("h2", { className: "text-2xl font-semibold tracking-tight", children: t('home.allApps', { defaultValue: 'All Applications' }) }), _jsxs("p", { className: "text-sm text-muted-foreground mt-1", children: [activeApps.length, ' · ', t('home.stats.apps', { defaultValue: 'Applications' })] })] }) }), _jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4", children: activeApps.map((app, idx) => (_jsx(AppCard, { app: app, index: idx, onClick: () => navigate(`/apps/${app.name}`), isFavorite: favorites.some(f => f.id === `app:${app.name}`) }, app.name))) })] })] }) })] }));
|
|
76
|
+
return (_jsxs("div", { className: "relative isolate min-h-full bg-gradient-to-b from-background via-background to-muted/40", children: [_jsxs("div", { "aria-hidden": true, className: "pointer-events-none absolute inset-x-0 top-0 -z-10 h-[28rem] overflow-hidden", children: [_jsx("div", { className: "absolute -top-32 -left-24 h-[28rem] w-[28rem] rounded-full bg-primary/30 blur-3xl opacity-70 dark:opacity-40" }), _jsx("div", { className: "absolute -top-20 right-[-6rem] h-[26rem] w-[36rem] rounded-full bg-sky-400/30 blur-3xl opacity-70 dark:opacity-35" }), _jsx("div", { className: "absolute top-32 left-1/3 h-[18rem] w-[24rem] rounded-full bg-fuchsia-400/25 blur-3xl opacity-60 dark:opacity-25" }), _jsx("div", { className: "absolute inset-0 bg-gradient-to-b from-transparent via-background/40 to-background" })] }), _jsx("section", { className: "px-4 sm:px-6 lg:px-8 pt-10 pb-6", children: _jsxs("div", { className: "max-w-7xl mx-auto", children: [_jsxs("div", { className: "flex items-center gap-2 text-xs font-medium text-muted-foreground mb-3", children: [_jsx(Sparkles, { className: "h-3.5 w-3.5 text-primary" }), _jsx("span", { className: "uppercase tracking-wider", children: t('home.title', { defaultValue: 'Home' }) })] }), _jsxs("h1", { className: "text-3xl sm:text-4xl lg:text-5xl font-bold tracking-tight text-pretty", children: [_jsxs("span", { className: "bg-gradient-to-r from-foreground via-foreground to-foreground/70 bg-clip-text text-transparent", children: [greeting, displayName ? `, ${displayName}` : ''] }), _jsx("span", { className: "text-foreground/40", children: "." })] }), _jsx("p", { className: "text-base sm:text-lg text-muted-foreground mt-2 max-w-2xl", children: t('home.heroTagline', { defaultValue: 'Pick up where you left off, or explore something new.' }) })] }) }), _jsx("div", { className: "px-4 sm:px-6 lg:px-8 pb-16", children: _jsxs("div", { className: "max-w-7xl mx-auto space-y-10", children: [starredApps.length === 0 && recentApps.length === 0 && (_jsx(GettingStartedHint, { t: t })), starredApps.length > 0 && _jsx(StarredApps, { items: starredApps }), recentApps.length > 0 && _jsx(RecentApps, { items: recentApps }), _jsxs("section", { children: [_jsx("div", { className: "flex items-end justify-between mb-5", children: _jsxs("div", { children: [_jsx("h2", { className: "text-2xl font-semibold tracking-tight", children: t('home.allApps', { defaultValue: 'All Applications' }) }), _jsxs("p", { className: "text-sm text-muted-foreground mt-1", children: [activeApps.length, ' · ', t('home.stats.apps', { defaultValue: 'Applications' })] })] }) }), _jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4", children: activeApps.map((app, idx) => (_jsx(AppCard, { app: app, index: idx, onClick: () => navigate(`/apps/${app.name}`), isFavorite: favorites.some(f => f.id === `app:${app.name}`) }, app.name))) })] })] }) })] }));
|
|
77
77
|
}
|
|
@@ -49,11 +49,11 @@ export function QuickActions() {
|
|
|
49
49
|
];
|
|
50
50
|
return (_jsxs("section", { children: [_jsx("h2", { className: "text-2xl font-semibold tracking-tight mb-5", children: t('home.quickActions.title', { defaultValue: 'Quick Actions' }) }), _jsx("div", { className: "grid grid-cols-1 md:grid-cols-3 gap-4", children: actions.map((action) => {
|
|
51
51
|
const Icon = action.icon;
|
|
52
|
-
return (_jsx(Card, { className: cn('group cursor-pointer border border-border/70 bg-card/80 backdrop-blur-sm', 'transition-
|
|
52
|
+
return (_jsx(Card, { className: cn('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 motion-reduce:transition-none motion-reduce:hover:transform-none', action.hoverBorder), onClick: () => navigate(action.href), "data-testid": `quick-action-${action.id}`, role: "link", tabIndex: 0, onKeyDown: (e) => {
|
|
53
53
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
54
54
|
e.preventDefault();
|
|
55
55
|
navigate(action.href);
|
|
56
56
|
}
|
|
57
|
-
}, "aria-label": action.label, children: _jsx(CardContent, { className: "p-5", children: _jsxs("div", { className: "flex items-start gap-4", children: [_jsx("div", { className: cn('inline-flex h-11 w-11 items-center justify-center rounded-xl ring-1 ring-inset shrink-0', action.iconBg), children: _jsx(Icon, { className: cn('h-5 w-5', action.iconText) }) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("h3", { className: "font-semibold text-base leading-tight", children: action.label }), _jsx(ArrowUpRight, { className: "h-4 w-4 text-muted-foreground opacity-0 -translate-x-1 transition-
|
|
57
|
+
}, "aria-label": action.label, children: _jsx(CardContent, { className: "p-5", children: _jsxs("div", { className: "flex items-start gap-4", children: [_jsx("div", { className: cn('inline-flex h-11 w-11 items-center justify-center rounded-xl ring-1 ring-inset shrink-0', action.iconBg), children: _jsx(Icon, { className: cn('h-5 w-5', action.iconText) }) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("h3", { className: "font-semibold text-base leading-tight", children: action.label }), _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" })] }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: action.description })] })] }) }) }, action.id));
|
|
58
58
|
}) })] }));
|
|
59
59
|
}
|
|
@@ -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,22 +17,30 @@ 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
|
});
|
|
31
38
|
const tone = TYPE_TONES[item.type] || TYPE_TONES.object;
|
|
32
|
-
return (_jsx(Card, { className: "group cursor-pointer border border-border/70 bg-card/80 backdrop-blur-sm transition-
|
|
39
|
+
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": `recent-item-${item.id}`, role: "link", tabIndex: 0, onKeyDown: (e) => {
|
|
33
40
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
34
41
|
e.preventDefault();
|
|
35
42
|
navigate(item.href);
|
|
36
43
|
}
|
|
37
|
-
}, 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-
|
|
44
|
+
}, 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));
|
|
38
45
|
}) })] }));
|
|
39
46
|
}
|
|
@@ -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;
|
|
29
|
-
|
|
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
|
+
});
|
|
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
|
}
|
|
@@ -4,11 +4,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
*
|
|
5
5
|
* Organization settings: general info form + danger zone.
|
|
6
6
|
*/
|
|
7
|
-
import { useCallback, useEffect, useState } from 'react';
|
|
8
|
-
import { Button, Input, Label, Separator, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@object-ui/components';
|
|
7
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
8
|
+
import { Avatar, AvatarFallback, AvatarImage, Button, Input, Label, Separator, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@object-ui/components';
|
|
9
9
|
import { useAuth } from '@object-ui/auth';
|
|
10
10
|
import { useObjectTranslation } from '@object-ui/i18n';
|
|
11
|
-
import {
|
|
11
|
+
import { useUpload } from '@object-ui/providers';
|
|
12
|
+
import { Loader2, Upload, X } from 'lucide-react';
|
|
12
13
|
import { toast } from 'sonner';
|
|
13
14
|
import { useNavigate } from 'react-router-dom';
|
|
14
15
|
import { useOrgContext } from './orgContext';
|
|
@@ -22,6 +23,9 @@ export function SettingsPage() {
|
|
|
22
23
|
const [slug, setSlug] = useState(org.slug ?? '');
|
|
23
24
|
const [logo, setLogo] = useState(org.logo ?? '');
|
|
24
25
|
const [isSaving, setIsSaving] = useState(false);
|
|
26
|
+
const [isUploadingLogo, setIsUploadingLogo] = useState(false);
|
|
27
|
+
const logoInputRef = useRef(null);
|
|
28
|
+
const { upload } = useUpload();
|
|
25
29
|
// Owner check
|
|
26
30
|
const [isOwner, setIsOwner] = useState(null);
|
|
27
31
|
const [membersLoading, setMembersLoading] = useState(true);
|
|
@@ -118,7 +122,33 @@ export function SettingsPage() {
|
|
|
118
122
|
defaultValue: 'Update your organization information.',
|
|
119
123
|
}) }), _jsx(Separator, { className: "my-4" }), !isOwner ? (_jsx("div", { className: "rounded-lg border bg-muted/50 p-4 text-sm text-muted-foreground", children: t('organization.settings.readOnlyNote', {
|
|
120
124
|
defaultValue: 'Only owners can change settings.',
|
|
121
|
-
}) })) : (_jsxs("form", { onSubmit: handleSave, className: "space-y-4 max-w-md", children: [_jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { htmlFor: "org-name", children: t('organization.settings.nameLabel', { defaultValue: 'Organization name' }) }), _jsx(Input, { id: "org-name", value: name, onChange: (e) => setName(e.target.value), required: true, "data-testid": "settings-name-input" })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { htmlFor: "org-slug", children: t('organization.settings.slugLabel', { defaultValue: 'Slug' }) }), _jsx(Input, { id: "org-slug", value: slug, onChange: (e) => setSlug(e.target.value), "data-testid": "settings-slug-input" })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, {
|
|
125
|
+
}) })) : (_jsxs("form", { onSubmit: handleSave, className: "space-y-4 max-w-md", children: [_jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { htmlFor: "org-name", children: t('organization.settings.nameLabel', { defaultValue: 'Organization name' }) }), _jsx(Input, { id: "org-name", value: name, onChange: (e) => setName(e.target.value), required: true, "data-testid": "settings-name-input" })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { htmlFor: "org-slug", children: t('organization.settings.slugLabel', { defaultValue: 'Slug' }) }), _jsx(Input, { id: "org-slug", value: slug, onChange: (e) => setSlug(e.target.value), "data-testid": "settings-slug-input" })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: t('organization.settings.logoLabel', { defaultValue: 'Logo' }) }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsxs(Avatar, { className: "size-16 rounded-md", children: [logo ? (_jsx(AvatarImage, { src: logo, alt: name, className: "object-cover" })) : null, _jsx(AvatarFallback, { className: "rounded-md text-base", children: (name || 'O').slice(0, 2).toUpperCase() })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex gap-2", children: [_jsx("input", { ref: logoInputRef, type: "file", accept: "image/*", className: "hidden", onChange: async (e) => {
|
|
126
|
+
const file = e.target.files?.[0];
|
|
127
|
+
if (logoInputRef.current)
|
|
128
|
+
logoInputRef.current.value = '';
|
|
129
|
+
if (!file)
|
|
130
|
+
return;
|
|
131
|
+
setIsUploadingLogo(true);
|
|
132
|
+
try {
|
|
133
|
+
const result = await upload(file);
|
|
134
|
+
setLogo(result.url);
|
|
135
|
+
toast.success(t('organization.settings.logoUploaded', {
|
|
136
|
+
defaultValue: 'Logo uploaded — save to apply',
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
toast.error(err instanceof Error
|
|
141
|
+
? err.message
|
|
142
|
+
: t('organization.settings.logoUploadFailed', {
|
|
143
|
+
defaultValue: 'Failed to upload logo',
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
setIsUploadingLogo(false);
|
|
148
|
+
}
|
|
149
|
+
}, "data-testid": "settings-logo-file" }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", disabled: isUploadingLogo, onClick: () => logoInputRef.current?.click(), "data-testid": "settings-logo-upload-btn", children: [isUploadingLogo ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Upload, { className: "mr-2 h-4 w-4" })), logo
|
|
150
|
+
? t('organization.settings.logoReplace', { defaultValue: 'Replace' })
|
|
151
|
+
: t('organization.settings.logoUpload', { defaultValue: 'Upload' })] }), logo && (_jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setLogo(''), "data-testid": "settings-logo-clear-btn", children: [_jsx(X, { className: "mr-2 h-4 w-4" }), t('organization.settings.logoClear', { defaultValue: 'Remove' })] }))] }), _jsx(Input, { id: "org-logo", type: "url", value: logo, onChange: (e) => setLogo(e.target.value), placeholder: "https://example.com/logo.png", className: "text-xs", "data-testid": "settings-logo-input" })] })] })] }), _jsxs(Button, { type: "submit", disabled: isSaving, "data-testid": "settings-save-btn", children: [isSaving && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), t('organization.settings.save', { defaultValue: 'Save changes' })] })] }))] }), _jsxs("section", { children: [_jsx("h2", { className: "text-lg font-semibold text-destructive", children: t('organization.settings.dangerZone', { defaultValue: 'Danger zone' }) }), _jsx(Separator, { className: "my-4" }), _jsxs("div", { className: "space-y-4 rounded-lg border border-destructive/50 bg-destructive/5 p-4", children: [_jsxs("div", { className: "flex items-center justify-between gap-4", children: [_jsxs("div", { children: [_jsx("p", { className: "font-medium text-sm", children: t('organization.settings.leaveTitle', { defaultValue: 'Leave organization' }) }), _jsx("p", { className: "text-xs text-muted-foreground", children: t('organization.settings.leaveDescription', {
|
|
122
152
|
defaultValue: 'You will lose access to this organization.',
|
|
123
153
|
}) })] }), _jsx(Button, { variant: "destructive", size: "sm", onClick: () => setIsLeaveOpen(true), children: t('organization.settings.leaveAction', { defaultValue: 'Leave' }) })] }), _jsx(Separator, {}), _jsxs("div", { className: "flex items-center justify-between gap-4", children: [_jsxs("div", { children: [_jsx("p", { className: "font-medium text-sm", children: t('organization.settings.deleteTitle', { defaultValue: 'Delete organization' }) }), _jsx("p", { className: "text-xs text-muted-foreground", children: t('organization.settings.deleteDescription', {
|
|
124
154
|
defaultValue: 'Permanently delete this organization and all its data.',
|
|
@@ -22,7 +22,7 @@ export interface FavoriteItem {
|
|
|
22
22
|
id: string;
|
|
23
23
|
label: string;
|
|
24
24
|
href: string;
|
|
25
|
-
type: 'object' | 'dashboard' | 'page' | 'report';
|
|
25
|
+
type: 'object' | 'dashboard' | 'page' | 'report' | 'record';
|
|
26
26
|
/** ISO timestamp of when the item was favorited */
|
|
27
27
|
favoritedAt: string;
|
|
28
28
|
}
|
|
@@ -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;
|
package/dist/layout/AppHeader.js
CHANGED
|
@@ -376,7 +376,7 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
|
|
|
376
376
|
const lastSegmentLabel = extraSegments[extraSegments.length - 1]?.label || appName || '';
|
|
377
377
|
return (_jsxs("div", { className: "flex items-center justify-between w-full h-full", children: [_jsxs("div", { className: "flex items-center min-w-0 flex-1", children: [_jsx(Link, { to: "/home", className: cn("flex items-center justify-center h-7 w-7 shrink-0 rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors", isApp && "hidden sm:flex"), title: "ObjectStack", children: _jsx(Boxes, { className: "h-4 w-4" }) }), resolvedVariant === 'home' && (_jsx("span", { className: "hidden sm:inline ml-2 text-sm font-semibold tracking-tight", children: "ObjectStack" })), resolvedVariant === 'orgs' && (_jsxs(_Fragment, { children: [_jsx(PathSep, {}), _jsx("span", { className: "text-sm font-medium text-foreground/80 px-1.5", children: t('organizations.title', { defaultValue: 'Organizations' }) })] })), isApp && (_jsxs(_Fragment, { children: [_jsx(SidebarTrigger, { className: "md:hidden shrink-0 ml-1", "aria-label": t('common.toggleSidebar') || 'Toggle sidebar' }), activeAppName && onAppChange ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "hidden sm:flex items-center", children: _jsx(PathSep, {}) }), _jsx("div", { className: "hidden sm:flex items-center", children: _jsx(AppSwitcher, { activeAppName: activeAppName, onAppChange: onAppChange }) })] })) : appName ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "hidden sm:flex items-center", children: _jsx(PathSep, {}) }), _jsx("span", { className: "hidden sm:inline text-sm font-medium text-foreground/80 px-1.5", children: appName })] })) : null, extraSegments.map((seg, i) => {
|
|
378
378
|
const isLast = i === extraSegments.length - 1;
|
|
379
|
-
return (_jsxs("span", { className: "hidden sm:flex items-center min-w-0", children: [_jsx(PathSep, {}), seg.siblings && seg.siblings.length > 1 ? (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { className: `flex items-center gap-1 rounded-md px-1.5 py-1 text-sm font-medium transition-colors outline-none hover:bg-accent hover:text-foreground ${!isLast ? 'text-foreground/60' : 'text-foreground/80'}`, children: [seg.label, _jsx(ChevronDown, { className: "h-3.5 w-3.5 text-muted-foreground" })] }), _jsxs(DropdownMenuContent, { align: "start", sideOffset: 8, className: "w-56 max-h-72 overflow-y-auto", children: [_jsx(DropdownMenuLabel, { className: "text-xs text-muted-foreground font-normal", children: "Switch Object" }), _jsx(DropdownMenuSeparator, {}), seg.siblings.map((sibling) => (_jsx(DropdownMenuItem, { asChild: true, children: _jsx(Link, { to: sibling.href, className: "w-full", children: sibling.label }) }, sibling.href)))] })] })) : seg.href ? (_jsx(Link, { to: seg.href, className: `rounded-md px-1.5 py-1 text-sm font-medium transition-colors hover:bg-accent hover:text-foreground truncate max-w-[160px] ${isLast ? 'text-foreground/80' : 'text-foreground/60'}`, children: seg.label })) : (_jsx("span", { className: `px-1.5 py-1 text-sm font-medium truncate max-w-[160px] ${isLast ? 'text-foreground/80' : 'text-foreground/60'}`, children: seg.label }))] }, i));
|
|
379
|
+
return (_jsxs("span", { className: "hidden sm:flex items-center min-w-0", children: [_jsx(PathSep, {}), seg.siblings && seg.siblings.length > 1 ? (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { className: `flex items-center gap-1 rounded-md px-1.5 py-1 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring hover:bg-accent hover:text-foreground ${!isLast ? 'text-foreground/60' : 'text-foreground/80'}`, children: [seg.label, _jsx(ChevronDown, { className: "h-3.5 w-3.5 text-muted-foreground" })] }), _jsxs(DropdownMenuContent, { align: "start", sideOffset: 8, className: "w-56 max-h-72 overflow-y-auto", children: [_jsx(DropdownMenuLabel, { className: "text-xs text-muted-foreground font-normal", children: "Switch Object" }), _jsx(DropdownMenuSeparator, {}), seg.siblings.map((sibling) => (_jsx(DropdownMenuItem, { asChild: true, children: _jsx(Link, { to: sibling.href, className: "w-full", children: sibling.label }) }, sibling.href)))] })] })) : seg.href ? (_jsx(Link, { to: seg.href, className: `rounded-md px-1.5 py-1 text-sm font-medium transition-colors hover:bg-accent hover:text-foreground truncate max-w-[160px] ${isLast ? 'text-foreground/80' : 'text-foreground/60'}`, children: seg.label })) : (_jsx("span", { className: `px-1.5 py-1 text-sm font-medium truncate max-w-[160px] ${isLast ? 'text-foreground/80' : 'text-foreground/60'}`, children: seg.label }))] }, i));
|
|
380
380
|
}), mobileSwitcher && mobileSwitcher.views.length > 0 ? (mobileSwitcher.views.length > 1 ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: "sm:hidden flex items-center gap-0.5 min-w-0 ml-1 rounded-md px-1.5 py-1 text-sm font-medium hover:bg-accent active:bg-accent/80 transition-colors", "aria-label": "Switch view", children: [_jsx("span", { className: "truncate max-w-[180px]", children: mobileSwitcher.triggerLabel ??
|
|
381
381
|
mobileSwitcher.views.find((v) => v.id === mobileSwitcher.activeViewId)?.label ??
|
|
382
382
|
lastSegmentLabel }), _jsx(ChevronDown, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" })] }) }), _jsx(DropdownMenuContent, { align: "start", className: "min-w-[220px] max-w-[280px]", children: mobileSwitcher.views.map((v) => {
|
|
@@ -385,5 +385,5 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
|
|
|
385
385
|
if (!isActive)
|
|
386
386
|
mobileSwitcher.onChange(v.id);
|
|
387
387
|
}, className: "gap-2", children: [v.icon ? (_jsx("span", { className: "shrink-0 text-muted-foreground [&>svg]:h-4 [&>svg]:w-4", children: v.icon })) : null, _jsx("span", { className: "flex-1 truncate", children: v.label }), v.locked ? (_jsx(Lock, { className: "h-3 w-3 shrink-0 text-muted-foreground", "aria-hidden": true })) : null, isActive ? (_jsx(Check, { className: "h-4 w-4 shrink-0 text-foreground", "aria-hidden": true })) : null] }, v.id));
|
|
388
|
-
}) })] })) : (_jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: mobileSwitcher.triggerLabel ?? mobileSwitcher.views[0].label }))) : (_jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: lastSegmentLabel }))] }))] }), _jsxs("div", { className: "flex items-center gap-0.5 sm:gap-1 shrink-0 [&>*+*[data-topbar-group]]:ml-1 [&>[data-topbar-group]+[data-topbar-group]]:border-l [&>[data-topbar-group]+[data-topbar-group]]:border-border/60 [&>[data-topbar-group]+[data-topbar-group]]:pl-1 sm:[&>[data-topbar-group]+[data-topbar-group]]:pl-2 sm:[&>[data-topbar-group]+[data-topbar-group]]:ml-2", children: [!isOnline && (_jsxs("div", { className: "flex items-center gap-1 px-2 py-1 rounded-full bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200 text-xs font-medium", children: [_jsx("span", { className: "h-2 w-2 rounded-full bg-yellow-500 animate-pulse" }), "Offline"] })), isApp && connectionState && _jsx(ConnectionStatus, { state: connectionState }), isApp && activeUsers.length > 0 && (_jsx("div", { className: "hidden md:flex items-center shrink-0", title: "Users currently online", children: _jsx(PresenceAvatars, { users: activeUsers, size: "sm", maxVisible: 3, showStatus: true }) })), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 sm:gap-1 shrink-0", children: [_jsxs("button", { onClick: () => document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true })), className: "hidden lg:flex relative items-center gap-2 w-48 xl:w-64 h-8 px-3 text-sm rounded-md border bg-muted/50 text-muted-foreground hover:bg-muted transition-colors", children: [_jsx(Search, { className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "flex-1 text-left text-xs", children: t('console.search', { defaultValue: 'Search
|
|
388
|
+
}) })] })) : (_jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: mobileSwitcher.triggerLabel ?? mobileSwitcher.views[0].label }))) : (_jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: lastSegmentLabel }))] }))] }), _jsxs("div", { className: "flex items-center gap-0.5 sm:gap-1 shrink-0 [&>*+*[data-topbar-group]]:ml-1 [&>[data-topbar-group]+[data-topbar-group]]:border-l [&>[data-topbar-group]+[data-topbar-group]]:border-border/60 [&>[data-topbar-group]+[data-topbar-group]]:pl-1 sm:[&>[data-topbar-group]+[data-topbar-group]]:pl-2 sm:[&>[data-topbar-group]+[data-topbar-group]]:ml-2", children: [!isOnline && (_jsxs("div", { className: "flex items-center gap-1 px-2 py-1 rounded-full bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200 text-xs font-medium", children: [_jsx("span", { className: "h-2 w-2 rounded-full bg-yellow-500 animate-pulse" }), "Offline"] })), isApp && connectionState && _jsx(ConnectionStatus, { state: connectionState }), isApp && activeUsers.length > 0 && (_jsx("div", { className: "hidden md:flex items-center shrink-0", title: "Users currently online", children: _jsx(PresenceAvatars, { users: activeUsers, size: "sm", maxVisible: 3, showStatus: true }) })), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 sm:gap-1 shrink-0", children: [_jsxs("button", { onClick: () => document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true })), className: "hidden lg:flex relative items-center gap-2 w-48 xl:w-64 h-8 px-3 text-sm rounded-md border bg-muted/50 text-muted-foreground hover:bg-muted transition-colors", children: [_jsx(Search, { className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "flex-1 text-left text-xs", children: t('console.search', { defaultValue: 'Search…' }) }), _jsxs("kbd", { className: "pointer-events-none inline-flex h-5 items-center gap-0.5 rounded border bg-background px-1.5 text-[10px] font-medium text-muted-foreground", children: [_jsx("span", { className: "text-xs", children: "\u2318" }), "K"] })] }), _jsx(Button, { variant: "ghost", size: "icon", className: "lg:hidden h-8 w-8 shrink-0", onClick: () => document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true })), "aria-label": t('console.search', { defaultValue: 'Search…' }), children: _jsx(Search, { className: "h-4 w-4" }) })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [_jsx(InboxPopover, { notifications: notifications, unreadCount: unreadCount, pendingApprovalsCount: pendingApprovalsCount, activities: activeActivities, onMarkAllRead: markAllRead, onMarkRead: markNotificationRead }), _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 hidden md:flex shrink-0", asChild: true, "aria-label": t('sidebar.helpTooltip', { defaultValue: 'Help & Documentation' }), children: _jsx("a", { href: "https://docs.objectstack.ai", target: "_blank", rel: "noopener noreferrer", children: _jsx(HelpCircle, { className: "h-4 w-4" }) }) })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [" ", _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 shrink-0 rounded-full", children: _jsxs(Avatar, { className: "h-7 w-7 rounded-full", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-full bg-primary text-primary-foreground text-xs", children: getUserInitials(user) })] }) }) }), _jsxs(DropdownMenuContent, { align: "end", className: "min-w-64 rounded-lg", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "p-0 font-normal", children: _jsxs("div", { className: "flex items-center gap-2 px-2 py-2", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground", children: getUserInitials(user) })] }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.name ?? 'User' }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email ?? '' })] })] }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuGroup, { children: [hasOrgSection && (_jsxs(DropdownMenuItem, { onClick: () => navigate('/organizations'), className: "cursor-pointer", children: [_jsx(Boxes, { className: "mr-2 h-4 w-4" }), t('organizations.mine', { defaultValue: 'My Organizations' })] })), _jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/setup/system/profile'), children: [_jsx(UserIcon, { className: "mr-2 h-4 w-4" }), t('user.profile', { defaultValue: 'Profile' })] }), _jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/setup'), children: [_jsx(Settings, { className: "mr-2 h-4 w-4" }), t('sidebar.settings', { defaultValue: 'Settings' })] })] }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuLabel, { className: "text-[11px] font-normal text-muted-foreground uppercase tracking-wide px-2", children: t('user.preferences', { defaultValue: 'Preferences' }) }), _jsxs("div", { className: "flex items-center justify-between px-2 py-1.5 text-sm", children: [_jsx("span", { className: "text-foreground/80", children: t('user.theme', { defaultValue: 'Theme' }) }), _jsx(ModeToggle, {})] }), _jsxs("div", { className: "flex items-center justify-between px-2 py-1.5 text-sm", children: [_jsx("span", { className: "text-foreground/80", children: t('user.language', { defaultValue: 'Language' }) }), _jsx(LocaleSwitcher, {})] }), 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' })] })] }))] })] })] })] })] }));
|
|
389
389
|
}
|
|
@@ -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
|
@@ -19,7 +19,8 @@ import { ObjectView as PluginObjectView, ViewTabBar, ManageViewsDialog } from '@
|
|
|
19
19
|
// uses ComponentRegistry.registerLazy so heavy plugins stay code-split).
|
|
20
20
|
// Do NOT add eager `import '@object-ui/plugin-*'` side-effect imports here.
|
|
21
21
|
import { Button, Empty, EmptyTitle, EmptyDescription, NavigationOverlay, } from '@object-ui/components';
|
|
22
|
-
import { Plus, Upload, Table as TableIcon, KanbanSquare, Calendar, LayoutGrid, Activity, GanttChart, MapPin, BarChart3 } from 'lucide-react';
|
|
22
|
+
import { Plus, Upload, Star, StarOff, Table as TableIcon, KanbanSquare, Calendar, LayoutGrid, Activity, GanttChart, MapPin, BarChart3 } from 'lucide-react';
|
|
23
|
+
import { useFavorites } from '../hooks/useFavorites';
|
|
23
24
|
import { getIcon } from '../utils/getIcon';
|
|
24
25
|
import { MetadataPanel, useMetadataInspector } from './MetadataInspector';
|
|
25
26
|
import { ViewConfigPanel } from './ViewConfigPanel';
|
|
@@ -194,11 +195,12 @@ function fromSysViewRecord(sv) {
|
|
|
194
195
|
}
|
|
195
196
|
export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey }) {
|
|
196
197
|
const navigate = useNavigate();
|
|
197
|
-
const { objectName, viewId } = useParams();
|
|
198
|
+
const { appName, objectName, viewId } = useParams();
|
|
198
199
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
199
200
|
const { showDebug } = useMetadataInspector();
|
|
200
201
|
const { t } = useObjectTranslation();
|
|
201
|
-
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
|
+
const { isFavorite, toggleFavorite } = useFavorites();
|
|
202
204
|
// Inline view config panel state (Airtable-style right sidebar)
|
|
203
205
|
const [showViewConfigPanel, setShowViewConfigPanel] = useState(false);
|
|
204
206
|
const [viewConfigPanelMode, setViewConfigPanelMode] = useState('edit');
|
|
@@ -1431,9 +1433,9 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
1431
1433
|
showRecordCount: viewDef.showRecordCount ?? listSchema.showRecordCount,
|
|
1432
1434
|
allowPrinting: viewDef.allowPrinting ?? listSchema.allowPrinting,
|
|
1433
1435
|
virtualScroll: viewDef.virtualScroll ?? listSchema.virtualScroll,
|
|
1434
|
-
emptyState: viewDef.emptyState
|
|
1436
|
+
emptyState: viewEmptyState(objectDef.name, viewDef.name || viewDef.id || '', viewDef.emptyState
|
|
1435
1437
|
?? listSchema.emptyState
|
|
1436
|
-
?? resolveManagedByEmptyState(objectDef?.managedBy),
|
|
1438
|
+
?? resolveManagedByEmptyState(objectDef?.managedBy)),
|
|
1437
1439
|
aria: viewDef.aria ?? listSchema.aria,
|
|
1438
1440
|
tabs: listSchema.tabs,
|
|
1439
1441
|
// Propagate filter/sort as default filters/sort for data flow
|
|
@@ -1581,7 +1583,16 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
1581
1583
|
activeOrganization: activeOrganization
|
|
1582
1584
|
? { id: activeOrganization.id, slug: activeOrganization.slug, name: activeOrganization.name }
|
|
1583
1585
|
: null,
|
|
1584
|
-
}, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, modal: serverActionHandler }, children: [_jsxs("div", { className: "h-full flex flex-col bg-background min-w-0 overflow-hidden", children: [_jsx("div", { className: "hidden sm:block", children: _jsx(PageHeader, { title: _jsxs("span", { className: "inline-flex items-center gap-2", children: [_jsx("span", { className: "truncate", children: objectLabel(objectDef) }), _jsx(ManagedByBadge, { managedBy: objectDef?.managedBy })] }), description: objectDef.description ? objectDesc(objectDef) : undefined, icon: (() => { const I = getIcon(objectDef?.icon); return _jsx(I, { className: "h-4 w-4" }); })(), actions: _jsxs(_Fragment, { children: [
|
|
1586
|
+
}, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, modal: serverActionHandler }, children: [_jsxs("div", { className: "h-full flex flex-col bg-background min-w-0 overflow-hidden", children: [_jsx("div", { className: "hidden sm:block", children: _jsx(PageHeader, { title: _jsxs("span", { className: "inline-flex items-center gap-2", children: [_jsx("span", { className: "truncate", children: objectLabel(objectDef) }), _jsx(ManagedByBadge, { managedBy: objectDef?.managedBy })] }), description: objectDef.description ? objectDesc(objectDef) : undefined, icon: (() => { const I = getIcon(objectDef?.icon); return _jsx(I, { className: "h-4 w-4" }); })(), actions: _jsxs(_Fragment, { children: [objectName && (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => toggleFavorite({
|
|
1587
|
+
id: `object:${objectName}`,
|
|
1588
|
+
label: objectLabel(objectDef),
|
|
1589
|
+
href: `/apps/${appName}/${objectName}`,
|
|
1590
|
+
type: 'object',
|
|
1591
|
+
}), className: "h-8 sm:h-9 px-2", "aria-pressed": isFavorite(`object:${objectName}`), "aria-label": isFavorite(`object:${objectName}`)
|
|
1592
|
+
? t('common.removeFromFavorites', { defaultValue: 'Remove from favorites' })
|
|
1593
|
+
: t('common.addToFavorites', { defaultValue: 'Add to favorites' }), "data-testid": `object-favorite-btn-${objectName}`, children: isFavorite(`object:${objectName}`)
|
|
1594
|
+
? _jsx(Star, { className: "h-4 w-4 fill-amber-400 text-amber-400" })
|
|
1595
|
+
: _jsx(StarOff, { className: "h-4 w-4" }) })), affordances.create && can(objectDef.name, 'create') && (_jsxs(Button, { size: "sm", onClick: actions.create, className: "shadow-none gap-1.5 sm:gap-2 h-8 sm:h-9", children: [_jsx(Plus, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: t('console.objectView.new') })] })), affordances.import && can(objectDef.name, 'create') && (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => setShowImport(true), className: "hidden sm:inline-flex shadow-none gap-1.5 sm:gap-2 h-8 sm:h-9", title: t('console.objectView.importTitle'), "data-testid": "object-view-import-button", children: [_jsx(Upload, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: t('console.objectView.import') })] })), objectDef.actions?.some((a) => a.locations?.includes('list_toolbar')) && (_jsx(SchemaRenderer, { schema: {
|
|
1585
1596
|
type: 'action:bar',
|
|
1586
1597
|
location: 'list_toolbar',
|
|
1587
1598
|
actions: (objectDef.actions || []).map((a) => ({
|
|
@@ -1657,7 +1668,7 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
1657
1668
|
{ title: 'View Configuration', data: activeView },
|
|
1658
1669
|
{ title: 'Object Definition', data: objectDef },
|
|
1659
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)
|
|
1660
|
-
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) => {
|
|
1661
1672
|
const recordId = (record.id || record._id);
|
|
1662
1673
|
return (_jsx(RecordDetailView, { dataSource: dataSource, objects: objects, onEdit: onEdit, objectNameOverride: objectDef.name, recordIdOverride: recordId, embedded: true }));
|
|
1663
1674
|
} }))] }), _jsx(ActionConfirmDialog, { state: confirmState, onOpenChange: (open) => {
|
|
@@ -27,6 +27,8 @@ import { resolveActionParams } from '../utils/resolveActionParams';
|
|
|
27
27
|
import { useRecordBreadcrumbTitle } from '../context/NavigationContext';
|
|
28
28
|
import { useRecordApprovals } from '../hooks/useRecordApprovals';
|
|
29
29
|
import { getRecordDisplayName } from '../utils';
|
|
30
|
+
import { useFavorites } from '../hooks/useFavorites';
|
|
31
|
+
import { useRecentItems } from '../hooks/useRecentItems';
|
|
30
32
|
const FALLBACK_USER = { id: 'current-user', name: 'Demo User' };
|
|
31
33
|
/**
|
|
32
34
|
* Audit field names auto-injected by the framework's `applySystemFields`.
|
|
@@ -58,6 +60,8 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
58
60
|
const navigate = useNavigate();
|
|
59
61
|
const { t } = useObjectTranslation();
|
|
60
62
|
const { objectLabel, viewLabel: _vLabel, sectionLabel, actionLabel, actionConfirm, actionSuccess, fieldLabel, fieldOptionLabel } = useObjectLabel();
|
|
63
|
+
const { isFavorite, toggleFavorite, refreshLabel: refreshFavoriteLabel } = useFavorites();
|
|
64
|
+
const { addRecentItem } = useRecentItems();
|
|
61
65
|
const [isLoading, setIsLoading] = useState(true);
|
|
62
66
|
const [feedItems, setFeedItems] = useState([]);
|
|
63
67
|
const [recordViewers, setRecordViewers] = useState([]);
|
|
@@ -75,6 +79,21 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
75
79
|
// Navigation code passes `record.id || record._id` directly into the URL
|
|
76
80
|
// without adding any prefix, so no stripping is needed.
|
|
77
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]);
|
|
78
97
|
// ─── Page Assignment (Salesforce Lightning-style record Pages) ──────
|
|
79
98
|
// If a PageSchema(pageType='record') is authored for this object, render
|
|
80
99
|
// it via SchemaRenderer (which dispatches to the registered 'record'
|
|
@@ -132,22 +151,63 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
132
151
|
// page subtitle interpolation and record:* renderers depend on this.
|
|
133
152
|
const expandFields = buildExpandFields(objectDef?.fields);
|
|
134
153
|
const params = expandFields.length > 0 ? { $expand: expandFields } : undefined;
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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);
|
|
147
180
|
return () => {
|
|
148
181
|
cancelled = true;
|
|
182
|
+
window.removeEventListener('objectui:record-changed', onChanged);
|
|
149
183
|
};
|
|
150
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]);
|
|
151
211
|
// ─── Action Provider Handlers ───────────────────────────────────────
|
|
152
212
|
// Confirm dialog state (promise-based)
|
|
153
213
|
const [confirmState, setConfirmState] = useState({ open: false, message: '' });
|
|
@@ -1105,14 +1165,23 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1105
1165
|
title: document.title,
|
|
1106
1166
|
url: window.location.href,
|
|
1107
1167
|
});
|
|
1108
|
-
|
|
1109
|
-
else {
|
|
1110
|
-
await navigator.clipboard.writeText(window.location.href);
|
|
1111
|
-
toast.success(t('detail.linkCopied', { defaultValue: 'Link copied' }));
|
|
1168
|
+
return;
|
|
1112
1169
|
}
|
|
1113
1170
|
}
|
|
1114
1171
|
catch {
|
|
1115
|
-
// 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}` : ''));
|
|
1116
1185
|
}
|
|
1117
1186
|
},
|
|
1118
1187
|
});
|
|
@@ -1183,7 +1252,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1183
1252
|
history: synthHistory,
|
|
1184
1253
|
...(assignedSlots ? { slots: assignedSlots } : {}),
|
|
1185
1254
|
});
|
|
1186
|
-
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, 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: {
|
|
1187
1256
|
position: 'bottom',
|
|
1188
1257
|
collapsible: false,
|
|
1189
1258
|
feed: {
|
|
@@ -1199,7 +1268,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1199
1268
|
setParamState(s => ({ ...s, open: false }));
|
|
1200
1269
|
} })] }));
|
|
1201
1270
|
}
|
|
1202
|
-
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 }), 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) => {
|
|
1203
1272
|
if (!record || typeof record !== 'object')
|
|
1204
1273
|
return;
|
|
1205
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,34 +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/
|
|
40
|
-
"@object-ui/
|
|
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"
|
|
41
42
|
},
|
|
42
43
|
"peerDependencies": {
|
|
43
44
|
"react": "^18.0.0 || ^19.0.0",
|
|
44
45
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
45
46
|
"react-router-dom": "^6.0.0 || ^7.0.0",
|
|
46
|
-
"@object-ui/plugin-calendar": "^5.0.
|
|
47
|
-
"@object-ui/plugin-charts": "^5.0.
|
|
48
|
-
"@object-ui/plugin-chatbot": "^5.0.
|
|
49
|
-
"@object-ui/plugin-dashboard": "^5.0.
|
|
50
|
-
"@object-ui/plugin-designer": "^5.0.
|
|
51
|
-
"@object-ui/plugin-detail": "^5.0.
|
|
52
|
-
"@object-ui/plugin-form": "^5.0.
|
|
53
|
-
"@object-ui/plugin-grid": "^5.0.
|
|
54
|
-
"@object-ui/plugin-kanban": "^5.0.
|
|
55
|
-
"@object-ui/plugin-list": "^5.0.
|
|
56
|
-
"@object-ui/plugin-report": "^5.0.
|
|
57
|
-
"@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"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
60
61
|
"@types/node": "^25.9.0",
|