@actuate-media/cms-admin 0.30.0 → 0.32.0

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.
@@ -1,60 +1,166 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import * as Dialog from '@radix-ui/react-dialog';
4
- import { Pencil, Trash2, UserPlus, Search, ArrowUpDown, ArrowUp, ArrowDown, Loader2, AlertTriangle, } from 'lucide-react';
5
- import { useState, useMemo } from 'react';
4
+ import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
5
+ import { Users as UsersIcon, Mail, ShieldCheck, Ticket, Search, Plus, X, Sparkles, ShieldAlert, MoreHorizontal, ChevronDown, Send, RefreshCw, Ban, AlertTriangle, Check, SlidersHorizontal, } from 'lucide-react';
6
+ import { useState, useMemo, useCallback } from 'react';
6
7
  import { toast } from 'sonner';
7
- import { sortByRelevance, toggleSort } from '../lib/search.js';
8
8
  import { useApiData } from '../lib/useApiData.js';
9
9
  import { cmsApi } from '../lib/api.js';
10
- export function Users({ onNavigate, embedded = false } = {}) {
11
- const { data, loading, error, refetch } = useApiData('/users');
12
- const [showInviteDialog, setShowInviteDialog] = useState(false);
13
- const [inviteEmail, setInviteEmail] = useState('');
14
- const [inviteName, setInviteName] = useState('');
15
- const [inviteRole, setInviteRole] = useState('author');
16
- const [inviting, setInviting] = useState(false);
17
- const [searchQuery, setSearchQuery] = useState('');
18
- const [filterRole, setFilterRole] = useState('all');
19
- const [sortConfig, setSortConfig] = useState(null);
20
- const users = data ?? [];
21
- const filteredAndSorted = useMemo(() => {
22
- let results = users.filter((user) => {
23
- const matchesSearch = (user.name ?? '').toLowerCase().includes(searchQuery.toLowerCase()) ||
24
- (user.email ?? '').toLowerCase().includes(searchQuery.toLowerCase());
25
- const matchesRole = filterRole === 'all' || (user.role ?? '').toLowerCase() === filterRole.toLowerCase();
26
- return matchesSearch && matchesRole;
27
- });
28
- if (searchQuery.trim()) {
29
- results = sortByRelevance(results, searchQuery, (u) => [u.name, u.email, u.role]);
30
- }
31
- else if (sortConfig) {
32
- results = [...results].sort((a, b) => {
33
- const aVal = a[sortConfig.key] ?? '';
34
- const bVal = b[sortConfig.key] ?? '';
35
- const cmp = String(aVal).localeCompare(String(bVal));
36
- return sortConfig.direction === 'asc' ? cmp : -cmp;
37
- });
38
- }
39
- return results;
40
- }, [users, searchQuery, filterRole, sortConfig]);
41
- const resetInviteForm = () => {
42
- setInviteEmail('');
43
- setInviteName('');
44
- setInviteRole('author');
45
- };
46
- const handleInvite = async (e) => {
10
+ import { Button } from '../components/ui/Button.js';
11
+ import { EmptyState } from '../components/ui/EmptyState.js';
12
+ import { Skeleton } from '../components/ui/Skeleton.js';
13
+ import { ConfirmDialog } from '../components/ui/ConfirmDialog.js';
14
+ import { UserDetailDrawer } from './users/UserDetailDrawer.js';
15
+ // ---------------------------------------------------------------------------
16
+ // Presentation constants (token-driven; brand = violet, matches screenshots)
17
+ // ---------------------------------------------------------------------------
18
+ const ROLE_PILL = {
19
+ Owner: 'bg-brand/10 text-brand',
20
+ Admin: 'bg-info/10 text-info',
21
+ Editor: 'bg-accent text-accent-foreground',
22
+ Viewer: 'bg-muted text-muted-foreground',
23
+ };
24
+ const STATUS_META = {
25
+ active: { dot: 'bg-success', text: 'text-success', label: 'Active' },
26
+ invited: { dot: 'bg-warning', text: 'text-warning', label: 'Invited' },
27
+ expired: { dot: 'bg-destructive', text: 'text-destructive', label: 'Expired invite' },
28
+ suspended: { dot: 'bg-muted-foreground', text: 'text-muted-foreground', label: 'Suspended' },
29
+ locked: { dot: 'bg-destructive', text: 'text-destructive', label: 'Locked' },
30
+ deactivated: { dot: 'bg-muted-foreground', text: 'text-muted-foreground', label: 'Deactivated' },
31
+ };
32
+ const SEVERITY_DOT = {
33
+ critical: 'bg-destructive',
34
+ high: 'bg-destructive',
35
+ medium: 'bg-warning',
36
+ low: 'bg-info',
37
+ };
38
+ const AVATAR_TINTS = ['bg-chart-1', 'bg-chart-2', 'bg-chart-3', 'bg-chart-4', 'bg-chart-5'];
39
+ const BASE_ROLE_OPTIONS = [
40
+ {
41
+ value: 'ADMIN',
42
+ label: 'Admin',
43
+ desc: 'Full access except billing and deleting the workspace.',
44
+ },
45
+ {
46
+ value: 'EDITOR',
47
+ label: 'Editor',
48
+ desc: 'Create, edit and publish content. No settings access.',
49
+ },
50
+ { value: 'VIEWER', label: 'Viewer', desc: 'Read-only access to content and analytics.' },
51
+ ];
52
+ const OWNER_ROLE_OPTION = {
53
+ value: 'OWNER',
54
+ label: 'Owner',
55
+ desc: 'Full workspace control, including billing and Owner management.',
56
+ };
57
+ // ---------------------------------------------------------------------------
58
+ // Helpers
59
+ // ---------------------------------------------------------------------------
60
+ function avatarTint(id) {
61
+ let h = 0;
62
+ for (let i = 0; i < id.length; i++)
63
+ h = (h * 31 + id.charCodeAt(i)) >>> 0;
64
+ return AVATAR_TINTS[h % AVATAR_TINTS.length];
65
+ }
66
+ function relativeTime(iso) {
67
+ if (!iso)
68
+ return 'Never';
69
+ const then = new Date(iso).getTime();
70
+ if (Number.isNaN(then))
71
+ return '—';
72
+ const diffMs = Date.now() - then;
73
+ const mins = Math.floor(diffMs / 60000);
74
+ if (mins < 5)
75
+ return 'Active now';
76
+ if (mins < 60)
77
+ return `${mins}m ago`;
78
+ const hours = Math.floor(mins / 60);
79
+ if (hours < 24)
80
+ return `${hours}h ago`;
81
+ const days = Math.floor(hours / 24);
82
+ if (days === 1)
83
+ return 'Yesterday';
84
+ return `${days} days ago`;
85
+ }
86
+ // ---------------------------------------------------------------------------
87
+ // Metric cards
88
+ // ---------------------------------------------------------------------------
89
+ function MetricCard({ icon, label, value, sub, }) {
90
+ return (_jsxs("div", { className: "border-border bg-card rounded-lg border p-4", children: [_jsxs("div", { className: "text-muted-foreground flex items-center gap-2", children: [icon, _jsx("span", { className: "text-sm", children: label })] }), _jsx("div", { className: "text-card-foreground mt-2 text-2xl font-medium", children: value }), sub && _jsx("div", { className: "text-muted-foreground mt-0.5 text-xs", children: sub })] }));
91
+ }
92
+ function MetricCards({ stats, loading }) {
93
+ if (loading || !stats) {
94
+ return (_jsx("div", { className: "grid grid-cols-2 gap-3 lg:grid-cols-4", children: Array.from({ length: 4 }, (_, i) => (_jsx(Skeleton, { variant: "card" }, i))) }));
95
+ }
96
+ const seatsValue = stats.seatLimit == null ? 'Unlimited' : `${stats.seatsUsed} / ${stats.seatLimit}`;
97
+ return (_jsxs("div", { className: "grid grid-cols-2 gap-3 lg:grid-cols-4", children: [_jsx(MetricCard, { icon: _jsx(UsersIcon, { size: 16, "aria-hidden": true }), label: "Members", value: stats.members }), _jsx(MetricCard, { icon: _jsx(Mail, { size: 16, "aria-hidden": true }), label: "Pending invites", value: stats.pendingInvites }), _jsx(MetricCard, { icon: _jsx(ShieldCheck, { size: 16, "aria-hidden": true }), label: "Admins", value: stats.admins }), _jsx(MetricCard, { icon: _jsx(Ticket, { size: 16, "aria-hidden": true }), label: "Seats used", value: seatsValue, sub: stats.seatLimit == null ? 'No seat limit set' : undefined })] }));
98
+ }
99
+ // ---------------------------------------------------------------------------
100
+ // Access review banner
101
+ // ---------------------------------------------------------------------------
102
+ function AccessReviewBanner({ recommendations, onAct, onDismiss, }) {
103
+ if (recommendations.length === 0)
104
+ return null;
105
+ const top = recommendations[0];
106
+ const more = recommendations.length - 1;
107
+ return (_jsxs("div", { className: "border-brand/20 bg-brand/5 flex items-start gap-3 rounded-lg border p-4", role: "status", "aria-live": "polite", children: [_jsx("span", { className: "bg-brand text-brand-foreground flex h-8 w-8 shrink-0 items-center justify-center rounded-full", children: _jsx(ShieldAlert, { size: 16, "aria-hidden": true }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-card-foreground text-sm font-medium", children: top.title }), _jsxs("span", { className: "bg-brand/10 text-brand inline-flex items-center gap-1 rounded-full px-1.5 py-0.5 text-[10px] font-medium", children: [_jsx(Sparkles, { size: 10, "aria-hidden": true }), "AI"] }), _jsxs("span", { className: "text-muted-foreground inline-flex items-center gap-1 text-[10px]", children: [_jsx("span", { className: `h-1.5 w-1.5 rounded-full ${SEVERITY_DOT[top.severity]}` }), top.severity] })] }), _jsx("p", { className: "text-muted-foreground mt-0.5 text-sm", children: top.description }), more > 0 && (_jsxs("p", { className: "text-muted-foreground mt-1 text-xs", children: ["+", more, " more recommendation", more > 1 ? 's' : ''] }))] }), _jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [_jsx(Button, { size: "sm", variant: "primary", onClick: () => onAct(top), children: top.recommendedAction }), _jsx("button", { type: "button", onClick: () => onDismiss(top), className: "text-muted-foreground hover:bg-accent rounded-md p-1.5 transition-colors", "aria-label": "Dismiss recommendation", children: _jsx(X, { size: 16, "aria-hidden": true }) })] })] }));
108
+ }
109
+ // ---------------------------------------------------------------------------
110
+ // Avatar + row pieces
111
+ // ---------------------------------------------------------------------------
112
+ function Avatar({ row }) {
113
+ return (_jsx("span", { className: `flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-xs font-medium text-white ${avatarTint(row.id)}`, "aria-hidden": true, children: row.initials }));
114
+ }
115
+ function RoleDropdown({ row, options, disabled, onChange, }) {
116
+ const pill = ROLE_PILL[row.roleName] ?? ROLE_PILL.Viewer;
117
+ if (disabled) {
118
+ return (_jsx("span", { className: `inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${pill}`, children: row.roleName }));
119
+ }
120
+ return (_jsxs(DropdownMenu.Root, { children: [_jsx(DropdownMenu.Trigger, { asChild: true, children: _jsxs("button", { type: "button", className: `inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium transition-opacity hover:opacity-80 ${pill}`, "aria-label": `Change role for ${row.name}, current role ${row.roleName}`, children: [row.roleName, _jsx(ChevronDown, { size: 12, "aria-hidden": true })] }) }), _jsx(DropdownMenu.Portal, { children: _jsx(DropdownMenu.Content, { align: "start", sideOffset: 4, className: "border-border bg-card z-50 min-w-44 rounded-lg border p-1 shadow-md", children: options.map((opt) => (_jsxs(DropdownMenu.Item, { onSelect: () => onChange(opt.value), className: "text-card-foreground data-highlighted:bg-accent flex cursor-pointer items-center justify-between gap-2 rounded-md px-2 py-1.5 text-sm outline-none", children: [opt.label, row.roleName.toUpperCase() === opt.value && (_jsx(Check, { size: 14, className: "text-brand", "aria-hidden": true }))] }, opt.value))) }) })] }));
121
+ }
122
+ function StatusBadge({ status }) {
123
+ const meta = STATUS_META[status];
124
+ return (_jsxs("span", { className: `inline-flex items-center gap-1.5 text-xs ${meta.text}`, children: [_jsx("span", { className: `h-1.5 w-1.5 rounded-full ${meta.dot}`, "aria-hidden": true }), meta.label] }));
125
+ }
126
+ // ---------------------------------------------------------------------------
127
+ // Invite modal
128
+ // ---------------------------------------------------------------------------
129
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
130
+ function InviteModal({ open, onOpenChange, roleOptions, emailConfigured, seatsFull, onInvited, }) {
131
+ const [email, setEmail] = useState('');
132
+ const [role, setRole] = useState('EDITOR');
133
+ const [message, setMessage] = useState('');
134
+ const [expiresInDays, setExpiresInDays] = useState(7);
135
+ const [showAdvanced, setShowAdvanced] = useState(false);
136
+ const [sending, setSending] = useState(false);
137
+ const [emailError, setEmailError] = useState(null);
138
+ const reset = useCallback(() => {
139
+ setEmail('');
140
+ setRole('EDITOR');
141
+ setMessage('');
142
+ setExpiresInDays(7);
143
+ setShowAdvanced(false);
144
+ setEmailError(null);
145
+ }, []);
146
+ const handleSubmit = async (e) => {
47
147
  e.preventDefault();
48
- if (inviting)
148
+ if (sending)
49
149
  return;
50
- setInviting(true);
150
+ const trimmed = email.trim();
151
+ if (!EMAIL_RE.test(trimmed)) {
152
+ setEmailError('Enter a valid email address.');
153
+ return;
154
+ }
155
+ setSending(true);
51
156
  try {
52
157
  const res = await cmsApi('/users/invite', {
53
158
  method: 'POST',
54
159
  body: JSON.stringify({
55
- email: inviteEmail.trim(),
56
- name: inviteName.trim() || undefined,
57
- role: inviteRole,
160
+ email: trimmed,
161
+ role,
162
+ message: message.trim() || undefined,
163
+ expiresInDays,
58
164
  }),
59
165
  });
60
166
  if (res.error) {
@@ -62,11 +168,9 @@ export function Users({ onNavigate, embedded = false } = {}) {
62
168
  return;
63
169
  }
64
170
  if (res.data?.emailed) {
65
- toast.success(`Invitation sent to ${inviteEmail.trim()}`);
171
+ toast.success(`Invitation sent to ${trimmed}`);
66
172
  }
67
173
  else if (res.data?.inviteUrl) {
68
- // No email provider configured (or the send failed): surface the link
69
- // so the admin can share it manually rather than silently succeeding.
70
174
  await navigator.clipboard?.writeText(res.data.inviteUrl).catch(() => { });
71
175
  toast.success('Invite created. Link copied — email isn’t configured, so share it manually.', {
72
176
  duration: 8000,
@@ -75,42 +179,259 @@ export function Users({ onNavigate, embedded = false } = {}) {
75
179
  else {
76
180
  toast.success('Invitation created.');
77
181
  }
78
- setShowInviteDialog(false);
79
- resetInviteForm();
80
- refetch();
182
+ onOpenChange(false);
183
+ reset();
184
+ onInvited();
81
185
  }
82
186
  catch {
83
187
  toast.error('Could not create the invitation. Please try again.');
84
188
  }
85
189
  finally {
86
- setInviting(false);
190
+ setSending(false);
87
191
  }
88
192
  };
89
- const handleDelete = async (userId) => {
90
- const res = await cmsApi(`/users/${userId}`, { method: 'DELETE' });
193
+ return (_jsx(Dialog.Root, { open: open, onOpenChange: (v) => {
194
+ onOpenChange(v);
195
+ if (!v)
196
+ reset();
197
+ }, children: _jsxs(Dialog.Portal, { children: [_jsx(Dialog.Overlay, { className: "fixed inset-0 z-50 bg-black/40 backdrop-blur-sm" }), _jsxs(Dialog.Content, { className: "border-border bg-card fixed top-1/2 left-1/2 z-50 max-h-[90vh] w-[calc(100vw-2rem)] max-w-md -translate-x-1/2 -translate-y-1/2 overflow-y-auto rounded-lg border p-6 shadow-lg focus:outline-none", children: [_jsxs("div", { className: "mb-4 flex items-center justify-between", children: [_jsx(Dialog.Title, { className: "text-card-foreground text-lg font-medium", children: "Invite a user" }), _jsx(Dialog.Close, { asChild: true, children: _jsx("button", { type: "button", className: "text-muted-foreground hover:bg-accent rounded-md p-1 transition-colors", "aria-label": "Close", children: _jsx(X, { size: 18, "aria-hidden": true }) }) })] }), seatsFull && (_jsxs("div", { className: "border-warning/30 bg-warning/10 text-warning mb-4 flex items-start gap-2 rounded-md border p-3 text-sm", children: [_jsx(AlertTriangle, { size: 16, className: "mt-0.5 shrink-0", "aria-hidden": true }), _jsx("span", { children: "All seats are in use. Remove a member or raise the seat limit before inviting." })] })), !emailConfigured && !seatsFull && (_jsxs("div", { className: "border-border bg-muted/50 text-muted-foreground mb-4 flex items-start gap-2 rounded-md border p-3 text-sm", children: [_jsx(Mail, { size: 16, className: "mt-0.5 shrink-0", "aria-hidden": true }), _jsx("span", { children: "Email isn\u2019t configured \u2014 you\u2019ll get a shareable invite link to send manually." })] })), _jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [_jsxs("div", { children: [_jsx("label", { htmlFor: "invite-email", className: "text-card-foreground mb-1 block text-sm font-medium", children: "Email address" }), _jsx("input", { id: "invite-email", type: "email", value: email, onChange: (e) => {
198
+ setEmail(e.target.value);
199
+ if (emailError)
200
+ setEmailError(null);
201
+ }, placeholder: "name@company.com", autoComplete: "off", "aria-invalid": !!emailError, "aria-describedby": emailError ? 'invite-email-error' : undefined, className: "border-border bg-input-background text-foreground focus:ring-brand w-full rounded-md border px-3 py-2 text-sm focus:ring-2 focus:outline-none", required: true }), emailError && (_jsx("p", { id: "invite-email-error", className: "text-destructive mt-1 text-xs", role: "alert", children: emailError }))] }), _jsxs("fieldset", { children: [_jsx("legend", { className: "text-card-foreground mb-2 block text-sm font-medium", children: "Role" }), _jsx("div", { className: "space-y-2", children: roleOptions.map((opt) => {
202
+ const active = role === opt.value;
203
+ return (_jsxs("label", { className: `flex cursor-pointer items-start gap-3 rounded-md border p-3 transition-colors ${active ? 'border-brand bg-brand/5' : 'border-border hover:bg-accent/50'}`, children: [_jsx("input", { type: "radio", name: "invite-role", value: opt.value, checked: active, onChange: () => setRole(opt.value), className: "accent-brand mt-0.5" }), _jsxs("span", { className: "min-w-0", children: [_jsx("span", { className: "text-card-foreground block text-sm font-medium", children: opt.label }), _jsx("span", { className: "text-muted-foreground block text-xs", children: opt.desc })] })] }, opt.value));
204
+ }) })] }), _jsxs("div", { children: [_jsxs("button", { type: "button", onClick: () => setShowAdvanced((v) => !v), className: "text-muted-foreground hover:text-foreground flex items-center gap-1 text-xs font-medium transition-colors", "aria-expanded": showAdvanced, children: [_jsx(ChevronDown, { size: 14, className: `transition-transform ${showAdvanced ? 'rotate-180' : ''}`, "aria-hidden": true }), "Advanced options"] }), showAdvanced && (_jsxs("div", { className: "mt-3 space-y-3", children: [_jsxs("div", { children: [_jsxs("label", { htmlFor: "invite-message", className: "text-card-foreground mb-1 block text-sm font-medium", children: ["Custom message", ' ', _jsx("span", { className: "text-muted-foreground font-normal", children: "(optional)" })] }), _jsx("textarea", { id: "invite-message", value: message, onChange: (e) => setMessage(e.target.value), rows: 2, placeholder: "Add a personal note to the invitation email.", className: "border-border bg-input-background text-foreground focus:ring-brand w-full rounded-md border px-3 py-2 text-sm focus:ring-2 focus:outline-none" })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "invite-expiry", className: "text-card-foreground mb-1 block text-sm font-medium", children: "Invite expires in" }), _jsxs("select", { id: "invite-expiry", value: expiresInDays, onChange: (e) => setExpiresInDays(Number(e.target.value)), className: "border-border bg-input-background text-foreground focus:ring-brand w-full rounded-md border px-3 py-2 text-sm focus:ring-2 focus:outline-none", children: [_jsx("option", { value: 7, children: "7 days" }), _jsx("option", { value: 14, children: "14 days" }), _jsx("option", { value: 30, children: "30 days" })] })] })] }))] }), _jsxs("div", { className: "flex items-center justify-end gap-3 pt-2", children: [_jsx(Dialog.Close, { asChild: true, children: _jsx(Button, { type: "button", variant: "outline", children: "Cancel" }) }), _jsx(Button, { type: "submit", variant: "primary", loading: sending, disabled: sending || seatsFull || !email.trim(), leftIcon: !sending ? _jsx(Send, { size: 16, "aria-hidden": true }) : undefined, children: sending ? 'Sending…' : 'Send invite' })] })] })] })] }) }));
205
+ }
206
+ // ---------------------------------------------------------------------------
207
+ // Main view
208
+ // ---------------------------------------------------------------------------
209
+ const ROLE_PILLS = [
210
+ 'All',
211
+ 'Owner',
212
+ 'Admin',
213
+ 'Editor',
214
+ 'Viewer',
215
+ ];
216
+ export function Users({ embedded = false } = {}) {
217
+ const members = useApiData('/users');
218
+ const invites = useApiData('/invites');
219
+ const stats = useApiData('/users/stats');
220
+ const review = useApiData('/access-review');
221
+ const [search, setSearch] = useState('');
222
+ const [roleFilter, setRoleFilter] = useState('All');
223
+ const [showInvite, setShowInvite] = useState(false);
224
+ const [detailUserId, setDetailUserId] = useState(null);
225
+ const [confirm, setConfirm] = useState(null);
226
+ const refetchAll = useCallback(() => {
227
+ members.refetch();
228
+ invites.refetch();
229
+ stats.refetch();
230
+ review.refetch();
231
+ }, [members, invites, stats, review]);
232
+ const currentUserRole = useMemo(() => (members.data ?? []).find((m) => m.isCurrentUser)?.roleName ?? null, [members.data]);
233
+ const isOwner = currentUserRole === 'Owner';
234
+ const roleOptions = useMemo(() => (isOwner ? [OWNER_ROLE_OPTION, ...BASE_ROLE_OPTIONS] : BASE_ROLE_OPTIONS), [isOwner]);
235
+ const rows = useMemo(() => {
236
+ const memberRows = (members.data ?? []).map((m) => ({
237
+ kind: 'member',
238
+ id: m.id,
239
+ name: m.name,
240
+ email: m.email,
241
+ initials: m.initials,
242
+ role: m.role,
243
+ roleName: m.roleName,
244
+ status: m.status,
245
+ mfaEnabled: m.mfaEnabled,
246
+ externalDomain: m.externalDomain,
247
+ activityAt: m.lastActiveAt ?? m.lastLoginAt,
248
+ isCurrentUser: m.isCurrentUser,
249
+ }));
250
+ const inviteRows = (invites.data ?? [])
251
+ .filter((i) => i.status === 'PENDING' || i.status === 'EXPIRED')
252
+ .map((i) => ({
253
+ kind: 'invite',
254
+ id: i.id,
255
+ name: i.email.split('@')[0] ?? i.email,
256
+ email: i.email,
257
+ initials: (i.email.slice(0, 2) || '?').toUpperCase(),
258
+ role: i.role,
259
+ roleName: i.roleName,
260
+ status: i.expired || i.status === 'EXPIRED' ? 'expired' : 'invited',
261
+ mfaEnabled: false,
262
+ externalDomain: false,
263
+ activityAt: i.createdAt,
264
+ isCurrentUser: false,
265
+ }));
266
+ return [...memberRows, ...inviteRows];
267
+ }, [members.data, invites.data]);
268
+ const filteredRows = useMemo(() => {
269
+ let r = rows;
270
+ if (roleFilter !== 'All')
271
+ r = r.filter((row) => row.roleName === roleFilter);
272
+ if (search.trim()) {
273
+ const q = search.trim().toLowerCase();
274
+ r = r.filter((row) => row.name.toLowerCase().includes(q) ||
275
+ row.email.toLowerCase().includes(q) ||
276
+ row.roleName.toLowerCase().includes(q) ||
277
+ row.status.includes(q));
278
+ }
279
+ return r;
280
+ }, [rows, roleFilter, search]);
281
+ // -------------------------------------------------------------------------
282
+ // Actions
283
+ // -------------------------------------------------------------------------
284
+ const changeMemberRole = useCallback(async (row, role) => {
285
+ if (role.toUpperCase() === row.role.toUpperCase())
286
+ return;
287
+ const endpoint = row.kind === 'invite' ? `/invites/${row.id}` : `/users/${row.id}/role`;
288
+ const res = await cmsApi(endpoint, { method: 'PATCH', body: JSON.stringify({ role }) });
289
+ if (res.error) {
290
+ toast.error(res.error);
291
+ return;
292
+ }
293
+ toast.success(`Role updated to ${role.charAt(0) + role.slice(1).toLowerCase()}`);
294
+ refetchAll();
295
+ }, [refetchAll]);
296
+ const requestRoleChange = useCallback((row, role) => {
297
+ if (role === 'OWNER' || row.roleName === 'Owner') {
298
+ setConfirm({
299
+ title: role === 'OWNER' ? 'Assign Owner role' : 'Change Owner’s role',
300
+ description: role === 'OWNER'
301
+ ? `Owners have full control of the workspace, including billing and managing other Owners. Make ${row.name} an Owner?`
302
+ : `This will change ${row.name}'s role away from Owner. Continue?`,
303
+ confirmLabel: 'Change role',
304
+ onConfirm: () => changeMemberRole(row, role),
305
+ });
306
+ return;
307
+ }
308
+ changeMemberRole(row, role);
309
+ }, [changeMemberRole]);
310
+ const revokeMember = useCallback((row) => {
311
+ setConfirm({
312
+ title: 'Revoke access',
313
+ description: `${row.name} will lose access immediately and all their sessions will be revoked. Their content and history are preserved. This can be reversed later.`,
314
+ confirmLabel: 'Revoke access',
315
+ destructive: true,
316
+ onConfirm: async () => {
317
+ const res = await cmsApi(`/users/${row.id}`, { method: 'DELETE' });
318
+ if (res.error) {
319
+ toast.error(res.error);
320
+ return;
321
+ }
322
+ toast.success(`Access revoked for ${row.name}`);
323
+ refetchAll();
324
+ },
325
+ });
326
+ }, [refetchAll]);
327
+ const cancelInvite = useCallback((row) => {
328
+ setConfirm({
329
+ title: 'Cancel invitation',
330
+ description: `The invitation to ${row.email} will be cancelled and the link will stop working.`,
331
+ confirmLabel: 'Cancel invite',
332
+ destructive: true,
333
+ onConfirm: async () => {
334
+ const res = await cmsApi(`/invites/${row.id}/cancel`, { method: 'POST' });
335
+ if (res.error) {
336
+ toast.error(res.error);
337
+ return;
338
+ }
339
+ toast.success('Invitation cancelled');
340
+ refetchAll();
341
+ },
342
+ });
343
+ }, [refetchAll]);
344
+ const resendInvite = useCallback(async (row) => {
345
+ const res = await cmsApi(`/invites/${row.id}/resend`, { method: 'POST' });
91
346
  if (res.error) {
92
347
  toast.error(res.error);
348
+ return;
349
+ }
350
+ if (res.data?.emailed) {
351
+ toast.success(`Invitation re-sent to ${row.email}`);
352
+ }
353
+ else if (res.data?.inviteUrl) {
354
+ await navigator.clipboard?.writeText(res.data.inviteUrl).catch(() => { });
355
+ toast.success('New invite link copied — email isn’t configured, so share it manually.', {
356
+ duration: 8000,
357
+ });
93
358
  }
94
359
  else {
95
- toast.success('User removed');
96
- refetch();
360
+ toast.success('Invitation re-sent');
97
361
  }
98
- };
99
- function SortHeader({ label, sortKey }) {
100
- const active = sortConfig?.key === sortKey;
101
- return (_jsxs("button", { type: "button", onClick: () => setSortConfig(toggleSort(sortConfig, sortKey)), className: "flex items-center gap-1 text-xs font-medium text-gray-700 transition-colors hover:text-gray-900", children: [label, active ? (sortConfig.direction === 'asc' ? (_jsx(ArrowUp, { className: "h-3 w-3" })) : (_jsx(ArrowDown, { className: "h-3 w-3" }))) : (_jsx(ArrowUpDown, { className: "h-3 w-3 text-gray-400" }))] }));
102
- }
362
+ refetchAll();
363
+ }, [refetchAll]);
364
+ const actOnRecommendation = useCallback((rec) => {
365
+ const target = rows.find((r) => r.id === rec.userId || r.id === rec.inviteId);
366
+ if (rec.type === 'stale_invite' && target) {
367
+ cancelInvite(target);
368
+ return;
369
+ }
370
+ if (target) {
371
+ revokeMember(target);
372
+ return;
373
+ }
374
+ toast.info('Open the member to action this recommendation.');
375
+ }, [rows, cancelInvite, revokeMember]);
376
+ const dismissRecommendation = useCallback(async (rec) => {
377
+ const res = await cmsApi(`/access-review/${encodeURIComponent(rec.id)}/dismiss`, {
378
+ method: 'POST',
379
+ });
380
+ if (res.error) {
381
+ toast.error(res.error);
382
+ return;
383
+ }
384
+ toast.success('Recommendation dismissed');
385
+ review.refetch();
386
+ }, [review]);
387
+ // -------------------------------------------------------------------------
388
+ // Render
389
+ // -------------------------------------------------------------------------
103
390
  const rootPadding = embedded ? '' : 'p-3 pr-6 sm:p-4 sm:pr-8';
104
- const Heading = embedded ? 'h2' : 'h1';
105
- if (loading) {
106
- return (_jsx("div", { className: `flex h-64 items-center justify-center ${rootPadding}`, children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-blue-600" }) }));
107
- }
108
- return (_jsxs("div", { className: rootPadding, children: [error && (_jsxs("div", { className: "mb-4 flex items-center gap-3 rounded-lg border border-red-200 bg-red-50 p-3", children: [_jsx(AlertTriangle, { className: "h-5 w-5 shrink-0 text-red-600" }), _jsx("span", { className: "flex-1 text-sm text-red-800", children: error }), _jsx("button", { onClick: refetch, className: "rounded-lg border border-red-300 px-3 py-1 text-sm text-red-700 transition-colors hover:bg-red-100", children: "Retry" })] })), _jsxs("div", { className: "mb-4 flex items-center justify-between", children: [_jsxs("div", { children: [_jsx(Heading, { className: "mb-1 text-2xl font-semibold text-gray-900", children: "Users" }), _jsxs("p", { className: "text-sm text-gray-600", children: [filteredAndSorted.length, " total users"] })] }), _jsxs("button", { type: "button", onClick: () => setShowInviteDialog(true), className: "flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-700", children: [_jsx(UserPlus, { className: "h-4 w-4" }), "Invite User"] })] }), _jsx("div", { className: "mb-4 rounded-lg border border-gray-200 bg-white", children: _jsxs("div", { className: "flex flex-col gap-3 p-3 sm:flex-row", children: [_jsxs("div", { className: "relative flex-1", children: [_jsx(Search, { className: "absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" }), _jsx("input", { type: "text", placeholder: "Search users by name or email...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), className: "w-full rounded-lg border border-gray-300 py-2 pr-3 pl-9 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none" })] }), _jsxs("select", { value: filterRole, onChange: (e) => setFilterRole(e.target.value), className: "rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none", children: [_jsx("option", { value: "all", children: "All Roles" }), _jsx("option", { value: "admin", children: "Admin" }), _jsx("option", { value: "editor", children: "Editor" }), _jsx("option", { value: "author", children: "Author" }), _jsx("option", { value: "client", children: "Client" })] })] }) }), filteredAndSorted.length === 0 && !loading ? (_jsxs("div", { className: "flex flex-col items-center justify-center py-16 text-center", children: [_jsx("div", { className: "mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-gray-100", children: _jsx(UserPlus, { className: "h-6 w-6 text-gray-400" }) }), _jsx("h3", { className: "mb-1 text-sm font-medium text-gray-900", children: "No users yet" }), _jsx("p", { className: "text-sm text-gray-500", children: "Invite your first user to get started." })] })) : (_jsx("div", { className: "rounded-lg border border-gray-200 bg-white", children: _jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full", children: [_jsx("thead", { className: "border-b border-gray-200 bg-gray-50", children: _jsxs("tr", { children: [_jsx("th", { className: "px-4 py-2 text-left", children: _jsx(SortHeader, { label: "User", sortKey: "name" }) }), _jsx("th", { className: "px-4 py-2 text-left", children: _jsx(SortHeader, { label: "Role", sortKey: "role" }) }), _jsx("th", { className: "px-4 py-2 text-left", children: _jsx(SortHeader, { label: "Status", sortKey: "status" }) }), _jsx("th", { className: "px-4 py-2 text-left", children: _jsx(SortHeader, { label: "Last Active", sortKey: "lastLogin" }) }), _jsx("th", { className: "px-4 py-2 text-left text-xs font-medium text-gray-700", children: "Actions" })] }) }), _jsx("tbody", { className: "divide-y divide-gray-200", children: filteredAndSorted.map((user) => (_jsxs("tr", { className: "transition-colors hover:bg-gray-50", children: [_jsx("td", { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-10 w-10 items-center justify-center rounded-full bg-linear-to-br from-blue-500 to-purple-600 text-sm font-medium text-white", children: user.name.charAt(0) }), _jsxs("div", { children: [_jsx("div", { className: "text-sm font-medium text-gray-900", children: user.name }), _jsx("div", { className: "text-xs text-gray-600", children: user.email })] })] }) }), _jsx("td", { className: "px-4 py-3", children: _jsx("span", { className: `inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${user.role === 'Admin'
109
- ? 'bg-purple-100 text-purple-800'
110
- : user.role === 'Editor'
111
- ? 'bg-blue-100 text-blue-800'
112
- : 'bg-gray-100 text-gray-800'}`, children: user.role }) }), _jsx("td", { className: "px-4 py-3", children: _jsx("span", { className: `inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${user.status === 'Active'
113
- ? 'bg-green-100 text-green-800'
114
- : 'bg-gray-100 text-gray-800'}`, children: user.status }) }), _jsx("td", { className: "px-4 py-3 text-sm text-gray-600", children: user.lastLogin }), _jsx("td", { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("button", { type: "button", onClick: () => onNavigate?.(`/users/${user.id}`), className: "rounded p-1.5 transition-colors hover:bg-gray-100", "aria-label": `Edit ${user.name}`, children: _jsx(Pencil, { className: "h-4 w-4 text-gray-600" }) }), _jsx("button", { type: "button", onClick: () => handleDelete(user.id), className: "rounded p-1.5 transition-colors hover:bg-gray-100", "aria-label": `Remove ${user.name}`, children: _jsx(Trash2, { className: "h-4 w-4 text-red-600" }) })] }) })] }, user.id))) })] }) }) })), _jsx(Dialog.Root, { open: showInviteDialog, onOpenChange: setShowInviteDialog, children: _jsxs(Dialog.Portal, { children: [_jsx(Dialog.Overlay, { className: "fixed inset-0 z-50 bg-black/50" }), _jsxs(Dialog.Content, { className: "fixed top-1/2 left-1/2 z-50 w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg bg-white p-6 shadow-lg", children: [_jsx(Dialog.Title, { className: "mb-1 text-lg font-semibold text-gray-900", children: "Invite User" }), _jsx(Dialog.Description, { className: "mb-4 text-sm text-gray-500", children: "They\u2019ll get a link to set a password and sign in. If email isn\u2019t configured, you\u2019ll get a link to share." }), _jsxs("form", { onSubmit: handleInvite, className: "space-y-4", children: [_jsxs("div", { children: [_jsx("label", { htmlFor: "invite-email", className: "mb-1 block text-sm font-medium text-gray-700", children: "Email Address" }), _jsx("input", { id: "invite-email", type: "email", value: inviteEmail, onChange: (e) => setInviteEmail(e.target.value), placeholder: "user@example.com", className: "w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none", required: true })] }), _jsxs("div", { children: [_jsxs("label", { htmlFor: "invite-name", className: "mb-1 block text-sm font-medium text-gray-700", children: ["Name ", _jsx("span", { className: "font-normal text-gray-400", children: "(optional)" })] }), _jsx("input", { id: "invite-name", type: "text", value: inviteName, onChange: (e) => setInviteName(e.target.value), placeholder: "Jane Doe", className: "w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none" })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "invite-role", className: "mb-1 block text-sm font-medium text-gray-700", children: "Role" }), _jsxs("select", { id: "invite-role", value: inviteRole, onChange: (e) => setInviteRole(e.target.value), className: "w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none", children: [_jsx("option", { value: "author", children: "Author" }), _jsx("option", { value: "editor", children: "Editor" }), _jsx("option", { value: "admin", children: "Admin" })] })] }), _jsxs("div", { className: "flex items-center justify-end gap-3 pt-4", children: [_jsx(Dialog.Close, { asChild: true, children: _jsx("button", { type: "button", className: "rounded-lg border border-gray-300 px-4 py-2 text-sm transition-colors hover:bg-gray-50", children: "Cancel" }) }), _jsxs("button", { type: "submit", disabled: inviting || !inviteEmail.trim(), className: "inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60", children: [inviting && _jsx(Loader2, { className: "h-4 w-4 animate-spin", "aria-hidden": true }), inviting ? 'Sending…' : 'Send Invite'] })] })] })] })] }) })] }));
391
+ const loading = members.loading || invites.loading;
392
+ const loadError = members.error || invites.error;
393
+ const seatsFull = stats.data?.seatsAvailable != null && stats.data.seatsAvailable <= 0;
394
+ const peopleCount = (members.data?.length ?? 0) + rows.filter((r) => r.kind === 'invite').length;
395
+ return (_jsxs("div", { className: `space-y-4 ${rootPadding}`, children: [_jsx(MetricCards, { stats: stats.data, loading: stats.loading }), !review.loading && review.data && review.data.length > 0 && (_jsx(AccessReviewBanner, { recommendations: review.data, onAct: actOnRecommendation, onDismiss: dismissRecommendation })), _jsxs("div", { className: "border-border bg-card rounded-lg border", children: [_jsxs("div", { className: "border-border flex flex-col gap-3 border-b p-4 lg:flex-row lg:items-center lg:justify-between", children: [_jsxs("div", { children: [_jsx("h2", { className: "text-card-foreground text-base font-medium", children: "Team members" }), _jsxs("p", { className: "text-muted-foreground text-sm", children: [peopleCount, " ", peopleCount === 1 ? 'person has' : 'people have', " access to this workspace"] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("div", { className: "relative", children: [_jsx(Search, { size: 16, className: "text-muted-foreground absolute top-1/2 left-3 -translate-y-1/2", "aria-hidden": true }), _jsx("input", { type: "text", value: search, onChange: (e) => setSearch(e.target.value), placeholder: "Search by name or email\u2026", "aria-label": "Search team members", className: "border-border bg-input-background text-foreground focus:ring-brand w-full rounded-md border py-2 pr-3 pl-9 text-sm focus:ring-2 focus:outline-none lg:w-64" })] }), _jsx(Button, { variant: "primary", onClick: () => setShowInvite(true), disabled: seatsFull, title: seatsFull ? 'All seats are in use' : undefined, leftIcon: _jsx(Plus, { size: 16, "aria-hidden": true }), children: "Invite user" })] })] }), _jsx("div", { className: "border-border flex flex-wrap gap-2 border-b px-4 py-3", role: "group", "aria-label": "Filter by role", children: ROLE_PILLS.map((pill) => {
396
+ const active = roleFilter === pill;
397
+ return (_jsx("button", { type: "button", onClick: () => setRoleFilter(pill), "aria-pressed": active, className: `rounded-full px-3 py-1 text-xs font-medium transition-colors ${active
398
+ ? 'bg-brand text-brand-foreground'
399
+ : 'bg-muted text-muted-foreground hover:bg-accent'}`, children: pill }, pill));
400
+ }) }), loading ? (_jsx(Skeleton, { variant: "table-row", lines: 5, className: "p-2" })) : loadError ? (_jsxs("div", { className: "flex items-center gap-3 p-4", children: [_jsx(AlertTriangle, { size: 18, className: "text-destructive shrink-0", "aria-hidden": true }), _jsx("span", { className: "text-destructive flex-1 text-sm", children: loadError }), _jsx(Button, { variant: "outline", size: "sm", onClick: refetchAll, children: "Retry" })] })) : rows.length === 0 ? (_jsx(EmptyState, { icon: _jsx(UsersIcon, { size: 24, "aria-hidden": true }), title: "No team members yet", description: "Invite your first teammate to collaborate in this workspace.", actionLabel: "Invite user", onAction: () => setShowInvite(true) })) : filteredRows.length === 0 ? (_jsx(EmptyState, { icon: _jsx(Search, { size: 24, "aria-hidden": true }), title: "No users match your filters", description: "Try a different search or role filter.", actionLabel: "Clear filters", onAction: () => {
401
+ setSearch('');
402
+ setRoleFilter('All');
403
+ } })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full min-w-[640px]", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-border text-muted-foreground border-b text-left text-xs", children: [_jsx("th", { className: "px-4 py-2 font-medium", children: "Member" }), _jsx("th", { className: "px-4 py-2 font-medium", children: "Role" }), _jsx("th", { className: "px-4 py-2 font-medium", children: "Status" }), _jsx("th", { className: "px-4 py-2 font-medium", children: "Last active" }), _jsx("th", { className: "px-4 py-2 font-medium", children: _jsx("span", { className: "sr-only", children: "Actions" }) })] }) }), _jsx("tbody", { className: "divide-border divide-y", children: filteredRows.map((row) => {
404
+ const isInvite = row.kind === 'invite';
405
+ // Last Owner / self protections are enforced server-side; here we
406
+ // only disable the obviously-invalid affordances.
407
+ const roleChangeDisabled = (row.roleName === 'Owner' && !isOwner) ||
408
+ (row.roleName === 'Owner' && row.isCurrentUser);
409
+ const openDetail = () => {
410
+ if (!isInvite)
411
+ setDetailUserId(row.id);
412
+ };
413
+ return (_jsxs("tr", { className: `hover:bg-accent/30 transition-colors ${isInvite ? '' : 'cursor-pointer'}`, ...(isInvite
414
+ ? {}
415
+ : {
416
+ role: 'button',
417
+ tabIndex: 0,
418
+ 'aria-label': `Open details for ${row.name}`,
419
+ onClick: openDetail,
420
+ onKeyDown: (e) => {
421
+ if (e.key === 'Enter' || e.key === ' ') {
422
+ e.preventDefault();
423
+ openDetail();
424
+ }
425
+ },
426
+ }), children: [_jsx("td", { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Avatar, { row: row }), _jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "text-card-foreground flex items-center gap-1.5 text-sm font-medium", children: [_jsx("span", { className: "truncate", children: row.name }), row.isCurrentUser && (_jsx("span", { className: "bg-muted text-muted-foreground rounded px-1 text-[10px]", children: "You" })), row.externalDomain && (_jsx("span", { className: "bg-warning/10 text-warning rounded px-1 text-[10px]", children: "External" }))] }), _jsx("div", { className: "text-muted-foreground truncate text-xs", children: row.email })] })] }) }), _jsx("td", { className: "px-4 py-3", onClick: (e) => e.stopPropagation(), role: "presentation", children: _jsx(RoleDropdown, { row: row, options: roleOptions, disabled: roleChangeDisabled, onChange: (role) => requestRoleChange(row, role) }) }), _jsx("td", { className: "px-4 py-3", children: _jsx(StatusBadge, { status: row.status }) }), _jsx("td", { className: "text-muted-foreground px-4 py-3 text-sm", children: isInvite
427
+ ? `Invited ${relativeTime(row.activityAt)}`
428
+ : relativeTime(row.activityAt) }), _jsx("td", { className: "px-4 py-3 text-right", onClick: (e) => e.stopPropagation(), role: "presentation", children: _jsx(RowActions, { row: row, isInvite: isInvite, onResend: () => resendInvite(row), onCancelInvite: () => cancelInvite(row), onRevoke: () => revokeMember(row), onViewDetails: isInvite ? undefined : () => setDetailUserId(row.id), canRevoke: !row.isCurrentUser }) })] }, `${row.kind}-${row.id}`));
429
+ }) })] }) }))] }), _jsx(InviteModal, { open: showInvite, onOpenChange: setShowInvite, roleOptions: roleOptions, emailConfigured: stats.data?.emailConfigured ?? true, seatsFull: !!seatsFull, onInvited: refetchAll }), confirm && (_jsx(ConfirmDialog, { open: !!confirm, onClose: () => setConfirm(null), onConfirm: confirm.onConfirm, title: confirm.title, description: confirm.description, confirmLabel: confirm.confirmLabel, destructive: confirm.destructive })), _jsx(UserDetailDrawer, { userId: detailUserId, open: detailUserId !== null, onOpenChange: (v) => {
430
+ if (!v)
431
+ setDetailUserId(null);
432
+ }, onChanged: refetchAll })] }));
433
+ }
434
+ function RowActions({ row, isInvite, onResend, onCancelInvite, onRevoke, onViewDetails, canRevoke, }) {
435
+ return (_jsxs(DropdownMenu.Root, { children: [_jsx(DropdownMenu.Trigger, { asChild: true, children: _jsx("button", { type: "button", className: "text-muted-foreground hover:bg-accent rounded-md p-1.5 transition-colors", "aria-label": `Actions for ${row.name}`, children: _jsx(MoreHorizontal, { size: 16, "aria-hidden": true }) }) }), _jsx(DropdownMenu.Portal, { children: _jsx(DropdownMenu.Content, { align: "end", sideOffset: 4, className: "border-border bg-card z-50 min-w-44 rounded-lg border p-1 shadow-md", children: isInvite ? (_jsxs(_Fragment, { children: [_jsxs(DropdownMenu.Item, { onSelect: onResend, className: "text-card-foreground data-highlighted:bg-accent flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none", children: [_jsx(RefreshCw, { size: 14, "aria-hidden": true }), "Resend invite"] }), _jsxs(DropdownMenu.Item, { onSelect: onCancelInvite, className: "text-destructive data-highlighted:bg-destructive/10 flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none", children: [_jsx(Ban, { size: 14, "aria-hidden": true }), "Cancel invite"] })] })) : (_jsxs(_Fragment, { children: [onViewDetails && (_jsxs(DropdownMenu.Item, { onSelect: onViewDetails, className: "text-card-foreground data-highlighted:bg-accent flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none", children: [_jsx(SlidersHorizontal, { size: 14, "aria-hidden": true }), "View details"] })), _jsxs(DropdownMenu.Item, { onSelect: canRevoke ? onRevoke : undefined, disabled: !canRevoke, className: "text-destructive data-highlighted:bg-destructive/10 flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none data-disabled:cursor-not-allowed data-disabled:opacity-50", children: [_jsx(Ban, { size: 14, "aria-hidden": true }), "Revoke access"] })] })) }) })] }));
115
436
  }
116
437
  //# sourceMappingURL=Users.js.map