@object-ui/app-shell 5.0.0 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -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 +2 -2
- package/dist/console/home/StarredApps.js +2 -2
- package/dist/console/organizations/manage/SettingsPage.js +34 -4
- package/dist/context/FavoritesProvider.d.ts +1 -1
- package/dist/layout/AppHeader.js +2 -2
- package/dist/views/ObjectView.js +14 -3
- package/dist/views/RecordDetailView.js +24 -4
- package/package.json +25 -24
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @object-ui/app-shell — Changelog
|
|
2
2
|
|
|
3
|
+
## 5.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- cb4879e: form
|
|
8
|
+
- @object-ui/types@5.0.1
|
|
9
|
+
- @object-ui/core@5.0.1
|
|
10
|
+
- @object-ui/i18n@5.0.1
|
|
11
|
+
- @object-ui/react@5.0.1
|
|
12
|
+
- @object-ui/components@5.0.1
|
|
13
|
+
- @object-ui/fields@5.0.1
|
|
14
|
+
- @object-ui/layout@5.0.1
|
|
15
|
+
- @object-ui/data-objectstack@5.0.1
|
|
16
|
+
- @object-ui/auth@5.0.1
|
|
17
|
+
- @object-ui/permissions@5.0.1
|
|
18
|
+
- @object-ui/collaboration@5.0.1
|
|
19
|
+
- @object-ui/providers@5.0.1
|
|
20
|
+
|
|
3
21
|
## 5.0.0
|
|
4
22
|
|
|
5
23
|
### 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
|
}
|
|
@@ -29,11 +29,11 @@ export function RecentApps({ items }) {
|
|
|
29
29
|
defaultValue: capitalizeFirst(item.type),
|
|
30
30
|
});
|
|
31
31
|
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-
|
|
32
|
+
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
33
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
34
34
|
e.preventDefault();
|
|
35
35
|
navigate(item.href);
|
|
36
36
|
}
|
|
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-
|
|
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-[opacity,transform] duration-200 group-hover:opacity-100 group-hover:translate-x-0" })] }) }) }, item.id));
|
|
38
38
|
}) })] }));
|
|
39
39
|
}
|
|
@@ -26,11 +26,11 @@ export function StarredApps({ items }) {
|
|
|
26
26
|
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
27
|
const Icon = getIcon(item.type);
|
|
28
28
|
const tone = TYPE_TONES[item.type] || TYPE_TONES.object;
|
|
29
|
-
return (_jsx(Card, { className: "group cursor-pointer border border-border/70 bg-card/80 backdrop-blur-sm transition-
|
|
29
|
+
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
30
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
31
31
|
e.preventDefault();
|
|
32
32
|
navigate(item.href);
|
|
33
33
|
}
|
|
34
|
-
}, children: _jsx(CardContent, { className: "p-3.5", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: cn('inline-flex h-10 w-10 items-center justify-center rounded-lg ring-1 shrink-0', tone), children: _jsx(Icon, { className: "h-5 w-5" }) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("h3", { className: "font-medium text-sm truncate", children: item.label }), _jsx("p", { className: "text-xs text-muted-foreground", children: capitalizeFirst(item.type) })] }), _jsx(ArrowUpRight, { className: "h-4 w-4 text-muted-foreground opacity-0 -translate-x-1 transition-
|
|
34
|
+
}, children: _jsx(CardContent, { className: "p-3.5", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: cn('inline-flex h-10 w-10 items-center justify-center rounded-lg ring-1 shrink-0', tone), children: _jsx(Icon, { className: "h-5 w-5" }) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("h3", { className: "font-medium text-sm truncate", children: item.label }), _jsx("p", { className: "text-xs text-muted-foreground", children: capitalizeFirst(item.type) })] }), _jsx(ArrowUpRight, { className: "h-4 w-4 text-muted-foreground opacity-0 -translate-x-1 transition-[opacity,transform] duration-200 group-hover:opacity-100 group-hover:translate-x-0" })] }) }) }, item.id));
|
|
35
35
|
}) })] }));
|
|
36
36
|
}
|
|
@@ -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
|
}
|
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
|
}
|
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
202
|
const { objectLabel, objectDescription: objectDesc, viewLabel, 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');
|
|
@@ -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) => ({
|
|
@@ -9,13 +9,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
9
9
|
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
10
10
|
import { useParams, useNavigate } from 'react-router-dom';
|
|
11
11
|
import { DetailView, RecordChatterPanel, buildDefaultPageSchema } from '@object-ui/plugin-detail';
|
|
12
|
-
import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
|
|
12
|
+
import { Empty, EmptyTitle, EmptyDescription, Button } from '@object-ui/components';
|
|
13
13
|
import { PresenceAvatars } from '@object-ui/collaboration';
|
|
14
14
|
import { useAuth, createAuthenticatedFetch } from '@object-ui/auth';
|
|
15
15
|
import { ActionProvider, useObjectTranslation, useObjectLabel, usePageAssignment, RecordContextProvider, SchemaRenderer, DiscussionContextProvider, HighlightFieldsProvider } from '@object-ui/react';
|
|
16
16
|
import { buildExpandFields } from '@object-ui/core';
|
|
17
17
|
import { toast } from 'sonner';
|
|
18
|
-
import { Database, Users } from 'lucide-react';
|
|
18
|
+
import { Database, Users, Star, StarOff } from 'lucide-react';
|
|
19
19
|
import { MetadataPanel, useMetadataInspector } from './MetadataInspector';
|
|
20
20
|
import { SkeletonDetail } from '../skeletons';
|
|
21
21
|
import { ManagedByBadge } from '../components/ManagedByBadge';
|
|
@@ -27,6 +27,7 @@ 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';
|
|
30
31
|
const FALLBACK_USER = { id: 'current-user', name: 'Demo User' };
|
|
31
32
|
/**
|
|
32
33
|
* Audit field names auto-injected by the framework's `applySystemFields`.
|
|
@@ -58,6 +59,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
58
59
|
const navigate = useNavigate();
|
|
59
60
|
const { t } = useObjectTranslation();
|
|
60
61
|
const { objectLabel, viewLabel: _vLabel, sectionLabel, actionLabel, actionConfirm, actionSuccess, fieldLabel, fieldOptionLabel } = useObjectLabel();
|
|
62
|
+
const { isFavorite, toggleFavorite } = useFavorites();
|
|
61
63
|
const [isLoading, setIsLoading] = useState(true);
|
|
62
64
|
const [feedItems, setFeedItems] = useState([]);
|
|
63
65
|
const [recordViewers, setRecordViewers] = useState([]);
|
|
@@ -1183,7 +1185,16 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1183
1185
|
history: synthHistory,
|
|
1184
1186
|
...(assignedSlots ? { slots: assignedSlots } : {}),
|
|
1185
1187
|
});
|
|
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: [
|
|
1188
|
+
return (_jsxs("div", { className: "h-full bg-background overflow-hidden flex flex-col relative", children: [_jsxs("div", { className: "absolute top-2 sm:top-4 right-2 sm:right-4 z-50 flex items-center gap-2", children: [objectName && pureRecordId && (_jsx(Button, { size: "sm", variant: "ghost", className: "h-8 w-8 p-0", onClick: () => toggleFavorite({
|
|
1189
|
+
id: `record:${objectName}:${pureRecordId}`,
|
|
1190
|
+
label: recordTitle || pureRecordId || '',
|
|
1191
|
+
href: `/apps/${appName}/${objectName}/record/${pureRecordId}`,
|
|
1192
|
+
type: 'record',
|
|
1193
|
+
}), "aria-pressed": isFavorite(`record:${objectName}:${pureRecordId}`), "aria-label": isFavorite(`record:${objectName}:${pureRecordId}`)
|
|
1194
|
+
? t('common.removeFromFavorites', { defaultValue: 'Remove from favorites' })
|
|
1195
|
+
: t('common.addToFavorites', { defaultValue: 'Add to favorites' }), "data-testid": `record-favorite-btn-${pureRecordId}`, children: isFavorite(`record:${objectName}:${pureRecordId}`)
|
|
1196
|
+
? _jsx(Star, { className: "h-4 w-4 fill-amber-400 text-amber-400" })
|
|
1197
|
+
: _jsx(StarOff, { className: "h-4 w-4" }) })), _jsx(ManagedByBadge, { managedBy: objectDef?.managedBy }), recordViewers.length > 0 && (_jsxs("div", { className: "flex items-center gap-1.5", title: t('recordDetail.viewersTooltip'), children: [_jsx(Users, { className: "h-3.5 w-3.5 text-muted-foreground" }), _jsx(PresenceAvatars, { users: recordViewers, size: "sm", maxVisible: 4, showStatus: true })] }))] }), _jsx(RecordContextProvider, { objectName: objectName, recordId: pureRecordId, data: pageRecord, objectSchema: objectDef, dataSource: dataSource, embedded: embedded, headerSystemActions: synthSystemActions, children: _jsx(HighlightFieldsProvider, { children: _jsx(DiscussionContextProvider, { items: feedItems, onAddComment: handleAddComment, onAddReply: handleAddReply, onToggleReaction: handleToggleReaction, children: _jsx(ActionProvider, { context: { record: pageRecord || {}, objectName, user: currentUser }, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, modal: serverActionHandler, approval: approvalHandler }, children: _jsxs("div", { className: "flex-1 overflow-hidden flex flex-row", children: [_jsxs("div", { className: "flex-1 overflow-auto p-3 sm:p-4 lg:p-6 scroll-pb-48", children: [_jsx(SchemaRenderer, { schema: renderedPage }), showAutoDiscussion && (_jsx("div", { className: "mt-6", children: _jsx(RecordChatterPanel, { config: {
|
|
1187
1198
|
position: 'bottom',
|
|
1188
1199
|
collapsible: false,
|
|
1189
1200
|
feed: {
|
|
@@ -1199,7 +1210,16 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1199
1210
|
setParamState(s => ({ ...s, open: false }));
|
|
1200
1211
|
} })] }));
|
|
1201
1212
|
}
|
|
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: [
|
|
1213
|
+
return (_jsxs("div", { className: "h-full bg-background overflow-hidden flex flex-col relative", children: [_jsxs("div", { className: "absolute top-2 sm:top-4 right-2 sm:right-4 z-50 flex items-center gap-2", children: [objectName && pureRecordId && (_jsx(Button, { size: "sm", variant: "ghost", className: "h-8 w-8 p-0", onClick: () => toggleFavorite({
|
|
1214
|
+
id: `record:${objectName}:${pureRecordId}`,
|
|
1215
|
+
label: recordTitle || pureRecordId || '',
|
|
1216
|
+
href: `/apps/${appName}/${objectName}/record/${pureRecordId}`,
|
|
1217
|
+
type: 'record',
|
|
1218
|
+
}), "aria-pressed": isFavorite(`record:${objectName}:${pureRecordId}`), "aria-label": isFavorite(`record:${objectName}:${pureRecordId}`)
|
|
1219
|
+
? t('common.removeFromFavorites', { defaultValue: 'Remove from favorites' })
|
|
1220
|
+
: t('common.addToFavorites', { defaultValue: 'Add to favorites' }), "data-testid": `record-favorite-btn-${pureRecordId}`, children: isFavorite(`record:${objectName}:${pureRecordId}`)
|
|
1221
|
+
? _jsx(Star, { className: "h-4 w-4 fill-amber-400 text-amber-400" })
|
|
1222
|
+
: _jsx(StarOff, { className: "h-4 w-4" }) })), _jsx(ManagedByBadge, { managedBy: objectDef?.managedBy }), recordViewers.length > 0 && (_jsxs("div", { className: "flex items-center gap-1.5", title: t('recordDetail.viewersTooltip'), children: [_jsx(Users, { className: "h-3.5 w-3.5 text-muted-foreground" }), _jsx(PresenceAvatars, { users: recordViewers, size: "sm", maxVisible: 4, showStatus: true })] }))] }), _jsxs("div", { className: "flex-1 overflow-hidden flex flex-row", children: [_jsx("div", { className: "flex-1 overflow-auto p-3 sm:p-4 lg:p-6 scroll-pb-48", children: _jsx(ActionProvider, { context: { record: {}, objectName, user: currentUser }, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, modal: serverActionHandler, approval: approvalHandler }, children: _jsx(DetailView, { schema: detailSchema, dataSource: dataSource, objectLabel: objectLabel({ name: objectDef.name, label: objectDef.label }), onDataLoaded: (record) => {
|
|
1203
1223
|
if (!record || typeof record !== 'object')
|
|
1204
1224
|
return;
|
|
1205
1225
|
// Resolve the same way DetailView's header does, so the
|
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.1",
|
|
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.1",
|
|
31
|
+
"@object-ui/collaboration": "5.0.1",
|
|
32
|
+
"@object-ui/components": "5.0.1",
|
|
33
|
+
"@object-ui/core": "5.0.1",
|
|
34
|
+
"@object-ui/data-objectstack": "5.0.1",
|
|
35
|
+
"@object-ui/fields": "5.0.1",
|
|
36
|
+
"@object-ui/i18n": "5.0.1",
|
|
37
|
+
"@object-ui/layout": "5.0.1",
|
|
38
|
+
"@object-ui/permissions": "5.0.1",
|
|
39
|
+
"@object-ui/providers": "5.0.1",
|
|
40
|
+
"@object-ui/react": "5.0.1",
|
|
41
|
+
"@object-ui/types": "5.0.1"
|
|
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.1",
|
|
48
|
+
"@object-ui/plugin-charts": "^5.0.1",
|
|
49
|
+
"@object-ui/plugin-chatbot": "^5.0.1",
|
|
50
|
+
"@object-ui/plugin-dashboard": "^5.0.1",
|
|
51
|
+
"@object-ui/plugin-designer": "^5.0.1",
|
|
52
|
+
"@object-ui/plugin-detail": "^5.0.1",
|
|
53
|
+
"@object-ui/plugin-form": "^5.0.1",
|
|
54
|
+
"@object-ui/plugin-grid": "^5.0.1",
|
|
55
|
+
"@object-ui/plugin-kanban": "^5.0.1",
|
|
56
|
+
"@object-ui/plugin-list": "^5.0.1",
|
|
57
|
+
"@object-ui/plugin-report": "^5.0.1",
|
|
58
|
+
"@object-ui/plugin-view": "^5.0.1"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
60
61
|
"@types/node": "^25.9.0",
|