@object-ui/app-shell 3.3.0 → 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 +73 -0
- package/README.md +22 -2
- package/dist/chrome/CommandPalette.d.ts +16 -0
- package/dist/chrome/CommandPalette.js +74 -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 +202 -0
- package/dist/layout/AppSwitcher.d.ts +12 -0
- package/dist/layout/AppSwitcher.js +24 -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/ConsoleFloatingChatbot.d.ts +10 -0
- package/dist/layout/ConsoleFloatingChatbot.js +27 -0
- package/dist/layout/ConsoleLayout.d.ts +22 -0
- package/dist/layout/ConsoleLayout.js +49 -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 +27 -0
- package/dist/layout/UnifiedSidebar.js +186 -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 +24 -0
- package/dist/utils/getIcon.js +87 -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 +11 -0
- package/dist/views/ObjectView.js +769 -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 +285 -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,100 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* AcceptInvitationPage
|
|
4
|
+
*
|
|
5
|
+
* Standalone page for accepting or rejecting an organization invitation.
|
|
6
|
+
* Route: /accept-invitation/:invitationId
|
|
7
|
+
*/
|
|
8
|
+
import { useEffect, useState } from 'react';
|
|
9
|
+
import { useNavigate, useParams } from 'react-router-dom';
|
|
10
|
+
import { Button } from '@object-ui/components';
|
|
11
|
+
import { useAuth } from '@object-ui/auth';
|
|
12
|
+
import { useObjectTranslation } from '@object-ui/i18n';
|
|
13
|
+
import { Loader2, Building2, CheckCircle, XCircle } from 'lucide-react';
|
|
14
|
+
import { toast } from 'sonner';
|
|
15
|
+
export function AcceptInvitationPage() {
|
|
16
|
+
const { t } = useObjectTranslation();
|
|
17
|
+
const navigate = useNavigate();
|
|
18
|
+
const { invitationId } = useParams();
|
|
19
|
+
const { isAuthenticated, isLoading: isAuthLoading, getInvitation, acceptInvitation, rejectInvitation, switchOrganization } = useAuth();
|
|
20
|
+
const [invitation, setInvitation] = useState(null);
|
|
21
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
22
|
+
const [error, setError] = useState(null);
|
|
23
|
+
const [isAccepting, setIsAccepting] = useState(false);
|
|
24
|
+
const [isDeclining, setIsDeclining] = useState(false);
|
|
25
|
+
// Redirect to login if not authenticated
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!isAuthLoading && !isAuthenticated) {
|
|
28
|
+
navigate('/login?redirect=' + encodeURIComponent(window.location.pathname));
|
|
29
|
+
}
|
|
30
|
+
}, [isAuthenticated, isAuthLoading, navigate]);
|
|
31
|
+
// Fetch invitation details
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (!invitationId || !isAuthenticated)
|
|
34
|
+
return;
|
|
35
|
+
let cancelled = false;
|
|
36
|
+
setIsLoading(true);
|
|
37
|
+
setError(null);
|
|
38
|
+
getInvitation(invitationId)
|
|
39
|
+
.then((inv) => {
|
|
40
|
+
if (!cancelled)
|
|
41
|
+
setInvitation(inv);
|
|
42
|
+
})
|
|
43
|
+
.catch((err) => {
|
|
44
|
+
if (!cancelled)
|
|
45
|
+
setError(err instanceof Error ? err.message : 'Invitation not found or expired');
|
|
46
|
+
})
|
|
47
|
+
.finally(() => {
|
|
48
|
+
if (!cancelled)
|
|
49
|
+
setIsLoading(false);
|
|
50
|
+
});
|
|
51
|
+
return () => {
|
|
52
|
+
cancelled = true;
|
|
53
|
+
};
|
|
54
|
+
}, [invitationId, isAuthenticated, getInvitation]);
|
|
55
|
+
const handleAccept = async () => {
|
|
56
|
+
if (!invitation || !invitationId)
|
|
57
|
+
return;
|
|
58
|
+
setIsAccepting(true);
|
|
59
|
+
try {
|
|
60
|
+
await acceptInvitation(invitationId);
|
|
61
|
+
await switchOrganization(invitation.organizationId).catch(() => null);
|
|
62
|
+
toast.success(t('organization.accept.accepted', { defaultValue: 'Invitation accepted' }));
|
|
63
|
+
navigate('/home');
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
toast.error(err instanceof Error
|
|
67
|
+
? err.message
|
|
68
|
+
: t('organization.accept.acceptFailed', { defaultValue: 'Failed to accept invitation' }));
|
|
69
|
+
setIsAccepting(false);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
const handleDecline = async () => {
|
|
73
|
+
if (!invitationId)
|
|
74
|
+
return;
|
|
75
|
+
setIsDeclining(true);
|
|
76
|
+
try {
|
|
77
|
+
await rejectInvitation(invitationId);
|
|
78
|
+
toast.success(t('organization.accept.declined', { defaultValue: 'Invitation declined' }));
|
|
79
|
+
navigate('/organizations');
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
toast.error(err instanceof Error
|
|
83
|
+
? err.message
|
|
84
|
+
: t('organization.accept.declineFailed', { defaultValue: 'Failed to decline invitation' }));
|
|
85
|
+
setIsDeclining(false);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
// Show spinner while auth is loading
|
|
89
|
+
if (isAuthLoading) {
|
|
90
|
+
return (_jsx("div", { className: "flex min-h-svh items-center justify-center", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-muted-foreground" }) }));
|
|
91
|
+
}
|
|
92
|
+
// Will redirect if not authenticated — show nothing in the meantime
|
|
93
|
+
if (!isAuthenticated)
|
|
94
|
+
return null;
|
|
95
|
+
return (_jsx("div", { className: "flex min-h-svh items-center justify-center px-4 py-12 bg-background", children: _jsx("div", { className: "w-full max-w-md rounded-xl border bg-card p-8 shadow-sm", "data-testid": "accept-invitation-page", children: isLoading ? (_jsxs("div", { className: "flex flex-col items-center gap-4 py-8", children: [_jsx(Loader2, { className: "h-8 w-8 animate-spin text-muted-foreground" }), _jsx("p", { className: "text-sm text-muted-foreground", children: t('organization.accept.loading', { defaultValue: 'Loading invitation…' }) })] })) : error ? (_jsxs("div", { className: "flex flex-col items-center gap-4 py-8 text-center", children: [_jsx(XCircle, { className: "h-10 w-10 text-destructive" }), _jsx("h1", { className: "text-xl font-semibold", children: t('organization.accept.errorTitle', { defaultValue: 'Invitation unavailable' }) }), _jsx("p", { className: "text-sm text-muted-foreground", children: error }), _jsx(Button, { variant: "outline", onClick: () => navigate('/organizations'), children: t('organization.accept.goToOrgs', { defaultValue: 'Go to organizations' }) })] })) : invitation ? (_jsxs("div", { className: "flex flex-col items-center gap-6 text-center", children: [_jsx("div", { className: "flex h-14 w-14 items-center justify-center rounded-full bg-primary/10", children: _jsx(Building2, { className: "h-7 w-7 text-primary" }) }), _jsxs("div", { children: [_jsx("h1", { className: "text-xl font-bold", children: t('organization.accept.title', { defaultValue: 'You have been invited' }) }), _jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: t('organization.accept.description', {
|
|
96
|
+
defaultValue: 'You have been invited to join {{orgName}} as {{role}}.',
|
|
97
|
+
orgName: invitation.organizationName ?? invitation.organizationId,
|
|
98
|
+
role: invitation.role,
|
|
99
|
+
}) })] }), _jsxs("div", { className: "w-full rounded-lg border bg-muted/50 p-4 text-left space-y-2", children: [_jsxs("div", { className: "flex justify-between text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: t('organization.accept.organization', { defaultValue: 'Organization' }) }), _jsx("span", { className: "font-medium", children: invitation.organizationName ?? invitation.organizationId })] }), _jsxs("div", { className: "flex justify-between text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: t('organization.accept.role', { defaultValue: 'Role' }) }), _jsx("span", { className: "font-medium capitalize", children: invitation.role })] }), invitation.expiresAt && (_jsxs("div", { className: "flex justify-between text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: t('organization.accept.expiresAt', { defaultValue: 'Expires' }) }), _jsx("span", { className: "font-medium", children: new Date(invitation.expiresAt).toLocaleDateString() })] }))] }), _jsxs("div", { className: "flex w-full gap-3", children: [_jsxs(Button, { variant: "outline", className: "flex-1", onClick: handleDecline, disabled: isDeclining || isAccepting, children: [isDeclining && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), t('organization.accept.decline', { defaultValue: 'Decline' })] }), _jsxs(Button, { className: "flex-1", onClick: handleAccept, disabled: isAccepting || isDeclining, children: [isAccepting && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), _jsx(CheckCircle, { className: "mr-2 h-4 w-4" }), t('organization.accept.accept', { defaultValue: 'Accept invitation' })] })] })] })) : null }) }));
|
|
100
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* InvitationsPage
|
|
4
|
+
*
|
|
5
|
+
* Lists organization invitations with filter tabs and per-invitation actions.
|
|
6
|
+
*/
|
|
7
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
8
|
+
import { Avatar, AvatarFallback, Badge, Button, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Separator, } from '@object-ui/components';
|
|
9
|
+
import { useAuth } from '@object-ui/auth';
|
|
10
|
+
import { useObjectTranslation } from '@object-ui/i18n';
|
|
11
|
+
import { Loader2, Copy, Check, X, Mail } from 'lucide-react';
|
|
12
|
+
import { toast } from 'sonner';
|
|
13
|
+
import { useOrgContext } from './orgContext';
|
|
14
|
+
function buildAcceptUrl(invitationId) {
|
|
15
|
+
const base = import.meta.env?.BASE_URL ?? '/';
|
|
16
|
+
const normalized = base.endsWith('/') ? base : `${base}/`;
|
|
17
|
+
return `${window.location.origin}${normalized}accept-invitation/${invitationId}`;
|
|
18
|
+
}
|
|
19
|
+
function statusBadgeVariant(status) {
|
|
20
|
+
switch (status) {
|
|
21
|
+
case 'pending':
|
|
22
|
+
return 'outline';
|
|
23
|
+
case 'accepted':
|
|
24
|
+
return 'default';
|
|
25
|
+
case 'rejected':
|
|
26
|
+
case 'canceled':
|
|
27
|
+
return 'destructive';
|
|
28
|
+
default:
|
|
29
|
+
return 'secondary';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function InvitationsPage() {
|
|
33
|
+
const { t } = useObjectTranslation();
|
|
34
|
+
const { org } = useOrgContext();
|
|
35
|
+
const { listInvitations, cancelInvitation } = useAuth();
|
|
36
|
+
const [invitations, setInvitations] = useState([]);
|
|
37
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
38
|
+
const [error, setError] = useState(null);
|
|
39
|
+
const [filter, setFilter] = useState('all');
|
|
40
|
+
const [cancelingInvitation, setCancelingInvitation] = useState(null);
|
|
41
|
+
const [copiedId, setCopiedId] = useState(null);
|
|
42
|
+
const fetchInvitations = useCallback(async () => {
|
|
43
|
+
setIsLoading(true);
|
|
44
|
+
setError(null);
|
|
45
|
+
try {
|
|
46
|
+
const data = await listInvitations(org.id);
|
|
47
|
+
setInvitations(data);
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
setError(err instanceof Error ? err.message : 'Failed to load invitations');
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
setIsLoading(false);
|
|
54
|
+
}
|
|
55
|
+
}, [org.id, listInvitations]);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
fetchInvitations();
|
|
58
|
+
}, [fetchInvitations]);
|
|
59
|
+
const handleCopyLink = async (inv) => {
|
|
60
|
+
const url = buildAcceptUrl(inv.id);
|
|
61
|
+
try {
|
|
62
|
+
await navigator.clipboard.writeText(url);
|
|
63
|
+
setCopiedId(inv.id);
|
|
64
|
+
toast.success(t('organization.invitations.linkCopied', { defaultValue: 'Invitation link copied' }));
|
|
65
|
+
setTimeout(() => setCopiedId(null), 2000);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
toast.error(t('organization.invitations.copyFailed', { defaultValue: 'Failed to copy link' }));
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const handleCancel = async () => {
|
|
72
|
+
if (!cancelingInvitation)
|
|
73
|
+
return;
|
|
74
|
+
try {
|
|
75
|
+
await cancelInvitation(cancelingInvitation.id);
|
|
76
|
+
toast.success(t('organization.invitations.canceled', { defaultValue: 'Invitation canceled' }));
|
|
77
|
+
setCancelingInvitation(null);
|
|
78
|
+
fetchInvitations();
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
toast.error(err instanceof Error
|
|
82
|
+
? err.message
|
|
83
|
+
: t('organization.invitations.cancelFailed', { defaultValue: 'Failed to cancel invitation' }));
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const filtered = filter === 'all' ? invitations : invitations.filter((inv) => inv.status === filter);
|
|
87
|
+
const statusCounts = {
|
|
88
|
+
all: invitations.length,
|
|
89
|
+
pending: invitations.filter((i) => i.status === 'pending').length,
|
|
90
|
+
accepted: invitations.filter((i) => i.status === 'accepted').length,
|
|
91
|
+
rejected: invitations.filter((i) => i.status === 'rejected').length,
|
|
92
|
+
canceled: invitations.filter((i) => i.status === 'canceled').length,
|
|
93
|
+
};
|
|
94
|
+
const tabs = ['all', 'pending', 'accepted', 'rejected', 'canceled'];
|
|
95
|
+
if (isLoading) {
|
|
96
|
+
return (_jsx("div", { className: "flex items-center justify-center py-20", children: _jsx(Loader2, { className: "h-5 w-5 animate-spin text-muted-foreground" }) }));
|
|
97
|
+
}
|
|
98
|
+
if (error) {
|
|
99
|
+
return (_jsxs("div", { className: "rounded-lg border border-destructive/50 bg-destructive/10 p-6 text-center text-sm text-destructive", children: [error, _jsx(Button, { variant: "outline", size: "sm", className: "ml-4", onClick: fetchInvitations, children: t('common.retry', { defaultValue: 'Retry' }) })] }));
|
|
100
|
+
}
|
|
101
|
+
return (_jsxs("div", { className: "space-y-4", "data-testid": "invitations-page", children: [_jsx("div", { className: "flex items-center justify-between", children: _jsx("h2", { className: "text-lg font-semibold", children: t('organization.invitations.title', { defaultValue: 'Invitations' }) }) }), _jsx("div", { className: "flex gap-1 border-b", children: tabs.map((tab) => (_jsxs("button", { onClick: () => setFilter(tab), className: `px-3 py-2 text-sm font-medium border-b-2 transition-colors capitalize ${filter === tab
|
|
102
|
+
? 'border-primary text-foreground'
|
|
103
|
+
: 'border-transparent text-muted-foreground hover:text-foreground'}`, children: [t(`organization.invitations.status.${tab}`, { defaultValue: tab }), statusCounts[tab] > 0 && (_jsx("span", { className: "ml-1.5 rounded-full bg-muted px-1.5 py-0.5 text-xs", children: statusCounts[tab] }))] }, tab))) }), _jsx(Separator, {}), filtered.length === 0 ? (_jsx("div", { className: "rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground", children: t('organization.invitations.empty', { defaultValue: 'No invitations found.' }) })) : (_jsx("div", { className: "space-y-2", children: filtered.map((inv) => (_jsxs("div", { className: "flex items-center gap-3 rounded-lg border bg-card p-3", "data-testid": `invitation-row-${inv.id}`, children: [_jsx(Avatar, { className: "h-9 w-9 shrink-0", children: _jsx(AvatarFallback, { className: "text-xs", children: _jsx(Mail, { className: "h-4 w-4" }) }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "truncate font-medium text-sm", children: inv.email }), inv.expiresAt && (_jsxs("div", { className: "text-xs text-muted-foreground", children: [t('organization.invitations.expiresAt', { defaultValue: 'Expires' }), ' ', new Date(inv.expiresAt).toLocaleDateString()] }))] }), _jsx(Badge, { variant: "outline", className: "shrink-0 capitalize", children: inv.role }), _jsx(Badge, { variant: statusBadgeVariant(inv.status), className: "shrink-0 capitalize", children: t(`organization.invitations.status.${inv.status}`, { defaultValue: inv.status }) }), inv.status === 'pending' && (_jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [_jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8", onClick: () => handleCopyLink(inv), "aria-label": "Copy invitation link", children: copiedId === inv.id ? (_jsx(Check, { className: "h-4 w-4 text-green-600" })) : (_jsx(Copy, { className: "h-4 w-4" })) }), _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 text-destructive hover:text-destructive", onClick: () => setCancelingInvitation(inv), "aria-label": "Cancel invitation", children: _jsx(X, { className: "h-4 w-4" }) })] }))] }, inv.id))) })), _jsx(AlertDialog, { open: !!cancelingInvitation, onOpenChange: (open) => !open && setCancelingInvitation(null), children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: t('organization.invitations.cancelTitle', { defaultValue: 'Cancel invitation?' }) }), _jsx(AlertDialogDescription, { children: t('organization.invitations.cancelDescription', {
|
|
104
|
+
defaultValue: 'The invitation for {{email}} will be revoked.',
|
|
105
|
+
email: cancelingInvitation?.email ?? '',
|
|
106
|
+
}) })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: t('common.cancel', { defaultValue: 'Cancel' }) }), _jsx(AlertDialogAction, { className: "bg-destructive text-destructive-foreground hover:bg-destructive/90", onClick: handleCancel, children: t('organization.invitations.cancelAction', { defaultValue: 'Cancel invitation' }) })] })] }) })] }));
|
|
107
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InviteMemberDialog
|
|
3
|
+
*
|
|
4
|
+
* Invites a new member to an organization. After creation, switches to a
|
|
5
|
+
* "share link" view so operators can copy the accept-invitation URL when no
|
|
6
|
+
* mailer is configured server-side.
|
|
7
|
+
*/
|
|
8
|
+
import type { AuthInvitation } from '@object-ui/auth';
|
|
9
|
+
interface InviteMemberDialogProps {
|
|
10
|
+
organizationId: string;
|
|
11
|
+
open: boolean;
|
|
12
|
+
onOpenChange: (open: boolean) => void;
|
|
13
|
+
onInvited?: (invitation: AuthInvitation) => void;
|
|
14
|
+
}
|
|
15
|
+
export declare function InviteMemberDialog({ organizationId, open, onOpenChange, onInvited, }: InviteMemberDialogProps): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* InviteMemberDialog
|
|
4
|
+
*
|
|
5
|
+
* Invites a new member to an organization. After creation, switches to a
|
|
6
|
+
* "share link" view so operators can copy the accept-invitation URL when no
|
|
7
|
+
* mailer is configured server-side.
|
|
8
|
+
*/
|
|
9
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
10
|
+
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Button, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@object-ui/components';
|
|
11
|
+
import { useAuth } from '@object-ui/auth';
|
|
12
|
+
import { useObjectTranslation } from '@object-ui/i18n';
|
|
13
|
+
import { Loader2, Copy, Check } from 'lucide-react';
|
|
14
|
+
import { toast } from 'sonner';
|
|
15
|
+
function buildAcceptUrl(invitationId) {
|
|
16
|
+
const base = import.meta.env?.BASE_URL ?? '/';
|
|
17
|
+
const normalized = base.endsWith('/') ? base : `${base}/`;
|
|
18
|
+
return `${window.location.origin}${normalized}accept-invitation/${invitationId}`;
|
|
19
|
+
}
|
|
20
|
+
export function InviteMemberDialog({ organizationId, open, onOpenChange, onInvited, }) {
|
|
21
|
+
const { t } = useObjectTranslation();
|
|
22
|
+
const { inviteMember } = useAuth();
|
|
23
|
+
const [email, setEmail] = useState('');
|
|
24
|
+
const [role, setRole] = useState('member');
|
|
25
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
26
|
+
const [error, setError] = useState(null);
|
|
27
|
+
const [createdInvitation, setCreatedInvitation] = useState(null);
|
|
28
|
+
const [copied, setCopied] = useState(false);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (open) {
|
|
31
|
+
setEmail('');
|
|
32
|
+
setRole('member');
|
|
33
|
+
setError(null);
|
|
34
|
+
setCreatedInvitation(null);
|
|
35
|
+
setCopied(false);
|
|
36
|
+
}
|
|
37
|
+
}, [open]);
|
|
38
|
+
const handleSubmit = useCallback(async (e) => {
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
if (!email.trim())
|
|
41
|
+
return;
|
|
42
|
+
setIsSubmitting(true);
|
|
43
|
+
setError(null);
|
|
44
|
+
try {
|
|
45
|
+
const inv = await inviteMember({
|
|
46
|
+
organizationId,
|
|
47
|
+
email: email.trim(),
|
|
48
|
+
role,
|
|
49
|
+
});
|
|
50
|
+
setCreatedInvitation(inv);
|
|
51
|
+
onInvited?.(inv);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
setError(err instanceof Error ? err.message : 'Failed to invite member');
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
setIsSubmitting(false);
|
|
58
|
+
}
|
|
59
|
+
}, [email, role, organizationId, inviteMember, onInvited]);
|
|
60
|
+
const handleCopy = useCallback(async () => {
|
|
61
|
+
if (!createdInvitation)
|
|
62
|
+
return;
|
|
63
|
+
const url = buildAcceptUrl(createdInvitation.id);
|
|
64
|
+
try {
|
|
65
|
+
await navigator.clipboard.writeText(url);
|
|
66
|
+
setCopied(true);
|
|
67
|
+
toast.success(t('organization.invitations.linkCopied', { defaultValue: 'Invitation link copied' }));
|
|
68
|
+
setTimeout(() => setCopied(false), 2000);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
toast.error(t('organization.invitations.copyFailed', { defaultValue: 'Failed to copy link' }));
|
|
72
|
+
}
|
|
73
|
+
}, [createdInvitation, t]);
|
|
74
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsx(DialogContent, { className: "sm:max-w-[480px]", "data-testid": "invite-member-dialog", children: createdInvitation ? (_jsxs(_Fragment, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: t('organization.invitations.sentTitle', { defaultValue: 'Invitation created' }) }), _jsx(DialogDescription, { children: t('organization.invitations.sentDescription', {
|
|
75
|
+
defaultValue: 'Share the link below with the invitee. They will need to sign in to accept.',
|
|
76
|
+
}) })] }), _jsxs("div", { className: "grid gap-3 py-4", children: [_jsx(Label, { children: t('organization.invitations.linkLabel', { defaultValue: 'Accept link' }) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { readOnly: true, value: buildAcceptUrl(createdInvitation.id), className: "font-mono text-xs", onFocus: (e) => e.currentTarget.select() }), _jsx(Button, { type: "button", variant: "outline", size: "icon", onClick: handleCopy, "aria-label": "Copy", children: copied ? _jsx(Check, { className: "h-4 w-4" }) : _jsx(Copy, { className: "h-4 w-4" }) })] }), _jsx("p", { className: "text-xs text-muted-foreground", children: t('organization.invitations.invitedAs', {
|
|
77
|
+
defaultValue: '{{email}} invited as {{role}}',
|
|
78
|
+
email: createdInvitation.email,
|
|
79
|
+
role: createdInvitation.role,
|
|
80
|
+
}) })] }), _jsx(DialogFooter, { children: _jsx(Button, { onClick: () => onOpenChange(false), children: t('common.done', { defaultValue: 'Done' }) }) })] })) : (_jsxs("form", { onSubmit: handleSubmit, children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: t('organization.invitations.inviteTitle', { defaultValue: 'Invite a member' }) }), _jsx(DialogDescription, { children: t('organization.invitations.inviteDescription', {
|
|
81
|
+
defaultValue: 'They will receive an invitation to join this organization.',
|
|
82
|
+
}) })] }), _jsxs("div", { className: "grid gap-4 py-4", children: [_jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { htmlFor: "invite-email", children: t('organization.invitations.emailLabel', { defaultValue: 'Email' }) }), _jsx(Input, { id: "invite-email", type: "email", required: true, placeholder: "user@example.com", value: email, onChange: (e) => setEmail(e.target.value), autoFocus: true, "data-testid": "invite-email-input" })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { htmlFor: "invite-role", children: t('organization.invitations.roleLabel', { defaultValue: 'Role' }) }), _jsxs(Select, { value: role, onValueChange: (v) => setRole(v), children: [_jsx(SelectTrigger, { id: "invite-role", "data-testid": "invite-role-select", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "member", children: t('organization.roles.member', { defaultValue: 'Member' }) }), _jsx(SelectItem, { value: "admin", children: t('organization.roles.admin', { defaultValue: 'Admin' }) }), _jsx(SelectItem, { value: "owner", children: t('organization.roles.owner', { defaultValue: 'Owner' }) })] })] })] }), error && (_jsx("p", { className: "text-sm text-destructive", "data-testid": "invite-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 || !email.trim(), "data-testid": "invite-submit", children: [isSubmitting && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), t('organization.invitations.sendInvite', { defaultValue: 'Send invite' })] })] })] })) }) }));
|
|
83
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* MembersPage
|
|
4
|
+
*
|
|
5
|
+
* Lists organization members, supports changing roles and removing members.
|
|
6
|
+
*/
|
|
7
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
8
|
+
import { Avatar, AvatarFallback, AvatarImage, Badge, Button, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Separator, } from '@object-ui/components';
|
|
9
|
+
import { useAuth } from '@object-ui/auth';
|
|
10
|
+
import { useObjectTranslation } from '@object-ui/i18n';
|
|
11
|
+
import { Loader2, MoreHorizontal, UserMinus, ShieldCheck } from 'lucide-react';
|
|
12
|
+
import { toast } from 'sonner';
|
|
13
|
+
import { useOrgContext } from './orgContext';
|
|
14
|
+
import { InviteMemberDialog } from './InviteMemberDialog';
|
|
15
|
+
function getMemberInitials(name) {
|
|
16
|
+
if (!name)
|
|
17
|
+
return '?';
|
|
18
|
+
return name
|
|
19
|
+
.split(/[\s_-]+/)
|
|
20
|
+
.map((w) => w[0])
|
|
21
|
+
.join('')
|
|
22
|
+
.toUpperCase()
|
|
23
|
+
.slice(0, 2);
|
|
24
|
+
}
|
|
25
|
+
export function MembersPage() {
|
|
26
|
+
const { t } = useObjectTranslation();
|
|
27
|
+
const { org } = useOrgContext();
|
|
28
|
+
const { getMembers, removeMember, updateMemberRole } = useAuth();
|
|
29
|
+
const [members, setMembers] = useState([]);
|
|
30
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
31
|
+
const [error, setError] = useState(null);
|
|
32
|
+
const [isInviteOpen, setIsInviteOpen] = useState(false);
|
|
33
|
+
// AlertDialog state
|
|
34
|
+
const [removingMember, setRemovingMember] = useState(null);
|
|
35
|
+
const fetchMembers = useCallback(async () => {
|
|
36
|
+
setIsLoading(true);
|
|
37
|
+
setError(null);
|
|
38
|
+
try {
|
|
39
|
+
const data = await getMembers(org.id);
|
|
40
|
+
setMembers(data);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
setError(err instanceof Error ? err.message : 'Failed to load members');
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
setIsLoading(false);
|
|
47
|
+
}
|
|
48
|
+
}, [org.id, getMembers]);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
fetchMembers();
|
|
51
|
+
}, [fetchMembers]);
|
|
52
|
+
const handleChangeRole = async (member, role) => {
|
|
53
|
+
try {
|
|
54
|
+
await updateMemberRole({ organizationId: org.id, memberId: member.id, role });
|
|
55
|
+
toast.success(t('organization.members.roleUpdated', { defaultValue: 'Role updated' }));
|
|
56
|
+
fetchMembers();
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
toast.error(err instanceof Error
|
|
60
|
+
? err.message
|
|
61
|
+
: t('organization.members.roleUpdateFailed', { defaultValue: 'Failed to update role' }));
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const handleRemove = async () => {
|
|
65
|
+
if (!removingMember)
|
|
66
|
+
return;
|
|
67
|
+
try {
|
|
68
|
+
await removeMember({ organizationId: org.id, memberIdOrUserId: removingMember.id });
|
|
69
|
+
toast.success(t('organization.members.memberRemoved', { defaultValue: 'Member removed' }));
|
|
70
|
+
setRemovingMember(null);
|
|
71
|
+
fetchMembers();
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
toast.error(err instanceof Error
|
|
75
|
+
? err.message
|
|
76
|
+
: t('organization.members.removeFailed', { defaultValue: 'Failed to remove member' }));
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
if (isLoading) {
|
|
80
|
+
return (_jsx("div", { className: "flex items-center justify-center py-20", children: _jsx(Loader2, { className: "h-5 w-5 animate-spin text-muted-foreground" }) }));
|
|
81
|
+
}
|
|
82
|
+
if (error) {
|
|
83
|
+
return (_jsxs("div", { className: "rounded-lg border border-destructive/50 bg-destructive/10 p-6 text-center text-sm text-destructive", children: [error, _jsx(Button, { variant: "outline", size: "sm", className: "ml-4", onClick: fetchMembers, children: t('common.retry', { defaultValue: 'Retry' }) })] }));
|
|
84
|
+
}
|
|
85
|
+
return (_jsxs("div", { className: "space-y-4", "data-testid": "members-page", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("h2", { className: "text-lg font-semibold", children: [t('organization.members.title', { defaultValue: 'Members' }), " (", members.length, ")"] }), _jsx(Button, { onClick: () => setIsInviteOpen(true), "data-testid": "invite-member-btn", children: t('organization.members.inviteMember', { defaultValue: 'Invite member' }) })] }), _jsx(Separator, {}), _jsx("div", { className: "space-y-2", children: members.map((member) => (_jsxs("div", { className: "flex items-center gap-3 rounded-lg border bg-card p-3", "data-testid": `member-row-${member.id}`, children: [_jsxs(Avatar, { className: "h-9 w-9 shrink-0", children: [member.user?.image && _jsx(AvatarImage, { src: member.user.image, alt: member.user.name }), _jsx(AvatarFallback, { className: "text-xs font-medium", children: getMemberInitials(member.user?.name) })] }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "truncate font-medium text-sm", children: member.user?.name ?? '—' }), _jsx("div", { className: "truncate text-xs text-muted-foreground", children: member.user?.email ?? '—' })] }), _jsx(Badge, { variant: "outline", className: "shrink-0 capitalize", children: member.role }), member.createdAt && (_jsx("span", { className: "hidden shrink-0 text-xs text-muted-foreground sm:block", children: new Date(member.createdAt).toLocaleDateString() })), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 shrink-0", "aria-label": "Member actions", children: _jsx(MoreHorizontal, { className: "h-4 w-4" }) }) }), _jsxs(DropdownMenuContent, { align: "end", children: [_jsxs(DropdownMenuItem, { onClick: () => handleChangeRole(member, 'owner'), disabled: member.role === 'owner', children: [_jsx(ShieldCheck, { className: "mr-2 h-4 w-4" }), t('organization.roles.owner', { defaultValue: 'Owner' })] }), _jsxs(DropdownMenuItem, { onClick: () => handleChangeRole(member, 'admin'), disabled: member.role === 'admin', children: [_jsx(ShieldCheck, { className: "mr-2 h-4 w-4" }), t('organization.roles.admin', { defaultValue: 'Admin' })] }), _jsxs(DropdownMenuItem, { onClick: () => handleChangeRole(member, 'member'), disabled: member.role === 'member', children: [_jsx(ShieldCheck, { className: "mr-2 h-4 w-4" }), t('organization.roles.member', { defaultValue: 'Member' })] }), _jsxs(DropdownMenuItem, { className: "text-destructive focus:text-destructive", onClick: () => setRemovingMember(member), children: [_jsx(UserMinus, { className: "mr-2 h-4 w-4" }), t('organization.members.removeMember', { defaultValue: 'Remove member' })] })] })] })] }, member.id))) }), _jsx(AlertDialog, { open: !!removingMember, onOpenChange: (open) => !open && setRemovingMember(null), children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: t('organization.members.removeConfirmTitle', { defaultValue: 'Remove member?' }) }), _jsx(AlertDialogDescription, { children: t('organization.members.removeConfirmDescription', {
|
|
86
|
+
defaultValue: 'This will remove {{name}} from the organization. They will lose access immediately.',
|
|
87
|
+
name: removingMember?.user?.name ?? removingMember?.userId ?? '',
|
|
88
|
+
}) })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: t('common.cancel', { defaultValue: 'Cancel' }) }), _jsx(AlertDialogAction, { className: "bg-destructive text-destructive-foreground hover:bg-destructive/90", onClick: handleRemove, children: t('organization.members.removeConfirmAction', { defaultValue: 'Remove' }) })] })] }) }), _jsx(InviteMemberDialog, { organizationId: org.id, open: isInviteOpen, onOpenChange: setIsInviteOpen, onInvited: () => fetchMembers() })] }));
|
|
89
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OrganizationLayout
|
|
3
|
+
*
|
|
4
|
+
* Layout for `/organizations/:slug/*` management pages.
|
|
5
|
+
* Resolves the org by slug, ensures it is the active organization,
|
|
6
|
+
* renders nav tabs (Members / Invitations / Settings) and an Outlet.
|
|
7
|
+
*/
|
|
8
|
+
export declare function OrganizationLayout(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* OrganizationLayout
|
|
4
|
+
*
|
|
5
|
+
* Layout for `/organizations/:slug/*` management pages.
|
|
6
|
+
* Resolves the org by slug, ensures it is the active organization,
|
|
7
|
+
* renders nav tabs (Members / Invitations / Settings) and an Outlet.
|
|
8
|
+
*/
|
|
9
|
+
import { useEffect, useMemo } from 'react';
|
|
10
|
+
import { Outlet, useParams, NavLink, useNavigate } from 'react-router-dom';
|
|
11
|
+
import { useAuth } from '@object-ui/auth';
|
|
12
|
+
import { useObjectTranslation } from '@object-ui/i18n';
|
|
13
|
+
import { Loader2, ArrowLeft } from 'lucide-react';
|
|
14
|
+
import { Button } from '@object-ui/components';
|
|
15
|
+
import { useNavigationContext } from '../../../context/NavigationContext';
|
|
16
|
+
import { AppHeader } from '../../../layout/AppHeader';
|
|
17
|
+
export function OrganizationLayout() {
|
|
18
|
+
const { t } = useObjectTranslation();
|
|
19
|
+
const navigate = useNavigate();
|
|
20
|
+
const { slug } = useParams();
|
|
21
|
+
const { organizations, activeOrganization, isOrganizationsLoading, switchOrganization, } = useAuth();
|
|
22
|
+
const { setContext } = useNavigationContext();
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
setContext('home');
|
|
25
|
+
}, [setContext]);
|
|
26
|
+
const org = useMemo(() => (organizations ?? []).find((o) => o.slug === slug) ?? null, [organizations, slug]);
|
|
27
|
+
// Ensure the slug-matched org is the active one
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (org && activeOrganization?.id !== org.id) {
|
|
30
|
+
switchOrganization(org.id).catch((err) => {
|
|
31
|
+
console.error('[OrganizationLayout] switch failed', err);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}, [org, activeOrganization, switchOrganization]);
|
|
35
|
+
if (isOrganizationsLoading) {
|
|
36
|
+
return (_jsx("div", { className: "flex min-h-svh w-full items-center justify-center", children: _jsx(Loader2, { className: "h-5 w-5 animate-spin text-muted-foreground" }) }));
|
|
37
|
+
}
|
|
38
|
+
if (!org) {
|
|
39
|
+
return (_jsxs("div", { className: "mx-auto w-full max-w-3xl px-4 py-12 text-center", children: [_jsx("h1", { className: "text-xl font-semibold", children: t('organization.notFound', { defaultValue: 'Organization not found' }) }), _jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: t('organization.notFoundDescription', {
|
|
40
|
+
defaultValue: 'This organization does not exist or you do not have access.',
|
|
41
|
+
}) }), _jsx(Button, { className: "mt-6", onClick: () => navigate('/organizations'), children: t('organization.backToList', { defaultValue: 'Back to organizations' }) })] }));
|
|
42
|
+
}
|
|
43
|
+
const tabs = [
|
|
44
|
+
{ to: 'members', label: t('organization.tabs.members', { defaultValue: 'Members' }) },
|
|
45
|
+
{ to: 'invitations', label: t('organization.tabs.invitations', { defaultValue: 'Invitations' }) },
|
|
46
|
+
{ to: 'settings', label: t('organization.tabs.settings', { defaultValue: 'Settings' }) },
|
|
47
|
+
];
|
|
48
|
+
return (_jsxs("div", { className: "flex min-h-svh w-full flex-col bg-background", "data-testid": "organization-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("div", { className: "border-b", children: _jsxs("div", { className: "mx-auto w-full max-w-5xl px-4 sm:px-6 pt-6", children: [_jsxs("button", { onClick: () => navigate('/organizations'), className: "mb-3 inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground", children: [_jsx(ArrowLeft, { className: "h-3.5 w-3.5" }), t('organization.backToList', { defaultValue: 'Back to organizations' })] }), _jsx("div", { className: "flex items-center justify-between gap-4", children: _jsxs("div", { className: "min-w-0", children: [_jsx("h1", { className: "truncate text-2xl font-bold tracking-tight", children: org.name }), _jsx("p", { className: "text-xs text-muted-foreground", children: org.slug })] }) }), _jsx("nav", { className: "mt-4 flex gap-1 -mb-px", children: tabs.map((tab) => (_jsx(NavLink, { to: tab.to, end: true, className: ({ isActive }) => `border-b-2 px-3 py-2 text-sm font-medium transition-colors ${isActive
|
|
49
|
+
? 'border-primary text-foreground'
|
|
50
|
+
: 'border-transparent text-muted-foreground hover:text-foreground'}`, children: tab.label }, tab.to))) })] }) }), _jsx("main", { className: "flex-1 min-w-0 overflow-auto", children: _jsx("div", { className: "mx-auto w-full max-w-5xl px-4 sm:px-6 py-6", children: _jsx(Outlet, { context: { org } }) }) })] }));
|
|
51
|
+
}
|