@object-ui/app-shell 3.3.0 → 3.3.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 +45 -0
- package/README.md +22 -2
- package/dist/chrome/CommandPalette.d.ts +16 -0
- package/dist/chrome/CommandPalette.js +88 -0
- package/dist/chrome/ConditionalAuthWrapper.d.ts +23 -0
- package/dist/chrome/ConditionalAuthWrapper.js +135 -0
- package/dist/chrome/ConsoleToaster.d.ts +11 -0
- package/dist/chrome/ConsoleToaster.js +28 -0
- package/dist/chrome/ErrorBoundary.d.ts +39 -0
- package/dist/chrome/ErrorBoundary.js +64 -0
- package/dist/chrome/KeyboardShortcutsDialog.d.ts +7 -0
- package/dist/chrome/KeyboardShortcutsDialog.js +63 -0
- package/dist/chrome/LoadingScreen.d.ts +12 -0
- package/dist/chrome/LoadingScreen.js +44 -0
- package/dist/chrome/OnboardingWalkthrough.d.ts +9 -0
- package/dist/chrome/OnboardingWalkthrough.js +11 -0
- package/dist/chrome/ThemeProvider.d.ts +13 -0
- package/dist/chrome/ThemeProvider.js +37 -0
- package/dist/chrome/index.d.ts +8 -0
- package/dist/chrome/index.js +8 -0
- package/dist/console/AppContent.d.ts +26 -0
- package/dist/console/AppContent.js +266 -0
- package/dist/console/ConsoleShell.d.ts +66 -0
- package/dist/console/ConsoleShell.js +97 -0
- package/dist/console/auth/AuthPageLayout.d.ts +9 -0
- package/dist/console/auth/AuthPageLayout.js +4 -0
- package/dist/console/auth/ForgotPasswordPage.d.ts +4 -0
- package/dist/console/auth/ForgotPasswordPage.js +23 -0
- package/dist/console/auth/LoginPage.d.ts +4 -0
- package/dist/console/auth/LoginPage.js +24 -0
- package/dist/console/auth/RegisterPage.d.ts +4 -0
- package/dist/console/auth/RegisterPage.js +29 -0
- package/dist/console/auth/index.d.ts +4 -0
- package/dist/console/auth/index.js +4 -0
- package/dist/console/home/AppCard.d.ts +14 -0
- package/dist/console/home/AppCard.js +36 -0
- package/dist/console/home/HomeLayout.d.ts +15 -0
- package/dist/console/home/HomeLayout.js +20 -0
- package/dist/console/home/HomePage.d.ts +18 -0
- package/dist/console/home/HomePage.js +54 -0
- package/dist/console/home/QuickActions.d.ts +9 -0
- package/dist/console/home/QuickActions.js +53 -0
- package/dist/console/home/RecentApps.d.ts +13 -0
- package/dist/console/home/RecentApps.js +32 -0
- package/dist/console/home/StarredApps.d.ts +13 -0
- package/dist/console/home/StarredApps.js +29 -0
- package/dist/console/home/index.d.ts +11 -0
- package/dist/console/home/index.js +11 -0
- package/dist/console/organizations/CreateWorkspaceDialog.d.ts +16 -0
- package/dist/console/organizations/CreateWorkspaceDialog.js +71 -0
- package/dist/console/organizations/OrganizationsLayout.d.ts +15 -0
- package/dist/console/organizations/OrganizationsLayout.js +20 -0
- package/dist/console/organizations/OrganizationsPage.d.ts +9 -0
- package/dist/console/organizations/OrganizationsPage.js +76 -0
- package/dist/console/organizations/index.d.ts +8 -0
- package/dist/console/organizations/index.js +8 -0
- package/dist/console/organizations/manage/AcceptInvitationPage.d.ts +7 -0
- package/dist/console/organizations/manage/AcceptInvitationPage.js +100 -0
- package/dist/console/organizations/manage/InvitationsPage.d.ts +6 -0
- package/dist/console/organizations/manage/InvitationsPage.js +107 -0
- package/dist/console/organizations/manage/InviteMemberDialog.d.ts +16 -0
- package/dist/console/organizations/manage/InviteMemberDialog.js +83 -0
- package/dist/console/organizations/manage/MembersPage.d.ts +6 -0
- package/dist/console/organizations/manage/MembersPage.js +89 -0
- package/dist/console/organizations/manage/OrganizationLayout.d.ts +8 -0
- package/dist/console/organizations/manage/OrganizationLayout.js +51 -0
- package/dist/console/organizations/manage/SettingsPage.d.ts +6 -0
- package/dist/console/organizations/manage/SettingsPage.js +141 -0
- package/dist/console/organizations/manage/orgContext.d.ts +5 -0
- package/dist/console/organizations/manage/orgContext.js +4 -0
- package/dist/context/FavoritesProvider.d.ts +50 -0
- package/dist/context/FavoritesProvider.js +129 -0
- package/dist/context/NavigationContext.d.ts +46 -0
- package/dist/context/NavigationContext.js +59 -0
- package/dist/context/index.d.ts +3 -0
- package/dist/context/index.js +2 -0
- package/dist/hooks/index.d.ts +7 -0
- package/dist/hooks/index.js +7 -0
- package/dist/hooks/useFavorites.d.ts +13 -0
- package/dist/hooks/useFavorites.js +13 -0
- package/dist/hooks/useMetadataService.d.ts +13 -0
- package/dist/hooks/useMetadataService.js +18 -0
- package/dist/hooks/useNavPins.d.ts +16 -0
- package/dist/hooks/useNavPins.js +72 -0
- package/dist/hooks/useNavigationSync.d.ts +82 -0
- package/dist/hooks/useNavigationSync.js +495 -0
- package/dist/hooks/useObjectActions.d.ts +38 -0
- package/dist/hooks/useObjectActions.js +92 -0
- package/dist/hooks/useRecentItems.d.ts +21 -0
- package/dist/hooks/useRecentItems.js +50 -0
- package/dist/hooks/useResponsiveSidebar.d.ts +8 -0
- package/dist/hooks/useResponsiveSidebar.js +28 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +35 -0
- package/dist/layout/ActivityFeed.d.ts +22 -0
- package/dist/layout/ActivityFeed.js +57 -0
- package/dist/layout/AppHeader.d.ts +33 -0
- package/dist/layout/AppHeader.js +182 -0
- package/dist/layout/AppSidebar.d.ts +14 -0
- package/dist/layout/AppSidebar.js +217 -0
- package/dist/layout/AppSwitcher.d.ts +12 -0
- package/dist/layout/AppSwitcher.js +39 -0
- package/dist/layout/AuthPageLayout.d.ts +9 -0
- package/dist/layout/AuthPageLayout.js +4 -0
- package/dist/layout/ConnectionStatus.d.ts +14 -0
- package/dist/layout/ConnectionStatus.js +36 -0
- package/dist/layout/ConsoleLayout.d.ts +21 -0
- package/dist/layout/ConsoleLayout.js +71 -0
- package/dist/layout/LocaleSwitcher.d.ts +7 -0
- package/dist/layout/LocaleSwitcher.js +27 -0
- package/dist/layout/ModeToggle.d.ts +1 -0
- package/dist/layout/ModeToggle.js +10 -0
- package/dist/layout/UnifiedSidebar.d.ts +22 -0
- package/dist/layout/UnifiedSidebar.js +210 -0
- package/dist/layout/index.d.ts +11 -0
- package/dist/layout/index.js +10 -0
- package/dist/providers/AdapterProvider.d.ts +21 -0
- package/dist/providers/AdapterProvider.js +52 -0
- package/dist/providers/ExpressionProvider.d.ts +50 -0
- package/dist/providers/ExpressionProvider.js +67 -0
- package/dist/providers/MetadataProvider.d.ts +18 -0
- package/dist/providers/MetadataProvider.js +324 -0
- package/dist/services/MetadataService.d.ts +119 -0
- package/dist/services/MetadataService.js +211 -0
- package/dist/skeletons/SkeletonDashboard.d.ts +13 -0
- package/dist/skeletons/SkeletonDashboard.js +12 -0
- package/dist/skeletons/SkeletonDetail.d.ts +15 -0
- package/dist/skeletons/SkeletonDetail.js +12 -0
- package/dist/skeletons/SkeletonGrid.d.ts +15 -0
- package/dist/skeletons/SkeletonGrid.js +12 -0
- package/dist/skeletons/index.d.ts +3 -0
- package/dist/skeletons/index.js +3 -0
- package/dist/utils/getIcon.d.ts +10 -0
- package/dist/utils/getIcon.js +24 -0
- package/dist/utils/index.d.ts +33 -0
- package/dist/utils/index.js +62 -0
- package/dist/utils/metadataConverters.d.ts +78 -0
- package/dist/utils/metadataConverters.js +74 -0
- package/dist/views/ActionConfirmDialog.d.ts +22 -0
- package/dist/views/ActionConfirmDialog.js +22 -0
- package/dist/views/ActionParamDialog.d.ts +23 -0
- package/dist/views/ActionParamDialog.js +67 -0
- package/dist/views/DashboardView.d.ts +9 -0
- package/dist/views/DashboardView.js +357 -0
- package/dist/views/DesignDrawer.d.ts +28 -0
- package/dist/views/DesignDrawer.js +51 -0
- package/dist/views/MetadataInspector.d.ts +40 -0
- package/dist/views/MetadataInspector.js +69 -0
- package/dist/views/ObjectView.d.ts +14 -0
- package/dist/views/ObjectView.js +770 -0
- package/dist/views/PageView.d.ts +6 -0
- package/dist/views/PageView.js +49 -0
- package/dist/views/RecordDetailView.d.ts +14 -0
- package/dist/views/RecordDetailView.js +508 -0
- package/dist/views/ReportView.d.ts +4 -0
- package/dist/views/ReportView.js +284 -0
- package/dist/views/SearchResultsPage.d.ts +9 -0
- package/dist/views/SearchResultsPage.js +107 -0
- package/dist/views/ViewConfigPanel.d.ts +56 -0
- package/dist/views/ViewConfigPanel.js +91 -0
- package/dist/views/index.d.ts +11 -0
- package/dist/views/index.js +11 -0
- package/package.json +59 -11
- package/.turbo/turbo-build.log +0 -4
- package/src/components/AppShell.tsx +0 -31
- package/src/components/DashboardRenderer.tsx +0 -36
- package/src/components/FormRenderer.tsx +0 -71
- package/src/components/ObjectRenderer.tsx +0 -122
- package/src/components/PageRenderer.tsx +0 -28
- package/src/index.ts +0 -20
- package/src/types.ts +0 -78
- package/tsconfig.json +0 -20
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Register Page for ObjectStack Console
|
|
4
|
+
*/
|
|
5
|
+
import { useNavigate, Link } from 'react-router-dom';
|
|
6
|
+
import { RegisterForm } from '@object-ui/auth';
|
|
7
|
+
import { useObjectTranslation } from '@object-ui/i18n';
|
|
8
|
+
import { AuthPageLayout } from './AuthPageLayout';
|
|
9
|
+
const RouterLink = ({ href, className, children }) => (_jsx(Link, { to: href, className: className, children: children }));
|
|
10
|
+
export function RegisterPage() {
|
|
11
|
+
const navigate = useNavigate();
|
|
12
|
+
const { t } = useObjectTranslation();
|
|
13
|
+
return (_jsx(AuthPageLayout, { children: _jsx(RegisterForm, { onSuccess: () => navigate('/'), loginUrl: "/login", title: t('auth.register.title'), description: t('auth.register.description'), linkComponent: RouterLink, labels: {
|
|
14
|
+
nameLabel: t('auth.register.nameLabel'),
|
|
15
|
+
namePlaceholder: t('auth.register.namePlaceholder'),
|
|
16
|
+
emailLabel: t('auth.register.emailLabel'),
|
|
17
|
+
emailPlaceholder: t('auth.register.emailPlaceholder'),
|
|
18
|
+
passwordLabel: t('auth.register.passwordLabel'),
|
|
19
|
+
passwordPlaceholder: t('auth.register.passwordPlaceholder'),
|
|
20
|
+
confirmPasswordLabel: t('auth.register.confirmPasswordLabel'),
|
|
21
|
+
confirmPasswordPlaceholder: t('auth.register.confirmPasswordPlaceholder'),
|
|
22
|
+
passwordMismatchError: t('auth.register.passwordMismatchError'),
|
|
23
|
+
passwordTooShortError: t('auth.register.passwordTooShortError'),
|
|
24
|
+
submitButton: t('auth.register.submitButton'),
|
|
25
|
+
submittingButton: t('auth.register.submittingButton'),
|
|
26
|
+
hasAccountText: t('auth.register.hasAccountText'),
|
|
27
|
+
signInText: t('auth.register.signInText'),
|
|
28
|
+
} }) }));
|
|
29
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AppCard
|
|
3
|
+
*
|
|
4
|
+
* Display card for an application with icon, name, description, and favorite toggle.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
interface AppCardProps {
|
|
9
|
+
app: any;
|
|
10
|
+
onClick: () => void;
|
|
11
|
+
isFavorite: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function AppCard({ app, onClick, isFavorite }: AppCardProps): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* AppCard
|
|
4
|
+
*
|
|
5
|
+
* Display card for an application with icon, name, description, and favorite toggle.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import { Star, StarOff } from 'lucide-react';
|
|
10
|
+
import { Card, CardContent, Button, Badge } from '@object-ui/components';
|
|
11
|
+
import { useObjectTranslation, useObjectLabel } from '@object-ui/i18n';
|
|
12
|
+
import { resolveI18nLabel } from '../../utils';
|
|
13
|
+
import { useFavorites } from '../../hooks/useFavorites';
|
|
14
|
+
import { getIcon } from '../../utils/getIcon';
|
|
15
|
+
import { cn } from '@object-ui/components';
|
|
16
|
+
export function AppCard({ app, onClick, isFavorite }) {
|
|
17
|
+
const { t } = useObjectTranslation();
|
|
18
|
+
const { appLabel, appDescription } = useObjectLabel();
|
|
19
|
+
const { toggleFavorite } = useFavorites();
|
|
20
|
+
const Icon = getIcon(app.icon);
|
|
21
|
+
const label = appLabel({ name: app.name, label: resolveI18nLabel(app.label, t) });
|
|
22
|
+
const description = appDescription({ name: app.name, description: resolveI18nLabel(app.description, t) });
|
|
23
|
+
const primaryColor = app.branding?.primaryColor;
|
|
24
|
+
const handleToggleFavorite = (e) => {
|
|
25
|
+
e.stopPropagation();
|
|
26
|
+
toggleFavorite({
|
|
27
|
+
id: `app:${app.name}`,
|
|
28
|
+
label,
|
|
29
|
+
href: `/apps/${app.name}`,
|
|
30
|
+
type: 'object',
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
return (_jsx(Card, { className: "cursor-pointer hover:shadow-lg transition-all group relative", onClick: onClick, "data-testid": `app-card-${app.name}`, children: _jsxs(CardContent, { className: "p-6", children: [_jsx(Button, { variant: "ghost", size: "sm", className: "absolute top-2 right-2 opacity-0 group-hover:opacity-100 focus-visible:opacity-100 transition-opacity", onClick: handleToggleFavorite, "aria-label": isFavorite
|
|
34
|
+
? t('common.removeFromFavorites', { defaultValue: 'Remove from favorites' }) + ` — ${label}`
|
|
35
|
+
: 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-yellow-400 text-yellow-400" })) : (_jsx(StarOff, { className: "h-4 w-4" })) }), _jsx("div", { className: cn('inline-flex p-3 rounded-lg mb-4', primaryColor ? '' : 'bg-primary/10'), style: primaryColor ? { backgroundColor: `${primaryColor}20` } : {}, children: _jsx(Icon, { className: "h-8 w-8", style: primaryColor ? { color: primaryColor } : {} }) }), _jsxs("div", { children: [_jsx("h3", { className: "font-semibold text-lg mb-1", children: label }), description && (_jsx("p", { className: "text-sm text-muted-foreground line-clamp-2", children: description })), !description && (_jsx("p", { className: "text-sm text-muted-foreground", children: t('home.appCard.noDescription', { defaultValue: 'No description' }) }))] }), app.isDefault && (_jsx("div", { className: "mt-3", children: _jsx(Badge, { variant: "secondary", children: t('home.appCard.default', { defaultValue: 'Default' }) }) }))] }) }));
|
|
36
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HomeLayout
|
|
3
|
+
*
|
|
4
|
+
* Home (workspace) landing layout. Uses the unified `AppHeader` top bar in
|
|
5
|
+
* `home` variant so that `/home` shares chrome with the rest of the console;
|
|
6
|
+
* deliberately omits the sidebar.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import React from 'react';
|
|
11
|
+
interface HomeLayoutProps {
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
}
|
|
14
|
+
export declare function HomeLayout({ children }: HomeLayoutProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* HomeLayout
|
|
4
|
+
*
|
|
5
|
+
* Home (workspace) landing layout. Uses the unified `AppHeader` top bar in
|
|
6
|
+
* `home` variant so that `/home` shares chrome with the rest of the console;
|
|
7
|
+
* deliberately omits the sidebar.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
import { useEffect } from 'react';
|
|
12
|
+
import { useNavigationContext } from '../../context/NavigationContext';
|
|
13
|
+
import { AppHeader } from '../../layout/AppHeader';
|
|
14
|
+
export function HomeLayout({ children }) {
|
|
15
|
+
const { setContext } = useNavigationContext();
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
setContext('home');
|
|
18
|
+
}, [setContext]);
|
|
19
|
+
return (_jsxs("div", { className: "flex min-h-svh w-full flex-col bg-background", "data-testid": "home-layout", children: [_jsx("header", { className: "sticky top-0 z-30 flex h-14 w-full shrink-0 items-center gap-2 border-b bg-background px-2 sm:px-4", children: _jsx(AppHeader, { variant: "home" }) }), _jsx("main", { className: "flex-1 min-w-0 overflow-auto", children: children })] }));
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HomePage
|
|
3
|
+
*
|
|
4
|
+
* Unified Home Dashboard (Workspace) that displays all available applications,
|
|
5
|
+
* quick actions, recent items, and favorites. Inspired by Airtable/Notion home pages.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Display all active applications as cards
|
|
9
|
+
* - Quick actions for creating apps, importing data, etc.
|
|
10
|
+
* - Recent apps section (from useRecentItems)
|
|
11
|
+
* - Starred/Favorite apps section (from useFavorites)
|
|
12
|
+
* - Empty state guidance for new users
|
|
13
|
+
* - Responsive grid layout
|
|
14
|
+
* - i18n support
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
export declare function HomePage(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* HomePage
|
|
4
|
+
*
|
|
5
|
+
* Unified Home Dashboard (Workspace) that displays all available applications,
|
|
6
|
+
* quick actions, recent items, and favorites. Inspired by Airtable/Notion home pages.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Display all active applications as cards
|
|
10
|
+
* - Quick actions for creating apps, importing data, etc.
|
|
11
|
+
* - Recent apps section (from useRecentItems)
|
|
12
|
+
* - Starred/Favorite apps section (from useFavorites)
|
|
13
|
+
* - Empty state guidance for new users
|
|
14
|
+
* - Responsive grid layout
|
|
15
|
+
* - i18n support
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
import { useNavigate } from 'react-router-dom';
|
|
20
|
+
import { useMetadata } from '../../providers/MetadataProvider';
|
|
21
|
+
import { useRecentItems } from '../../hooks/useRecentItems';
|
|
22
|
+
import { useFavorites } from '../../hooks/useFavorites';
|
|
23
|
+
import { useObjectTranslation } from '@object-ui/i18n';
|
|
24
|
+
import { QuickActions } from './QuickActions';
|
|
25
|
+
import { AppCard } from './AppCard';
|
|
26
|
+
import { RecentApps } from './RecentApps';
|
|
27
|
+
import { StarredApps } from './StarredApps';
|
|
28
|
+
import { Empty, EmptyTitle, EmptyDescription, Button } from '@object-ui/components';
|
|
29
|
+
import { Plus, Settings } from 'lucide-react';
|
|
30
|
+
export function HomePage() {
|
|
31
|
+
const navigate = useNavigate();
|
|
32
|
+
const { t } = useObjectTranslation();
|
|
33
|
+
const { apps, loading } = useMetadata();
|
|
34
|
+
const { recentItems } = useRecentItems();
|
|
35
|
+
const { favorites } = useFavorites();
|
|
36
|
+
// Filter active apps
|
|
37
|
+
const activeApps = apps.filter((a) => a.active !== false);
|
|
38
|
+
// Get recent apps (only apps, not objects/dashboards)
|
|
39
|
+
const recentApps = recentItems
|
|
40
|
+
.filter(item => item.type === 'object' || item.type === 'dashboard' || item.type === 'page')
|
|
41
|
+
.slice(0, 6);
|
|
42
|
+
// Get starred apps
|
|
43
|
+
const starredApps = favorites
|
|
44
|
+
.filter(item => item.type === 'object' || item.type === 'dashboard' || item.type === 'page')
|
|
45
|
+
.slice(0, 8);
|
|
46
|
+
if (loading) {
|
|
47
|
+
return (_jsx("div", { className: "flex flex-1 items-center justify-center py-20", children: _jsx("div", { className: "text-muted-foreground", children: "Loading workspace..." }) }));
|
|
48
|
+
}
|
|
49
|
+
// Empty state - no apps configured
|
|
50
|
+
if (activeApps.length === 0) {
|
|
51
|
+
return (_jsx("div", { className: "flex flex-1 items-center justify-center p-6", children: _jsxs(Empty, { children: [_jsx(EmptyTitle, { children: "Welcome to ObjectUI" }), _jsx(EmptyDescription, { children: "Get started by creating your first application or configure your system settings." }), _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" }), "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" }), "System Settings"] })] })] }) }));
|
|
52
|
+
}
|
|
53
|
+
return (_jsxs("div", { className: "bg-background", children: [_jsxs("div", { className: "px-4 sm:px-6 pt-6 pb-4", children: [_jsx("h1", { className: "text-2xl sm:text-3xl font-bold tracking-tight", children: t('home.title', { defaultValue: 'Home' }) }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: t('home.subtitle', { defaultValue: 'Your workspace dashboard' }) })] }), _jsxs("div", { className: "px-4 sm:px-6 py-4 space-y-8", children: [_jsx(QuickActions, {}), starredApps.length > 0 && (_jsx(StarredApps, { items: starredApps })), recentApps.length > 0 && (_jsx(RecentApps, { items: recentApps })), _jsxs("section", { children: [_jsx("h2", { className: "text-2xl font-semibold tracking-tight mb-4", children: t('home.allApps', { defaultValue: 'All Applications' }) }), _jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4", children: activeApps.map((app) => (_jsx(AppCard, { app: app, onClick: () => navigate(`/apps/${app.name}`), isFavorite: favorites.some(f => f.id === `app:${app.name}`) }, app.name))) })] })] })] }));
|
|
54
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* QuickActions
|
|
4
|
+
*
|
|
5
|
+
* Quick action cards for common tasks like creating apps, importing data,
|
|
6
|
+
* accessing system settings, etc.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import { useNavigate } from 'react-router-dom';
|
|
11
|
+
import { useObjectTranslation } from '@object-ui/i18n';
|
|
12
|
+
import { Card, CardContent } from '@object-ui/components';
|
|
13
|
+
import { Plus, Settings, Database } from 'lucide-react';
|
|
14
|
+
import { cn } from '@object-ui/components';
|
|
15
|
+
export function QuickActions() {
|
|
16
|
+
const navigate = useNavigate();
|
|
17
|
+
const { t } = useObjectTranslation();
|
|
18
|
+
const actions = [
|
|
19
|
+
{
|
|
20
|
+
id: 'create-app',
|
|
21
|
+
label: t('home.quickActions.createApp', { defaultValue: 'Create App' }),
|
|
22
|
+
description: t('home.quickActions.createAppDesc', { defaultValue: 'Start with a new application' }),
|
|
23
|
+
icon: Plus,
|
|
24
|
+
href: '/create-app',
|
|
25
|
+
color: 'text-blue-600 dark:text-blue-400',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'manage-objects',
|
|
29
|
+
label: t('home.quickActions.manageObjects', { defaultValue: 'Manage Objects' }),
|
|
30
|
+
description: t('home.quickActions.manageObjectsDesc', { defaultValue: 'Configure data models' }),
|
|
31
|
+
icon: Database,
|
|
32
|
+
href: '/apps/setup/system/metadata/object',
|
|
33
|
+
color: 'text-purple-600 dark:text-purple-400',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'system-settings',
|
|
37
|
+
label: t('home.quickActions.systemSettings', { defaultValue: 'System Settings' }),
|
|
38
|
+
description: t('home.quickActions.systemSettingsDesc', { defaultValue: 'Configure your workspace' }),
|
|
39
|
+
icon: Settings,
|
|
40
|
+
href: '/apps/setup',
|
|
41
|
+
color: 'text-gray-600 dark:text-gray-400',
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
return (_jsxs("section", { children: [_jsx("h2", { className: "text-2xl font-semibold tracking-tight mb-4", 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) => {
|
|
45
|
+
const Icon = action.icon;
|
|
46
|
+
return (_jsx(Card, { className: "cursor-pointer hover:shadow-md transition-shadow", onClick: () => navigate(action.href), "data-testid": `quick-action-${action.id}`, role: "link", tabIndex: 0, onKeyDown: (e) => {
|
|
47
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
48
|
+
e.preventDefault();
|
|
49
|
+
navigate(action.href);
|
|
50
|
+
}
|
|
51
|
+
}, "aria-label": action.label, children: _jsx(CardContent, { className: "p-6", children: _jsxs("div", { className: "flex items-start gap-4", children: [_jsx("div", { className: cn('p-3 rounded-lg bg-muted', action.color), children: _jsx(Icon, { className: "h-6 w-6" }) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("h3", { className: "font-semibold text-base mb-1", children: action.label }), _jsx("p", { className: "text-sm text-muted-foreground", children: action.description })] })] }) }) }, action.id));
|
|
52
|
+
}) })] }));
|
|
53
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RecentApps
|
|
3
|
+
*
|
|
4
|
+
* Display section for recently accessed items (objects, dashboards, pages).
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
import type { RecentItem } from '../../hooks/useRecentItems';
|
|
9
|
+
interface RecentAppsProps {
|
|
10
|
+
items: RecentItem[];
|
|
11
|
+
}
|
|
12
|
+
export declare function RecentApps({ items }: RecentAppsProps): import("react/jsx-runtime").JSX.Element | null;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* RecentApps
|
|
4
|
+
*
|
|
5
|
+
* Display section for recently accessed items (objects, dashboards, pages).
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import { useNavigate } from 'react-router-dom';
|
|
10
|
+
import { useObjectTranslation } from '@object-ui/i18n';
|
|
11
|
+
import { Card, CardContent } from '@object-ui/components';
|
|
12
|
+
import { Clock } from 'lucide-react';
|
|
13
|
+
import { getIcon } from '../../utils/getIcon';
|
|
14
|
+
import { capitalizeFirst } from '../../utils';
|
|
15
|
+
export function RecentApps({ items }) {
|
|
16
|
+
const navigate = useNavigate();
|
|
17
|
+
const { t } = useObjectTranslation();
|
|
18
|
+
if (items.length === 0)
|
|
19
|
+
return null;
|
|
20
|
+
return (_jsxs("section", { children: [_jsxs("div", { className: "flex items-center gap-2 mb-4", children: [_jsx(Clock, { className: "h-5 w-5 text-muted-foreground" }), _jsx("h2", { className: "text-2xl font-semibold tracking-tight", children: t('home.recentApps.title', { defaultValue: 'Recently Accessed' }) })] }), _jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4", children: items.map((item) => {
|
|
21
|
+
const Icon = getIcon(item.type);
|
|
22
|
+
const typeLabel = t(`home.recentApps.itemType.${item.type}`, {
|
|
23
|
+
defaultValue: capitalizeFirst(item.type),
|
|
24
|
+
});
|
|
25
|
+
return (_jsx(Card, { className: "cursor-pointer hover:shadow-md transition-shadow", onClick: () => navigate(item.href), "data-testid": `recent-item-${item.id}`, role: "link", tabIndex: 0, onKeyDown: (e) => {
|
|
26
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
27
|
+
e.preventDefault();
|
|
28
|
+
navigate(item.href);
|
|
29
|
+
}
|
|
30
|
+
}, children: _jsx(CardContent, { className: "p-4", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "p-2 rounded-lg bg-muted shrink-0", 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 })] })] }) }) }, item.id));
|
|
31
|
+
}) })] }));
|
|
32
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StarredApps
|
|
3
|
+
*
|
|
4
|
+
* Display section for starred/favorite items (objects, dashboards, pages).
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
import type { FavoriteItem } from '../../hooks/useFavorites';
|
|
9
|
+
interface StarredAppsProps {
|
|
10
|
+
items: FavoriteItem[];
|
|
11
|
+
}
|
|
12
|
+
export declare function StarredApps({ items }: StarredAppsProps): import("react/jsx-runtime").JSX.Element | null;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* StarredApps
|
|
4
|
+
*
|
|
5
|
+
* Display section for starred/favorite items (objects, dashboards, pages).
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import { useNavigate } from 'react-router-dom';
|
|
10
|
+
import { useObjectTranslation } from '@object-ui/i18n';
|
|
11
|
+
import { Card, CardContent } from '@object-ui/components';
|
|
12
|
+
import { Star } from 'lucide-react';
|
|
13
|
+
import { getIcon } from '../../utils/getIcon';
|
|
14
|
+
import { capitalizeFirst } from '../../utils';
|
|
15
|
+
export function StarredApps({ items }) {
|
|
16
|
+
const navigate = useNavigate();
|
|
17
|
+
const { t } = useObjectTranslation();
|
|
18
|
+
if (items.length === 0)
|
|
19
|
+
return null;
|
|
20
|
+
return (_jsxs("section", { children: [_jsxs("div", { className: "flex items-center gap-2 mb-4", children: [_jsx(Star, { className: "h-5 w-5 text-yellow-500 fill-yellow-500" }), _jsx("h2", { className: "text-2xl font-semibold tracking-tight", children: t('home.starredApps.title', { defaultValue: 'Starred' }) })] }), _jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4", children: items.map((item) => {
|
|
21
|
+
const Icon = getIcon(item.type);
|
|
22
|
+
return (_jsx(Card, { className: "cursor-pointer hover:shadow-md transition-shadow", onClick: () => navigate(item.href), "data-testid": `starred-item-${item.id}`, role: "link", tabIndex: 0, onKeyDown: (e) => {
|
|
23
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
24
|
+
e.preventDefault();
|
|
25
|
+
navigate(item.href);
|
|
26
|
+
}
|
|
27
|
+
}, children: _jsx(CardContent, { className: "p-4", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "p-2 rounded-lg bg-muted shrink-0", 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) })] })] }) }) }, item.id));
|
|
28
|
+
}) })] }));
|
|
29
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Home Page Components
|
|
3
|
+
*
|
|
4
|
+
* Exports for the unified home dashboard components
|
|
5
|
+
*/
|
|
6
|
+
export { HomePage } from './HomePage';
|
|
7
|
+
export { HomeLayout } from './HomeLayout';
|
|
8
|
+
export { QuickActions } from './QuickActions';
|
|
9
|
+
export { AppCard } from './AppCard';
|
|
10
|
+
export { RecentApps } from './RecentApps';
|
|
11
|
+
export { StarredApps } from './StarredApps';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Home Page Components
|
|
3
|
+
*
|
|
4
|
+
* Exports for the unified home dashboard components
|
|
5
|
+
*/
|
|
6
|
+
export { HomePage } from './HomePage';
|
|
7
|
+
export { HomeLayout } from './HomeLayout';
|
|
8
|
+
export { QuickActions } from './QuickActions';
|
|
9
|
+
export { AppCard } from './AppCard';
|
|
10
|
+
export { RecentApps } from './RecentApps';
|
|
11
|
+
export { StarredApps } from './StarredApps';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CreateWorkspaceDialog
|
|
3
|
+
*
|
|
4
|
+
* Dialog for creating a new workspace (organization).
|
|
5
|
+
* Auto-generates a slug from the name.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import type { AuthOrganization } from '@object-ui/auth';
|
|
10
|
+
interface CreateWorkspaceDialogProps {
|
|
11
|
+
open: boolean;
|
|
12
|
+
onOpenChange: (open: boolean) => void;
|
|
13
|
+
onCreated?: (org: AuthOrganization) => void;
|
|
14
|
+
}
|
|
15
|
+
export declare function CreateWorkspaceDialog({ open, onOpenChange, onCreated, }: CreateWorkspaceDialogProps): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* CreateWorkspaceDialog
|
|
4
|
+
*
|
|
5
|
+
* Dialog for creating a new workspace (organization).
|
|
6
|
+
* Auto-generates a slug from the name.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import { useState, useCallback, useEffect } from 'react';
|
|
11
|
+
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Button, Input, Label, } from '@object-ui/components';
|
|
12
|
+
import { useAuth } from '@object-ui/auth';
|
|
13
|
+
import { useObjectTranslation } from '@object-ui/i18n';
|
|
14
|
+
import { Loader2 } from 'lucide-react';
|
|
15
|
+
/** Convert a display name to a URL-friendly slug */
|
|
16
|
+
function nameToSlug(name) {
|
|
17
|
+
return name
|
|
18
|
+
.toLowerCase()
|
|
19
|
+
.replace(/[^a-z0-9\s_-]/g, '')
|
|
20
|
+
.replace(/[\s_]+/g, '-')
|
|
21
|
+
.replace(/-+/g, '-')
|
|
22
|
+
.replace(/^-|-$/g, '')
|
|
23
|
+
.slice(0, 48);
|
|
24
|
+
}
|
|
25
|
+
export function CreateWorkspaceDialog({ open, onOpenChange, onCreated, }) {
|
|
26
|
+
const { t } = useObjectTranslation();
|
|
27
|
+
const { createOrganization } = useAuth();
|
|
28
|
+
const [name, setName] = useState('');
|
|
29
|
+
const [slug, setSlug] = useState('');
|
|
30
|
+
const [slugManuallyEdited, setSlugManuallyEdited] = useState(false);
|
|
31
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
32
|
+
const [error, setError] = useState(null);
|
|
33
|
+
// Auto-generate slug from name (unless manually edited)
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!slugManuallyEdited) {
|
|
36
|
+
setSlug(nameToSlug(name));
|
|
37
|
+
}
|
|
38
|
+
}, [name, slugManuallyEdited]);
|
|
39
|
+
// Reset form when dialog opens
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (open) {
|
|
42
|
+
setName('');
|
|
43
|
+
setSlug('');
|
|
44
|
+
setSlugManuallyEdited(false);
|
|
45
|
+
setError(null);
|
|
46
|
+
}
|
|
47
|
+
}, [open]);
|
|
48
|
+
const handleSubmit = useCallback(async (e) => {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
if (!name.trim() || !slug.trim())
|
|
51
|
+
return;
|
|
52
|
+
setIsSubmitting(true);
|
|
53
|
+
setError(null);
|
|
54
|
+
try {
|
|
55
|
+
const org = await createOrganization({ name: name.trim(), slug: slug.trim() });
|
|
56
|
+
onCreated?.(org);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
setError(err instanceof Error ? err.message : 'Failed to create workspace');
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
setIsSubmitting(false);
|
|
63
|
+
}
|
|
64
|
+
}, [name, slug, createOrganization, onCreated]);
|
|
65
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsx(DialogContent, { className: "sm:max-w-[425px]", "data-testid": "create-workspace-dialog", children: _jsxs("form", { onSubmit: handleSubmit, children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: t('workspace.createTitle', { defaultValue: 'Create a workspace' }) }), _jsx(DialogDescription, { children: t('workspace.createDescription', {
|
|
66
|
+
defaultValue: 'A workspace is a shared space for your team to collaborate.',
|
|
67
|
+
}) })] }), _jsxs("div", { className: "grid gap-4 py-4", children: [_jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { htmlFor: "workspace-name", children: t('workspace.nameLabel', { defaultValue: 'Workspace name' }) }), _jsx(Input, { id: "workspace-name", placeholder: t('workspace.namePlaceholder', { defaultValue: 'e.g., Acme Inc' }), value: name, onChange: (e) => setName(e.target.value), autoFocus: true, "data-testid": "workspace-name-input" })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { htmlFor: "workspace-slug", children: t('workspace.slugLabel', { defaultValue: 'URL slug' }) }), _jsx(Input, { id: "workspace-slug", placeholder: "acme-inc", value: slug, onChange: (e) => {
|
|
68
|
+
setSlug(e.target.value);
|
|
69
|
+
setSlugManuallyEdited(true);
|
|
70
|
+
}, "data-testid": "workspace-slug-input" }), _jsx("p", { className: "text-xs text-muted-foreground", children: t('workspace.slugHint', { defaultValue: 'Used in URLs. Only lowercase letters, numbers, and hyphens.' }) })] }), error && (_jsx("p", { className: "text-sm text-destructive", "data-testid": "workspace-create-error", children: error }))] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "outline", onClick: () => onOpenChange(false), disabled: isSubmitting, children: t('common.cancel', { defaultValue: 'Cancel' }) }), _jsxs(Button, { type: "submit", disabled: isSubmitting || !name.trim() || !slug.trim(), "data-testid": "workspace-create-submit", children: [isSubmitting && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), t('workspace.createButton', { defaultValue: 'Create workspace' })] })] })] }) }) }));
|
|
71
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OrganizationsLayout
|
|
3
|
+
*
|
|
4
|
+
* Landing layout for `/organizations`. Uses the unified `AppHeader` in `orgs`
|
|
5
|
+
* variant and has no sidebar. Shown when the user has not selected an
|
|
6
|
+
* organization or explicitly navigates to browse/create organizations.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import React from 'react';
|
|
11
|
+
interface OrganizationsLayoutProps {
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
}
|
|
14
|
+
export declare function OrganizationsLayout({ children }: OrganizationsLayoutProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* OrganizationsLayout
|
|
4
|
+
*
|
|
5
|
+
* Landing layout for `/organizations`. Uses the unified `AppHeader` in `orgs`
|
|
6
|
+
* variant and has no sidebar. Shown when the user has not selected an
|
|
7
|
+
* organization or explicitly navigates to browse/create organizations.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
import { useEffect } from 'react';
|
|
12
|
+
import { useNavigationContext } from '../../context/NavigationContext';
|
|
13
|
+
import { AppHeader } from '../../layout/AppHeader';
|
|
14
|
+
export function OrganizationsLayout({ children }) {
|
|
15
|
+
const { setContext } = useNavigationContext();
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
setContext('home');
|
|
18
|
+
}, [setContext]);
|
|
19
|
+
return (_jsxs("div", { className: "flex min-h-svh w-full flex-col bg-background", "data-testid": "organizations-layout", children: [_jsx("header", { className: "sticky top-0 z-30 flex h-14 w-full shrink-0 items-center gap-2 border-b bg-background px-2 sm:px-4", children: _jsx(AppHeader, { variant: "orgs" }) }), _jsx("main", { className: "flex-1 min-w-0 overflow-auto", children: children })] }));
|
|
20
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OrganizationsPage
|
|
3
|
+
*
|
|
4
|
+
* Supabase-style landing page that lists the organizations the current user
|
|
5
|
+
* belongs to. Users can search, select (which switches the active
|
|
6
|
+
* organization and navigates to `/home`), or create a new organization.
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
export declare function OrganizationsPage(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* OrganizationsPage
|
|
4
|
+
*
|
|
5
|
+
* Supabase-style landing page that lists the organizations the current user
|
|
6
|
+
* belongs to. Users can search, select (which switches the active
|
|
7
|
+
* organization and navigates to `/home`), or create a new organization.
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import { useMemo, useState } from 'react';
|
|
11
|
+
import { Avatar, AvatarImage, AvatarFallback, Button, Input, Empty, EmptyTitle, EmptyDescription, } from '@object-ui/components';
|
|
12
|
+
import { Plus, Search, Loader2 } from 'lucide-react';
|
|
13
|
+
import { useAuth } from '@object-ui/auth';
|
|
14
|
+
import { useObjectTranslation } from '@object-ui/i18n';
|
|
15
|
+
import { useNavigate } from 'react-router-dom';
|
|
16
|
+
import { CreateWorkspaceDialog } from './CreateWorkspaceDialog';
|
|
17
|
+
function getOrgInitials(name) {
|
|
18
|
+
return name
|
|
19
|
+
.split(/[\s_-]+/)
|
|
20
|
+
.map((w) => w[0])
|
|
21
|
+
.join('')
|
|
22
|
+
.toUpperCase()
|
|
23
|
+
.slice(0, 2);
|
|
24
|
+
}
|
|
25
|
+
export function OrganizationsPage() {
|
|
26
|
+
const { t } = useObjectTranslation();
|
|
27
|
+
const navigate = useNavigate();
|
|
28
|
+
const { organizations, activeOrganization, isOrganizationsLoading, switchOrganization, } = useAuth();
|
|
29
|
+
const [query, setQuery] = useState('');
|
|
30
|
+
const [isCreateOpen, setIsCreateOpen] = useState(false);
|
|
31
|
+
const [switchingId, setSwitchingId] = useState(null);
|
|
32
|
+
const orgList = organizations ?? [];
|
|
33
|
+
const filtered = useMemo(() => {
|
|
34
|
+
const q = query.trim().toLowerCase();
|
|
35
|
+
if (!q)
|
|
36
|
+
return orgList;
|
|
37
|
+
return orgList.filter((o) => o.name.toLowerCase().includes(q) || o.slug?.toLowerCase().includes(q));
|
|
38
|
+
}, [orgList, query]);
|
|
39
|
+
const handleSelect = async (org) => {
|
|
40
|
+
setSwitchingId(org.id);
|
|
41
|
+
try {
|
|
42
|
+
if (org.id !== activeOrganization?.id) {
|
|
43
|
+
await switchOrganization(org.id);
|
|
44
|
+
}
|
|
45
|
+
const base = import.meta.env.BASE_URL || '/';
|
|
46
|
+
const normalizedBase = base.endsWith('/') ? base : `${base}/`;
|
|
47
|
+
window.location.href = `${window.location.origin}${normalizedBase}home`;
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
console.error('[OrganizationsPage] Failed to switch:', err);
|
|
51
|
+
setSwitchingId(null);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const handleCreated = (org) => {
|
|
55
|
+
setIsCreateOpen(false);
|
|
56
|
+
handleSelect(org);
|
|
57
|
+
};
|
|
58
|
+
if (isOrganizationsLoading) {
|
|
59
|
+
return (_jsx("div", { className: "flex flex-1 items-center justify-center py-20", children: _jsx(Loader2, { className: "h-5 w-5 animate-spin text-muted-foreground" }) }));
|
|
60
|
+
}
|
|
61
|
+
return (_jsxs("div", { className: "mx-auto w-full max-w-5xl px-4 sm:px-6 py-8 sm:py-12", children: [_jsxs("div", { className: "mb-8", children: [_jsx("h1", { className: "text-2xl sm:text-3xl font-bold tracking-tight", children: t('organizations.heading', { defaultValue: 'Your Organizations' }) }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: t('organizations.subtitle', {
|
|
62
|
+
defaultValue: 'Select an organization to continue, or create a new one.',
|
|
63
|
+
}) })] }), _jsxs("div", { className: "mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between", children: [_jsxs("div", { className: "relative w-full sm:max-w-sm", children: [_jsx(Search, { className: "pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { value: query, onChange: (e) => setQuery(e.target.value), placeholder: t('organizations.searchPlaceholder', {
|
|
64
|
+
defaultValue: 'Search for an organization',
|
|
65
|
+
}), className: "pl-9", "data-testid": "organizations-search" })] }), _jsxs(Button, { onClick: () => setIsCreateOpen(true), "data-testid": "organizations-new", children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), t('organizations.new', { defaultValue: 'New organization' })] })] }), orgList.length === 0 ? (_jsxs(Empty, { children: [_jsx(EmptyTitle, { children: t('organizations.emptyTitle', { defaultValue: 'No organizations yet' }) }), _jsx(EmptyDescription, { children: t('organizations.emptyDescription', {
|
|
66
|
+
defaultValue: 'Create your first organization to get started.',
|
|
67
|
+
}) }), _jsxs(Button, { className: "mt-6", onClick: () => setIsCreateOpen(true), children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), t('organizations.new', { defaultValue: 'New organization' })] })] })) : filtered.length === 0 ? (_jsx("div", { className: "rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground", children: t('organizations.noMatches', {
|
|
68
|
+
defaultValue: 'No organizations match your search.',
|
|
69
|
+
}) })) : (_jsx("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2", children: filtered.map((org) => {
|
|
70
|
+
const isActive = org.id === activeOrganization?.id;
|
|
71
|
+
const isSwitching = switchingId === org.id;
|
|
72
|
+
return (_jsxs("button", { onClick: () => handleSelect(org), disabled: isSwitching, className: "group flex items-center gap-3 rounded-lg border bg-card p-4 text-left transition-colors hover:border-primary/50 hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-60", "data-testid": `organization-card-${org.slug}`, children: [_jsxs(Avatar, { className: "h-10 w-10 rounded-md shrink-0", children: [org.logo && _jsx(AvatarImage, { src: org.logo, alt: org.name }), _jsx(AvatarFallback, { className: "rounded-md bg-primary/10 text-primary text-sm font-medium", children: getOrgInitials(org.name) })] }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "truncate font-medium", children: org.name }), _jsx("div", { className: "truncate text-xs text-muted-foreground", children: isActive
|
|
73
|
+
? t('organizations.current', { defaultValue: 'Current organization' })
|
|
74
|
+
: org.slug })] }), _jsx("span", { className: "text-xs text-primary underline-offset-4 hover:underline shrink-0", onClick: (e) => { e.stopPropagation(); navigate(`/organizations/${org.slug}/members`); }, children: t('organizations.manage', { defaultValue: 'Manage' }) }), isSwitching && _jsx(Loader2, { className: "h-4 w-4 animate-spin text-muted-foreground" })] }, org.id));
|
|
75
|
+
}) })), _jsx(CreateWorkspaceDialog, { open: isCreateOpen, onOpenChange: setIsCreateOpen, onCreated: handleCreated })] }));
|
|
76
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { OrganizationsLayout } from './OrganizationsLayout';
|
|
2
|
+
export { OrganizationsPage } from './OrganizationsPage';
|
|
3
|
+
export { CreateWorkspaceDialog } from './CreateWorkspaceDialog';
|
|
4
|
+
export { OrganizationLayout } from './manage/OrganizationLayout';
|
|
5
|
+
export { MembersPage } from './manage/MembersPage';
|
|
6
|
+
export { InvitationsPage } from './manage/InvitationsPage';
|
|
7
|
+
export { SettingsPage } from './manage/SettingsPage';
|
|
8
|
+
export { AcceptInvitationPage } from './manage/AcceptInvitationPage';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { OrganizationsLayout } from './OrganizationsLayout';
|
|
2
|
+
export { OrganizationsPage } from './OrganizationsPage';
|
|
3
|
+
export { CreateWorkspaceDialog } from './CreateWorkspaceDialog';
|
|
4
|
+
export { OrganizationLayout } from './manage/OrganizationLayout';
|
|
5
|
+
export { MembersPage } from './manage/MembersPage';
|
|
6
|
+
export { InvitationsPage } from './manage/InvitationsPage';
|
|
7
|
+
export { SettingsPage } from './manage/SettingsPage';
|
|
8
|
+
export { AcceptInvitationPage } from './manage/AcceptInvitationPage';
|