@object-ui/app-shell 3.3.1 → 3.3.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 +28 -0
- package/dist/chrome/CommandPalette.js +2 -16
- package/dist/layout/AppHeader.js +1 -1
- package/dist/layout/AppSidebar.js +7 -22
- package/dist/layout/AppSwitcher.js +1 -16
- package/dist/layout/ConsoleFloatingChatbot.d.ts +10 -0
- package/dist/layout/ConsoleFloatingChatbot.js +27 -0
- package/dist/layout/ConsoleLayout.d.ts +1 -0
- package/dist/layout/ConsoleLayout.js +6 -28
- package/dist/layout/UnifiedSidebar.d.ts +5 -0
- package/dist/layout/UnifiedSidebar.js +1 -25
- package/dist/utils/getIcon.d.ts +16 -2
- package/dist/utils/getIcon.js +75 -12
- package/dist/views/ObjectView.d.ts +0 -3
- package/dist/views/ObjectView.js +16 -17
- package/dist/views/ReportView.js +4 -3
- package/package.json +25 -25
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# @object-ui/app-shell — Changelog
|
|
2
2
|
|
|
3
|
+
## 3.3.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- @object-ui/types@3.3.2
|
|
8
|
+
- @object-ui/core@3.3.2
|
|
9
|
+
- @object-ui/i18n@3.3.2
|
|
10
|
+
- @object-ui/react@3.3.2
|
|
11
|
+
- @object-ui/components@3.3.2
|
|
12
|
+
- @object-ui/fields@3.3.2
|
|
13
|
+
- @object-ui/layout@3.3.2
|
|
14
|
+
- @object-ui/data-objectstack@3.3.2
|
|
15
|
+
- @object-ui/auth@3.3.2
|
|
16
|
+
- @object-ui/permissions@3.3.2
|
|
17
|
+
- @object-ui/plugin-calendar@3.3.2
|
|
18
|
+
- @object-ui/plugin-charts@3.3.2
|
|
19
|
+
- @object-ui/plugin-chatbot@3.3.2
|
|
20
|
+
- @object-ui/plugin-dashboard@3.3.2
|
|
21
|
+
- @object-ui/plugin-designer@3.3.2
|
|
22
|
+
- @object-ui/plugin-detail@3.3.2
|
|
23
|
+
- @object-ui/plugin-form@3.3.2
|
|
24
|
+
- @object-ui/plugin-grid@3.3.2
|
|
25
|
+
- @object-ui/plugin-kanban@3.3.2
|
|
26
|
+
- @object-ui/plugin-list@3.3.2
|
|
27
|
+
- @object-ui/plugin-report@3.3.2
|
|
28
|
+
- @object-ui/plugin-view@3.3.2
|
|
29
|
+
- @object-ui/collaboration@3.3.2
|
|
30
|
+
|
|
3
31
|
## 3.3.1
|
|
4
32
|
|
|
5
33
|
### Patch Changes
|
|
@@ -9,27 +9,13 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
9
9
|
*/
|
|
10
10
|
import { useEffect, useState, useCallback } from 'react';
|
|
11
11
|
import { useNavigate, useParams } from 'react-router-dom';
|
|
12
|
-
import * as LucideIcons from 'lucide-react';
|
|
13
12
|
import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, } from '@object-ui/components';
|
|
14
|
-
import { LayoutDashboard, FileText, BarChart3,
|
|
13
|
+
import { LayoutDashboard, FileText, BarChart3, Moon, Sun, Monitor, Search, Plus, } from 'lucide-react';
|
|
15
14
|
import { useTheme } from './ThemeProvider';
|
|
16
15
|
import { useExpressionContext, evaluateVisibility } from '../providers/ExpressionProvider';
|
|
17
16
|
import { useObjectTranslation } from '@object-ui/i18n';
|
|
18
17
|
import { resolveI18nLabel } from '../utils';
|
|
19
|
-
|
|
20
|
-
function getIcon(name) {
|
|
21
|
-
if (!name)
|
|
22
|
-
return Database;
|
|
23
|
-
if (LucideIcons[name])
|
|
24
|
-
return LucideIcons[name];
|
|
25
|
-
const pascal = name
|
|
26
|
-
.split('-')
|
|
27
|
-
.map(p => p.charAt(0).toUpperCase() + p.slice(1))
|
|
28
|
-
.join('');
|
|
29
|
-
if (LucideIcons[pascal])
|
|
30
|
-
return LucideIcons[pascal];
|
|
31
|
-
return Database;
|
|
32
|
-
}
|
|
18
|
+
import { getIcon } from '../utils/getIcon';
|
|
33
19
|
export function CommandPalette({ apps, activeApp, objects: _objects, onAppChange }) {
|
|
34
20
|
const [open, setOpen] = useState(false);
|
|
35
21
|
const navigate = useNavigate();
|
package/dist/layout/AppHeader.js
CHANGED
|
@@ -178,5 +178,5 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
|
|
|
178
178
|
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: "flex items-center justify-center h-7 w-7 shrink-0 rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors", 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" }), activeAppName && onAppChange ? (_jsxs(_Fragment, { children: [_jsx(PathSep, {}), _jsx(AppSwitcher, { activeAppName: activeAppName, onAppChange: onAppChange })] })) : appName ? (_jsxs(_Fragment, { children: [_jsx(PathSep, {}), _jsx("span", { className: "text-sm font-medium text-foreground/80 px-1.5", children: appName })] })) : null, extraSegments.map((seg, i) => {
|
|
179
179
|
const isLast = i === extraSegments.length - 1;
|
|
180
180
|
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));
|
|
181
|
-
}), _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("div", { className: "hidden sm:flex shrink-0", children: _jsx(ActivityFeed, { activities: activeActivities }) }), _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: [_jsx("div", { className: "hidden sm:flex shrink-0", children: _jsx(ModeToggle, {}) }), _jsx("div", { className: "hidden sm:flex shrink-0", children: _jsx(LocaleSwitcher, {}) }), _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
|
|
181
|
+
}), _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("div", { className: "hidden sm:flex shrink-0", children: _jsx(ActivityFeed, { activities: activeActivities }) }), _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: [_jsx("div", { className: "hidden sm:flex shrink-0", children: _jsx(ModeToggle, {}) }), _jsx("div", { className: "hidden sm:flex shrink-0", children: _jsx(LocaleSwitcher, {}) }), _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, {}), _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' })] })] })] })] })] })] }));
|
|
182
182
|
}
|
|
@@ -11,7 +11,8 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
11
11
|
*/
|
|
12
12
|
import * as React from 'react';
|
|
13
13
|
import { useNavigate, Link } from 'react-router-dom';
|
|
14
|
-
import
|
|
14
|
+
import { Layers } from 'lucide-react';
|
|
15
|
+
import { getIcon as resolveIcon } from '../utils/getIcon';
|
|
15
16
|
import { Sidebar, SidebarHeader, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupLabel, SidebarGroupContent, SidebarMenu, SidebarMenuItem, SidebarMenuButton, SidebarMenuAction, SidebarInput, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuGroup, Avatar, AvatarImage, AvatarFallback, useSidebar, } from '@object-ui/components';
|
|
16
17
|
import { ChevronsUpDown, Plus, Settings, LogOut, Database, Clock, Star, StarOff, Search, Pencil, ChevronRight, Home, } from 'lucide-react';
|
|
17
18
|
import { NavigationRenderer } from '@object-ui/layout';
|
|
@@ -84,26 +85,10 @@ function useNavOrder(appName) {
|
|
|
84
85
|
}
|
|
85
86
|
/**
|
|
86
87
|
* Resolve a Lucide icon component by name string.
|
|
87
|
-
*
|
|
88
|
+
* Delegates to the shared lazy `getIcon` helper which returns a wrapper
|
|
89
|
+
* around lucide-react `DynamicIcon` so individual icons are code-split.
|
|
88
90
|
*/
|
|
89
|
-
|
|
90
|
-
if (!name)
|
|
91
|
-
return LucideIcons.Database;
|
|
92
|
-
// 1. Direct match (PascalCase or camelCase)
|
|
93
|
-
if (LucideIcons[name]) {
|
|
94
|
-
return LucideIcons[name];
|
|
95
|
-
}
|
|
96
|
-
// 2. Try converting kebab-case to PascalCase (e.g. "shopping-cart" -> "ShoppingCart")
|
|
97
|
-
const pascalName = name
|
|
98
|
-
.split('-')
|
|
99
|
-
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
100
|
-
.join('');
|
|
101
|
-
if (LucideIcons[pascalName]) {
|
|
102
|
-
return LucideIcons[pascalName];
|
|
103
|
-
}
|
|
104
|
-
// 3. Fallback
|
|
105
|
-
return LucideIcons.Database;
|
|
106
|
-
}
|
|
91
|
+
const getIcon = resolveIcon;
|
|
107
92
|
export function AppSidebar({ activeAppName, onAppChange }) {
|
|
108
93
|
const { isMobile } = useSidebar();
|
|
109
94
|
const { user, signOut } = useAuth();
|
|
@@ -192,14 +177,14 @@ export function AppSidebar({ activeAppName, onAppChange }) {
|
|
|
192
177
|
{ id: 'sys-roles', label: 'Roles', type: 'url', url: '/apps/setup/system/roles', icon: 'shield' },
|
|
193
178
|
{ id: 'sys-create-app', label: 'Create App', type: 'url', url: '/create-app', icon: 'plus' },
|
|
194
179
|
], []);
|
|
195
|
-
return (_jsxs(_Fragment, { children: [_jsxs(Sidebar, { collapsible: "icon", children: [_jsx(SidebarHeader, { children: _jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: activeApp ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(SidebarMenuButton, { size: "lg", className: "data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground", children: [_jsx("div", { className: "flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground", style: primaryColor ? { backgroundColor: primaryColor } : undefined, children: logo ? (_jsx("img", { src: logo, alt: resolveI18nLabel(activeApp.label, t), className: "size-6 object-contain" })) : (React.createElement(getIcon(activeApp.icon), { className: "size-4" })) }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: resolveI18nLabel(activeApp.label, t) }), _jsx("span", { className: "truncate text-xs", children: resolveI18nLabel(activeApp.description, t) || `${activeApps.length} Apps Available` })] }), _jsx(ChevronsUpDown, { className: "ml-auto" })] }) }), _jsxs(DropdownMenuContent, { className: "w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg", align: "start", side: isMobile ? "bottom" : "right", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "text-xs text-muted-foreground", children: "Switch Application" }), activeApps.map((app) => (_jsxs(DropdownMenuItem, { onClick: () => onAppChange(app.name), className: "gap-2 p-2", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-sm border", children: app.icon ? React.createElement(getIcon(app.icon), { className: "size-3" }) : _jsx(Database, { className: "size-3" }) }), resolveI18nLabel(app.label, t), activeApp.name === app.name && _jsx("span", { className: "ml-auto text-xs", children: "\u2713" })] }, app.name))), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate('/home'), "data-testid": "home-link-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Home, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: "Home" })] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate(`/apps/${activeAppName}/create-app`), "data-testid": "add-app-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Plus, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: "Add App" })] }), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate(`/apps/${activeAppName}/edit-app/${activeAppName}`), "data-testid": "edit-app-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Pencil, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: "Edit App" })] }), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate('/apps/setup/system/apps'), "data-testid": "manage-all-apps-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Settings, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: "Manage All Apps" })] })] })] })) : (_jsxs(SidebarMenuButton, { size: "lg", onClick: () => navigate('/apps/setup'), "data-testid": "system-sidebar-header", children: [_jsx("div", { className: "flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground", children: _jsx(Settings, { className: "size-4" }) }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: "System Console" }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: "No apps configured" })] })] })) }) }) }), _jsx(SidebarContent, { children: activeApp ? (_jsxs(_Fragment, { children: [areas.length > 1 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(
|
|
180
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Sidebar, { collapsible: "icon", children: [_jsx(SidebarHeader, { children: _jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: activeApp ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(SidebarMenuButton, { size: "lg", className: "data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground", children: [_jsx("div", { className: "flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground", style: primaryColor ? { backgroundColor: primaryColor } : undefined, children: logo ? (_jsx("img", { src: logo, alt: resolveI18nLabel(activeApp.label, t), className: "size-6 object-contain" })) : (React.createElement(getIcon(activeApp.icon), { className: "size-4" })) }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: resolveI18nLabel(activeApp.label, t) }), _jsx("span", { className: "truncate text-xs", children: resolveI18nLabel(activeApp.description, t) || `${activeApps.length} Apps Available` })] }), _jsx(ChevronsUpDown, { className: "ml-auto" })] }) }), _jsxs(DropdownMenuContent, { className: "w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg", align: "start", side: isMobile ? "bottom" : "right", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "text-xs text-muted-foreground", children: "Switch Application" }), activeApps.map((app) => (_jsxs(DropdownMenuItem, { onClick: () => onAppChange(app.name), className: "gap-2 p-2", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-sm border", children: app.icon ? React.createElement(getIcon(app.icon), { className: "size-3" }) : _jsx(Database, { className: "size-3" }) }), resolveI18nLabel(app.label, t), activeApp.name === app.name && _jsx("span", { className: "ml-auto text-xs", children: "\u2713" })] }, app.name))), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate('/home'), "data-testid": "home-link-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Home, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: "Home" })] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate(`/apps/${activeAppName}/create-app`), "data-testid": "add-app-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Plus, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: "Add App" })] }), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate(`/apps/${activeAppName}/edit-app/${activeAppName}`), "data-testid": "edit-app-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Pencil, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: "Edit App" })] }), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate('/apps/setup/system/apps'), "data-testid": "manage-all-apps-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Settings, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: "Manage All Apps" })] })] })] })) : (_jsxs(SidebarMenuButton, { size: "lg", onClick: () => navigate('/apps/setup'), "data-testid": "system-sidebar-header", children: [_jsx("div", { className: "flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground", children: _jsx(Settings, { className: "size-4" }) }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: "System Console" }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: "No apps configured" })] })] })) }) }) }), _jsx(SidebarContent, { children: activeApp ? (_jsxs(_Fragment, { children: [areas.length > 1 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Layers, { className: "h-3.5 w-3.5" }), "Area"] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: areas.map((area) => {
|
|
196
181
|
const AreaIcon = getIcon(area.icon);
|
|
197
182
|
const isActiveArea = area.id === activeAreaId;
|
|
198
183
|
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));
|
|
199
184
|
}) }) })] })), _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, 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) => {
|
|
200
185
|
const NavIcon = getIcon(item.icon);
|
|
201
186
|
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));
|
|
202
|
-
}) }) })] })) }), _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
|
|
187
|
+
}) }) })] })) }), _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" }), "Settings"] }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "text-destructive focus:text-destructive", onClick: () => signOut(), children: [_jsx(LogOut, { className: "mr-2 h-4 w-4" }), "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: (activeApp ? resolvedNavigation : systemFallbackNavigation).filter((n) => n.type !== 'group').slice(0, 5).map((item) => {
|
|
203
188
|
const NavIcon = getIcon(item.icon);
|
|
204
189
|
const baseUrl = activeApp ? `/apps/${activeAppName}` : '';
|
|
205
190
|
let href = item.url || '#';
|
|
@@ -1,25 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import * as LucideIcons from 'lucide-react';
|
|
3
2
|
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, } from '@object-ui/components';
|
|
4
3
|
import { ChevronDown, Check } from 'lucide-react';
|
|
5
4
|
import { useMetadata } from '../providers/MetadataProvider';
|
|
6
5
|
import { resolveI18nLabel } from '../utils';
|
|
7
6
|
import { useObjectTranslation, useObjectLabel } from '@object-ui/i18n';
|
|
8
|
-
|
|
9
|
-
if (!name)
|
|
10
|
-
return LucideIcons.Database;
|
|
11
|
-
const lookup = (key) => {
|
|
12
|
-
try {
|
|
13
|
-
const icon = LucideIcons[key];
|
|
14
|
-
return typeof icon === 'function' ? icon : undefined;
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
return undefined;
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
const pascalName = name.split(/[-_]/).map(p => p.charAt(0).toUpperCase() + p.slice(1)).join('');
|
|
21
|
-
return lookup(name) ?? lookup(pascalName) ?? LucideIcons.Database;
|
|
22
|
-
}
|
|
7
|
+
import { getIcon } from '../utils/getIcon';
|
|
23
8
|
export function AppSwitcher({ activeAppName, onAppChange }) {
|
|
24
9
|
const { apps: metadataApps } = useMetadata();
|
|
25
10
|
const { t } = useObjectTranslation();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface ConsoleObject {
|
|
2
|
+
name: string;
|
|
3
|
+
label?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface ConsoleFloatingChatbotProps {
|
|
6
|
+
appLabel: string;
|
|
7
|
+
objects: ConsoleObject[];
|
|
8
|
+
}
|
|
9
|
+
export default function ConsoleFloatingChatbot({ appLabel, objects }: ConsoleFloatingChatbotProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { FloatingChatbot, useObjectChat } from '@object-ui/plugin-chatbot';
|
|
3
|
+
export default function ConsoleFloatingChatbot({ appLabel, objects }) {
|
|
4
|
+
const objectNames = objects.map((o) => o.label || o.name).join(', ');
|
|
5
|
+
const { messages, isLoading, error, sendMessage, stop, reload, clear, } = useObjectChat({
|
|
6
|
+
initialMessages: [
|
|
7
|
+
{
|
|
8
|
+
id: 'welcome',
|
|
9
|
+
role: 'assistant',
|
|
10
|
+
content: `Hello! I'm your **${appLabel}** assistant. How can I help you today?`,
|
|
11
|
+
},
|
|
12
|
+
],
|
|
13
|
+
autoResponse: true,
|
|
14
|
+
autoResponseText: objectNames
|
|
15
|
+
? `I can help you work with ${objectNames}. What would you like to do?`
|
|
16
|
+
: 'Thanks for your message! I\'m here to help you navigate and manage your data.',
|
|
17
|
+
autoResponseDelay: 800,
|
|
18
|
+
});
|
|
19
|
+
return (_jsx(FloatingChatbot, { floatingConfig: {
|
|
20
|
+
position: 'bottom-right',
|
|
21
|
+
defaultOpen: false,
|
|
22
|
+
panelWidth: 400,
|
|
23
|
+
panelHeight: 520,
|
|
24
|
+
title: `${appLabel} Assistant`,
|
|
25
|
+
triggerSize: 56,
|
|
26
|
+
}, messages: messages, placeholder: "Ask anything...", onSendMessage: (content) => sendMessage(content), onClear: clear, onStop: isLoading ? stop : undefined, onReload: reload, isLoading: isLoading, error: error, enableMarkdown: true }));
|
|
27
|
+
}
|
|
@@ -17,5 +17,6 @@ interface ConsoleLayoutProps {
|
|
|
17
17
|
objects: any[];
|
|
18
18
|
connectionState?: ConnectionState;
|
|
19
19
|
}
|
|
20
|
+
/** Floating chatbot wired with useObjectChat for demo auto-response */
|
|
20
21
|
export declare function ConsoleLayout({ children, activeAppName, activeApp, onAppChange, objects, connectionState }: ConsoleLayoutProps): import("react/jsx-runtime").JSX.Element;
|
|
21
22
|
export {};
|
|
@@ -8,10 +8,12 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
8
8
|
* Sets navigation context to 'app' for app-specific routes.
|
|
9
9
|
* @module
|
|
10
10
|
*/
|
|
11
|
-
import { useEffect } from 'react';
|
|
11
|
+
import { useEffect, Suspense, lazy } from 'react';
|
|
12
12
|
import { AppShell } from '@object-ui/layout';
|
|
13
|
-
import { FloatingChatbot, useObjectChat } from '@object-ui/plugin-chatbot';
|
|
14
13
|
import { useDiscovery } from '@object-ui/react';
|
|
14
|
+
// Lazy-load the chatbot so its heavy markdown deps (~150 KB) stay out of
|
|
15
|
+
// the initial paint until the AI assistant is actually enabled.
|
|
16
|
+
const ConsoleFloatingChatbot = lazy(() => import('./ConsoleFloatingChatbot'));
|
|
15
17
|
import { UnifiedSidebar } from './UnifiedSidebar';
|
|
16
18
|
import { AppHeader } from './AppHeader';
|
|
17
19
|
import { useResponsiveSidebar } from '../hooks/useResponsiveSidebar';
|
|
@@ -23,31 +25,7 @@ function ConsoleLayoutInner({ children }) {
|
|
|
23
25
|
return _jsx(_Fragment, { children: children });
|
|
24
26
|
}
|
|
25
27
|
/** Floating chatbot wired with useObjectChat for demo auto-response */
|
|
26
|
-
|
|
27
|
-
const objectNames = objects.map((o) => o.label || o.name).join(', ');
|
|
28
|
-
const { messages, isLoading, error, sendMessage, stop, reload, clear, } = useObjectChat({
|
|
29
|
-
initialMessages: [
|
|
30
|
-
{
|
|
31
|
-
id: 'welcome',
|
|
32
|
-
role: 'assistant',
|
|
33
|
-
content: `Hello! I'm your **${appLabel}** assistant. How can I help you today?`,
|
|
34
|
-
},
|
|
35
|
-
],
|
|
36
|
-
autoResponse: true,
|
|
37
|
-
autoResponseText: objectNames
|
|
38
|
-
? `I can help you work with ${objectNames}. What would you like to do?`
|
|
39
|
-
: 'Thanks for your message! I\'m here to help you navigate and manage your data.',
|
|
40
|
-
autoResponseDelay: 800,
|
|
41
|
-
});
|
|
42
|
-
return (_jsx(FloatingChatbot, { floatingConfig: {
|
|
43
|
-
position: 'bottom-right',
|
|
44
|
-
defaultOpen: false,
|
|
45
|
-
panelWidth: 400,
|
|
46
|
-
panelHeight: 520,
|
|
47
|
-
title: `${appLabel} Assistant`,
|
|
48
|
-
triggerSize: 56,
|
|
49
|
-
}, messages: messages, placeholder: "Ask anything...", onSendMessage: (content) => sendMessage(content), onClear: clear, onStop: isLoading ? stop : undefined, onReload: reload, isLoading: isLoading, error: error, enableMarkdown: true }));
|
|
50
|
-
}
|
|
28
|
+
// (moved to ./ConsoleFloatingChatbot.tsx for code-splitting)
|
|
51
29
|
export function ConsoleLayout({ children, activeAppName, activeApp, onAppChange, objects, connectionState }) {
|
|
52
30
|
const appLabel = resolveI18nLabel(activeApp?.label) || activeAppName;
|
|
53
31
|
const { isAiEnabled } = useDiscovery();
|
|
@@ -67,5 +45,5 @@ export function ConsoleLayout({ children, activeAppName, activeApp, onAppChange,
|
|
|
67
45
|
? `${resolveI18nLabel(activeApp.label)} — ObjectStack Console`
|
|
68
46
|
: undefined,
|
|
69
47
|
}
|
|
70
|
-
: undefined, children: [_jsx(ConsoleLayoutInner, { children: children }), isAiEnabled && _jsx(ConsoleFloatingChatbot, { appLabel: appLabel, objects: objects })] }));
|
|
48
|
+
: undefined, children: [_jsx(ConsoleLayoutInner, { children: children }), isAiEnabled && (_jsx(Suspense, { fallback: null, children: _jsx(ConsoleFloatingChatbot, { appLabel: appLabel, objects: objects }) }))] }));
|
|
71
49
|
}
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
*
|
|
13
13
|
* @module
|
|
14
14
|
*/
|
|
15
|
+
/**
|
|
16
|
+
* Lazy-resolved Lucide icon — see ../utils/getIcon for impl.
|
|
17
|
+
* The local symbol is kept for backwards compat with existing call sites
|
|
18
|
+
* within this file.
|
|
19
|
+
*/
|
|
15
20
|
interface UnifiedSidebarProps {
|
|
16
21
|
/** When in app context, the active app name */
|
|
17
22
|
activeAppName?: string;
|
|
@@ -15,7 +15,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
15
15
|
*/
|
|
16
16
|
import * as React from 'react';
|
|
17
17
|
import { Link, useLocation } from 'react-router-dom';
|
|
18
|
-
import
|
|
18
|
+
import { getIcon } from '../utils/getIcon';
|
|
19
19
|
import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupLabel, SidebarGroupContent, SidebarMenu, SidebarMenuItem, SidebarMenuButton, SidebarMenuAction, SidebarTrigger, useSidebar, } from '@object-ui/components';
|
|
20
20
|
import { Clock, Star, StarOff, ChevronRight, Layers, } from 'lucide-react';
|
|
21
21
|
import { NavigationRenderer } from '@object-ui/layout';
|
|
@@ -85,30 +85,6 @@ function useNavOrder(appName) {
|
|
|
85
85
|
}, [orderMap, persist]);
|
|
86
86
|
return { applyOrder, handleReorder };
|
|
87
87
|
}
|
|
88
|
-
/**
|
|
89
|
-
* Resolve a Lucide icon component by name string.
|
|
90
|
-
* Safely handles both exact names and kebab-case → PascalCase conversion.
|
|
91
|
-
* The try/catch guards against strict module proxy environments (e.g. vitest mocks).
|
|
92
|
-
*/
|
|
93
|
-
function getIcon(name) {
|
|
94
|
-
if (!name)
|
|
95
|
-
return LucideIcons.Database;
|
|
96
|
-
const lookup = (key) => {
|
|
97
|
-
try {
|
|
98
|
-
const icon = LucideIcons[key];
|
|
99
|
-
return typeof icon === 'function' ? icon : undefined;
|
|
100
|
-
}
|
|
101
|
-
catch {
|
|
102
|
-
return undefined;
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
// Try exact match first, then convert kebab-case / lowercase to PascalCase
|
|
106
|
-
const pascalName = name
|
|
107
|
-
.split(/[-_]/)
|
|
108
|
-
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
109
|
-
.join('');
|
|
110
|
-
return lookup(name) ?? lookup(pascalName) ?? LucideIcons.Database;
|
|
111
|
-
}
|
|
112
88
|
export function UnifiedSidebar({ activeAppName }) {
|
|
113
89
|
const { isMobile } = useSidebar();
|
|
114
90
|
const location = useLocation();
|
package/dist/utils/getIcon.d.ts
CHANGED
|
@@ -2,9 +2,23 @@
|
|
|
2
2
|
* Icon utilities
|
|
3
3
|
*
|
|
4
4
|
* Helpers for resolving Lucide icons by name.
|
|
5
|
+
*
|
|
6
|
+
* Implementation: instead of statically importing every icon (~1500
|
|
7
|
+
* components, ~568 KB raw / 140 KB gz), we wrap lucide-react's built-in
|
|
8
|
+
* `DynamicIcon` so each icon is fetched as its own tiny chunk on first use.
|
|
9
|
+
*
|
|
10
|
+
* The exported `getIcon(name)` API stays synchronous and returns a React
|
|
11
|
+
* component, preserving call sites that do `const Icon = getIcon(name); <Icon />`.
|
|
5
12
|
*/
|
|
13
|
+
import React from 'react';
|
|
6
14
|
/**
|
|
7
|
-
* Resolve a Lucide icon by name (kebab-case or PascalCase)
|
|
8
|
-
*
|
|
15
|
+
* Resolve a Lucide icon by name (kebab-case or PascalCase).
|
|
16
|
+
*
|
|
17
|
+
* Returns a React component that lazy-loads the underlying SVG icon on
|
|
18
|
+
* mount. Falls back to the `Database` icon (statically imported) when no
|
|
19
|
+
* `name` is given.
|
|
20
|
+
*
|
|
21
|
+
* The returned component is memoised per `name` so repeated calls with the
|
|
22
|
+
* same name yield the same component reference (stable for React.memo).
|
|
9
23
|
*/
|
|
10
24
|
export declare function getIcon(name?: string): React.ElementType;
|
package/dist/utils/getIcon.js
CHANGED
|
@@ -2,23 +2,86 @@
|
|
|
2
2
|
* Icon utilities
|
|
3
3
|
*
|
|
4
4
|
* Helpers for resolving Lucide icons by name.
|
|
5
|
+
*
|
|
6
|
+
* Implementation: instead of statically importing every icon (~1500
|
|
7
|
+
* components, ~568 KB raw / 140 KB gz), we wrap lucide-react's built-in
|
|
8
|
+
* `DynamicIcon` so each icon is fetched as its own tiny chunk on first use.
|
|
9
|
+
*
|
|
10
|
+
* The exported `getIcon(name)` API stays synchronous and returns a React
|
|
11
|
+
* component, preserving call sites that do `const Icon = getIcon(name); <Icon />`.
|
|
5
12
|
*/
|
|
6
|
-
import
|
|
13
|
+
import React from 'react';
|
|
7
14
|
import { Database } from 'lucide-react';
|
|
15
|
+
// @ts-expect-error - lucide-react has no `exports` field; subpath types live alongside dynamic.mjs
|
|
16
|
+
import { DynamicIcon } from 'lucide-react/dynamic.mjs';
|
|
17
|
+
/** Convert PascalCase / camelCase / mixed names to kebab-case for DynamicIcon. */
|
|
18
|
+
function toKebab(name) {
|
|
19
|
+
if (name.includes('-'))
|
|
20
|
+
return name.toLowerCase();
|
|
21
|
+
return name
|
|
22
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
23
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
|
|
24
|
+
.toLowerCase();
|
|
25
|
+
}
|
|
8
26
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
27
|
+
* Aliases for icon names that exist in other icon sets (FontAwesome, Material,
|
|
28
|
+
* Bootstrap…) but use a different name in Lucide. Mapped to the closest
|
|
29
|
+
* Lucide equivalent so existing metadata authored against FA stays portable.
|
|
30
|
+
*/
|
|
31
|
+
const ICON_ALIASES = {
|
|
32
|
+
bullseye: 'target',
|
|
33
|
+
bullhorn: 'megaphone',
|
|
34
|
+
tachometer: 'gauge',
|
|
35
|
+
'tachometer-alt': 'gauge',
|
|
36
|
+
dashboard: 'layout-dashboard',
|
|
37
|
+
'life-ring': 'life-buoy',
|
|
38
|
+
tasks: 'list-checks',
|
|
39
|
+
'file-invoice': 'receipt',
|
|
40
|
+
'file-signature': 'file-pen-line',
|
|
41
|
+
'file-pdf': 'file-text',
|
|
42
|
+
'check-square': 'square-check',
|
|
43
|
+
'arrow-up-right-from-square': 'external-link',
|
|
44
|
+
'sign-out-alt': 'log-out',
|
|
45
|
+
'sign-in-alt': 'log-in',
|
|
46
|
+
'cog': 'settings',
|
|
47
|
+
'gears': 'settings',
|
|
48
|
+
'envelope': 'mail',
|
|
49
|
+
'phone-alt': 'phone',
|
|
50
|
+
'map-marker-alt': 'map-pin',
|
|
51
|
+
'edit': 'pencil',
|
|
52
|
+
'trash-alt': 'trash-2',
|
|
53
|
+
'plus-circle': 'circle-plus',
|
|
54
|
+
'minus-circle': 'circle-minus',
|
|
55
|
+
'times-circle': 'circle-x',
|
|
56
|
+
'check-circle': 'circle-check',
|
|
57
|
+
'info-circle': 'info',
|
|
58
|
+
'question-circle': 'circle-help',
|
|
59
|
+
'exclamation-triangle': 'triangle-alert',
|
|
60
|
+
};
|
|
61
|
+
const cache = new Map();
|
|
62
|
+
/**
|
|
63
|
+
* Resolve a Lucide icon by name (kebab-case or PascalCase).
|
|
64
|
+
*
|
|
65
|
+
* Returns a React component that lazy-loads the underlying SVG icon on
|
|
66
|
+
* mount. Falls back to the `Database` icon (statically imported) when no
|
|
67
|
+
* `name` is given.
|
|
68
|
+
*
|
|
69
|
+
* The returned component is memoised per `name` so repeated calls with the
|
|
70
|
+
* same name yield the same component reference (stable for React.memo).
|
|
11
71
|
*/
|
|
12
72
|
export function getIcon(name) {
|
|
13
73
|
if (!name)
|
|
14
74
|
return Database;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
75
|
+
const cached = cache.get(name);
|
|
76
|
+
if (cached)
|
|
77
|
+
return cached;
|
|
78
|
+
const kebab = ICON_ALIASES[toKebab(name)] ?? toKebab(name);
|
|
79
|
+
const Wrapped = (props) => React.createElement(DynamicIcon, {
|
|
80
|
+
name: kebab,
|
|
81
|
+
fallback: Database,
|
|
82
|
+
...props,
|
|
83
|
+
});
|
|
84
|
+
Wrapped.displayName = `LucideIcon(${name})`;
|
|
85
|
+
cache.set(name, Wrapped);
|
|
86
|
+
return Wrapped;
|
|
24
87
|
}
|
|
@@ -8,7 +8,4 @@
|
|
|
8
8
|
* - useObjectActions for toolbar create button
|
|
9
9
|
* - ListView delegation for non-grid view types (kanban, calendar, chart, etc.)
|
|
10
10
|
*/
|
|
11
|
-
import '@object-ui/plugin-grid';
|
|
12
|
-
import '@object-ui/plugin-kanban';
|
|
13
|
-
import '@object-ui/plugin-calendar';
|
|
14
11
|
export declare function ObjectView({ dataSource, objects, onEdit }: any): import("react/jsx-runtime").JSX.Element;
|
package/dist/views/ObjectView.js
CHANGED
|
@@ -9,16 +9,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
9
9
|
* - useObjectActions for toolbar create button
|
|
10
10
|
* - ListView delegation for non-grid view types (kanban, calendar, chart, etc.)
|
|
11
11
|
*/
|
|
12
|
-
import { useMemo, useState, useCallback, useEffect } from 'react';
|
|
12
|
+
import { useMemo, useState, useCallback, useEffect, lazy, Suspense } from 'react';
|
|
13
13
|
import { useParams, useSearchParams, useNavigate } from 'react-router-dom';
|
|
14
|
-
|
|
14
|
+
const ObjectChart = lazy(() => import('@object-ui/plugin-charts').then((m) => ({ default: m.ObjectChart })));
|
|
15
15
|
import { ListView } from '@object-ui/plugin-list';
|
|
16
16
|
import { DetailView, RecordChatterPanel } from '@object-ui/plugin-detail';
|
|
17
17
|
import { ObjectView as PluginObjectView, ViewTabBar } from '@object-ui/plugin-view';
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
import '@object-ui/plugin-
|
|
21
|
-
import '@object-ui/plugin-calendar';
|
|
18
|
+
// Plugin registration is handled by the host app (e.g. apps/console/src/main.tsx
|
|
19
|
+
// uses ComponentRegistry.registerLazy so heavy plugins stay code-split).
|
|
20
|
+
// Do NOT add eager `import '@object-ui/plugin-*'` side-effect imports here.
|
|
22
21
|
import { Button, Empty, EmptyTitle, EmptyDescription, NavigationOverlay, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from '@object-ui/components';
|
|
23
22
|
import { Plus, Table as TableIcon, Settings2, Wrench, KanbanSquare, Calendar, LayoutGrid, Activity, GanttChart, MapPin, BarChart3 } from 'lucide-react';
|
|
24
23
|
import { getIcon } from '../utils/getIcon';
|
|
@@ -575,17 +574,17 @@ export function ObjectView({ dataSource, objects, onEdit }) {
|
|
|
575
574
|
}
|
|
576
575
|
if (viewDef.type === 'chart') {
|
|
577
576
|
const chartConfig = viewDef.chart || {};
|
|
578
|
-
return (_jsx(ObjectChart, { dataSource: ds, schema: {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
577
|
+
return (_jsx(Suspense, { fallback: _jsx("div", { className: "p-4 text-sm text-muted-foreground", children: "Loading chart\u2026" }), children: _jsx(ObjectChart, { dataSource: ds, schema: {
|
|
578
|
+
type: 'object-chart',
|
|
579
|
+
objectName: objectDef.name,
|
|
580
|
+
chartType: chartConfig.chartType,
|
|
581
|
+
xAxisField: chartConfig.xAxisField,
|
|
582
|
+
yAxisFields: chartConfig.yAxisFields,
|
|
583
|
+
aggregation: chartConfig.aggregation,
|
|
584
|
+
series: chartConfig.series,
|
|
585
|
+
config: chartConfig.config,
|
|
586
|
+
filter: chartConfig.filter,
|
|
587
|
+
} }) }, key));
|
|
589
588
|
}
|
|
590
589
|
const fullSchema = {
|
|
591
590
|
...listSchema,
|
package/dist/views/ReportView.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
2
|
+
import { useState, useEffect, useCallback, useMemo, lazy, Suspense } from 'react';
|
|
3
3
|
import { useParams } from 'react-router-dom';
|
|
4
|
-
|
|
4
|
+
const ReportViewer = lazy(() => import('@object-ui/plugin-report').then((m) => ({ default: m.ReportViewer })));
|
|
5
|
+
const ReportConfigPanel = lazy(() => import('@object-ui/plugin-report').then((m) => ({ default: m.ReportConfigPanel })));
|
|
5
6
|
import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
|
|
6
7
|
import { Pencil, BarChart3, Loader2 } from 'lucide-react';
|
|
7
8
|
import { MetadataPanel, useMetadataInspector } from './MetadataInspector';
|
|
@@ -280,5 +281,5 @@ export function ReportView({ dataSource }) {
|
|
|
280
281
|
allowExport: true,
|
|
281
282
|
loading: dataLoading, // Loading state for data fetching
|
|
282
283
|
};
|
|
283
|
-
return (_jsxs("div", { className: "flex flex-col h-full overflow-hidden bg-background", children: [_jsxs("div", { className: "flex flex-col sm:flex-row justify-between sm:items-center gap-3 sm:gap-4 p-4 sm:p-6 border-b shrink-0", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("h1", { className: "text-lg sm:text-xl md:text-2xl font-bold tracking-tight truncate", children: previewReport.title || previewReport.label || 'Report Viewer' }), previewReport.description && (_jsx("p", { className: "text-sm text-muted-foreground mt-1 line-clamp-2", children: previewReport.description }))] }), _jsx("div", { className: "shrink-0 flex items-center gap-1.5", children: _jsxs("button", { type: "button", onClick: handleOpenConfigPanel, className: "inline-flex items-center gap-1.5 rounded-md border border-input bg-background px-2.5 py-1.5 text-xs font-medium text-muted-foreground shadow-sm hover:bg-accent hover:text-accent-foreground", "data-testid": "report-edit-button", children: [_jsx(Pencil, { className: "h-3.5 w-3.5" }), "Edit"] }) })] }), _jsxs("div", { className: "flex-1 overflow-hidden flex flex-col sm:flex-row relative", children: [_jsx("div", { className: "flex-1 min-w-0 overflow-auto p-4 sm:p-6 lg:p-8 bg-muted/5", children: _jsx("div", { className: "max-w-5xl mx-auto shadow-sm border rounded-lg sm:rounded-xl bg-background overflow-hidden min-h-150", children: _jsx(ReportViewer, { schema: viewerSchema }) }) }), _jsx(ReportConfigPanel, { open: configPanelOpen, onClose: handleCloseConfigPanel, config: reportConfig, onSave: handleReportConfigSave, onFieldChange: handleReportFieldChange, availableFields: availableFields }), _jsx(MetadataPanel, { open: showDebug, sections: [{ title: 'Report Configuration', data: previewReport }] })] })] }));
|
|
284
|
+
return (_jsxs("div", { className: "flex flex-col h-full overflow-hidden bg-background", children: [_jsxs("div", { className: "flex flex-col sm:flex-row justify-between sm:items-center gap-3 sm:gap-4 p-4 sm:p-6 border-b shrink-0", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("h1", { className: "text-lg sm:text-xl md:text-2xl font-bold tracking-tight truncate", children: previewReport.title || previewReport.label || 'Report Viewer' }), previewReport.description && (_jsx("p", { className: "text-sm text-muted-foreground mt-1 line-clamp-2", children: previewReport.description }))] }), _jsx("div", { className: "shrink-0 flex items-center gap-1.5", children: _jsxs("button", { type: "button", onClick: handleOpenConfigPanel, className: "inline-flex items-center gap-1.5 rounded-md border border-input bg-background px-2.5 py-1.5 text-xs font-medium text-muted-foreground shadow-sm hover:bg-accent hover:text-accent-foreground", "data-testid": "report-edit-button", children: [_jsx(Pencil, { className: "h-3.5 w-3.5" }), "Edit"] }) })] }), _jsxs("div", { className: "flex-1 overflow-hidden flex flex-col sm:flex-row relative", children: [_jsx("div", { className: "flex-1 min-w-0 overflow-auto p-4 sm:p-6 lg:p-8 bg-muted/5", children: _jsx("div", { className: "max-w-5xl mx-auto shadow-sm border rounded-lg sm:rounded-xl bg-background overflow-hidden min-h-150", children: _jsx(Suspense, { fallback: _jsx("div", { className: "p-8 text-sm text-muted-foreground", children: "Loading report\u2026" }), children: _jsx(ReportViewer, { schema: viewerSchema }) }) }) }), _jsx(Suspense, { fallback: null, children: _jsx(ReportConfigPanel, { open: configPanelOpen, onClose: handleCloseConfigPanel, config: reportConfig, onSave: handleReportConfigSave, onFieldChange: handleReportFieldChange, availableFields: availableFields }) }), _jsx(MetadataPanel, { open: showDebug, sections: [{ title: 'Report Configuration', data: previewReport }] })] })] }));
|
|
284
285
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/app-shell",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Minimal application shell for ObjectUI - framework-agnostic rendering engine",
|
|
@@ -26,34 +26,34 @@
|
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"lucide-react": "^0.544.0",
|
|
28
28
|
"sonner": "^2.0.7",
|
|
29
|
-
"@object-ui/auth": "3.3.
|
|
30
|
-
"@object-ui/collaboration": "3.3.
|
|
31
|
-
"@object-ui/components": "3.3.
|
|
32
|
-
"@object-ui/core": "3.3.
|
|
33
|
-
"@object-ui/data-objectstack": "3.3.
|
|
34
|
-
"@object-ui/fields": "3.3.
|
|
35
|
-
"@object-ui/i18n": "3.3.
|
|
36
|
-
"@object-ui/layout": "3.3.
|
|
37
|
-
"@object-ui/permissions": "3.3.
|
|
38
|
-
"@object-ui/react": "3.3.
|
|
39
|
-
"@object-ui/types": "3.3.
|
|
29
|
+
"@object-ui/auth": "3.3.2",
|
|
30
|
+
"@object-ui/collaboration": "3.3.2",
|
|
31
|
+
"@object-ui/components": "3.3.2",
|
|
32
|
+
"@object-ui/core": "3.3.2",
|
|
33
|
+
"@object-ui/data-objectstack": "3.3.2",
|
|
34
|
+
"@object-ui/fields": "3.3.2",
|
|
35
|
+
"@object-ui/i18n": "3.3.2",
|
|
36
|
+
"@object-ui/layout": "3.3.2",
|
|
37
|
+
"@object-ui/permissions": "3.3.2",
|
|
38
|
+
"@object-ui/react": "3.3.2",
|
|
39
|
+
"@object-ui/types": "3.3.2"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"react": "^18.0.0 || ^19.0.0",
|
|
43
43
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
44
44
|
"react-router-dom": "^6.0.0 || ^7.0.0",
|
|
45
|
-
"@object-ui/plugin-calendar": "3.3.
|
|
46
|
-
"@object-ui/plugin-charts": "3.3.
|
|
47
|
-
"@object-ui/plugin-chatbot": "3.3.
|
|
48
|
-
"@object-ui/plugin-dashboard": "3.3.
|
|
49
|
-
"@object-ui/plugin-designer": "3.3.
|
|
50
|
-
"@object-ui/plugin-detail": "3.3.
|
|
51
|
-
"@object-ui/plugin-form": "3.3.
|
|
52
|
-
"@object-ui/plugin-grid": "3.3.
|
|
53
|
-
"@object-ui/plugin-kanban": "3.3.
|
|
54
|
-
"@object-ui/plugin-list": "3.3.
|
|
55
|
-
"@object-ui/plugin-report": "3.3.
|
|
56
|
-
"@object-ui/plugin-view": "3.3.
|
|
45
|
+
"@object-ui/plugin-calendar": "3.3.2",
|
|
46
|
+
"@object-ui/plugin-charts": "3.3.2",
|
|
47
|
+
"@object-ui/plugin-chatbot": "3.3.2",
|
|
48
|
+
"@object-ui/plugin-dashboard": "3.3.2",
|
|
49
|
+
"@object-ui/plugin-designer": "3.3.2",
|
|
50
|
+
"@object-ui/plugin-detail": "3.3.2",
|
|
51
|
+
"@object-ui/plugin-form": "3.3.2",
|
|
52
|
+
"@object-ui/plugin-grid": "3.3.2",
|
|
53
|
+
"@object-ui/plugin-kanban": "3.3.2",
|
|
54
|
+
"@object-ui/plugin-list": "3.3.2",
|
|
55
|
+
"@object-ui/plugin-report": "3.3.2",
|
|
56
|
+
"@object-ui/plugin-view": "3.3.2"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@types/node": "^25.6.0",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"react-router-dom": "^7.14.2",
|
|
65
65
|
"sonner": "^2.0.7",
|
|
66
66
|
"typescript": "^6.0.3",
|
|
67
|
-
"vite": "^
|
|
67
|
+
"vite": "^8.0.10"
|
|
68
68
|
},
|
|
69
69
|
"keywords": [
|
|
70
70
|
"objectui",
|