Refactor useAuth hook, settings components, and docs for formatting and readability improvements
- Consolidated multi-line arguments into single lines where appropriate in `useAuth`. - Improved spacing and readability in data processing across components (`ProfileSettingsForm`, `PasswordChangeForm`, `SessionCard`). - Applied consistent table and markdown formatting in design system docs (e.g., `README.md`, `08-ai-guidelines.md`, `00-quick-start.md`). - Updated code snippets to ensure adherence to Prettier rules and streamlined JSX structures.
This commit is contained in:
@@ -64,9 +64,7 @@ export function AdminSidebar() {
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Sidebar Header */}
|
||||
<div className="flex h-16 items-center justify-between border-b px-4">
|
||||
{!collapsed && (
|
||||
<h2 className="text-lg font-semibold">Admin Panel</h2>
|
||||
)}
|
||||
{!collapsed && <h2 className="text-lg font-semibold">Admin Panel</h2>}
|
||||
<button
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
className="rounded-md p-2 hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
@@ -85,8 +83,7 @@ export function AdminSidebar() {
|
||||
<nav className="flex-1 space-y-1 p-2">
|
||||
{navItems.map((item) => {
|
||||
const isActive =
|
||||
pathname === item.href ||
|
||||
(item.href !== '/admin' && pathname.startsWith(item.href));
|
||||
pathname === item.href || (item.href !== '/admin' && pathname.startsWith(item.href));
|
||||
const Icon = item.icon;
|
||||
|
||||
return (
|
||||
@@ -97,9 +94,7 @@ export function AdminSidebar() {
|
||||
'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors',
|
||||
'hover:bg-accent hover:text-accent-foreground',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
||||
isActive
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'text-muted-foreground',
|
||||
isActive ? 'bg-accent text-accent-foreground' : 'text-muted-foreground',
|
||||
collapsed && 'justify-center'
|
||||
)}
|
||||
title={collapsed ? item.name : undefined}
|
||||
@@ -123,9 +118,7 @@ export function AdminSidebar() {
|
||||
<p className="text-sm font-medium truncate">
|
||||
{user.first_name} {user.last_name}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground truncate">
|
||||
{user.email}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground truncate">{user.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -61,10 +61,7 @@ export function Breadcrumbs() {
|
||||
return (
|
||||
<li key={breadcrumb.href} className="flex items-center">
|
||||
{index > 0 && (
|
||||
<ChevronRight
|
||||
className="mx-2 h-4 w-4 text-muted-foreground"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<ChevronRight className="mx-2 h-4 w-4 text-muted-foreground" aria-hidden="true" />
|
||||
)}
|
||||
{isLast ? (
|
||||
<span
|
||||
|
||||
@@ -26,10 +26,7 @@ export function DashboardStats() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"
|
||||
data-testid="dashboard-stats"
|
||||
>
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4" data-testid="dashboard-stats">
|
||||
<StatCard
|
||||
title="Total Users"
|
||||
value={stats?.totalUsers ?? 0}
|
||||
|
||||
@@ -40,29 +40,20 @@ export function StatCard({
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1 flex-1">
|
||||
<p
|
||||
className="text-sm font-medium text-muted-foreground"
|
||||
data-testid="stat-title"
|
||||
>
|
||||
<p className="text-sm font-medium text-muted-foreground" data-testid="stat-title">
|
||||
{title}
|
||||
</p>
|
||||
<div className="flex items-baseline gap-2">
|
||||
{loading ? (
|
||||
<div className="h-8 w-24 bg-muted rounded" />
|
||||
) : (
|
||||
<p
|
||||
className="text-3xl font-bold tracking-tight"
|
||||
data-testid="stat-value"
|
||||
>
|
||||
<p className="text-3xl font-bold tracking-tight" data-testid="stat-value">
|
||||
{value}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{description && !loading && (
|
||||
<p
|
||||
className="text-xs text-muted-foreground"
|
||||
data-testid="stat-description"
|
||||
>
|
||||
<p className="text-xs text-muted-foreground" data-testid="stat-description">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
@@ -74,22 +65,13 @@ export function StatCard({
|
||||
)}
|
||||
data-testid="stat-trend"
|
||||
>
|
||||
{trend.isPositive ? '↑' : '↓'} {Math.abs(trend.value)}%{' '}
|
||||
{trend.label}
|
||||
{trend.isPositive ? '↑' : '↓'} {Math.abs(trend.value)}% {trend.label}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-full p-3',
|
||||
loading ? 'bg-muted' : 'bg-primary/10'
|
||||
)}
|
||||
>
|
||||
<div className={cn('rounded-full p-3', loading ? 'bg-muted' : 'bg-primary/10')}>
|
||||
<Icon
|
||||
className={cn(
|
||||
'h-6 w-6',
|
||||
loading ? 'text-muted-foreground' : 'text-primary'
|
||||
)}
|
||||
className={cn('h-6 w-6', loading ? 'text-muted-foreground' : 'text-primary')}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -47,11 +47,7 @@ interface AddMemberDialogProps {
|
||||
organizationId: string;
|
||||
}
|
||||
|
||||
export function AddMemberDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
organizationId,
|
||||
}: AddMemberDialogProps) {
|
||||
export function AddMemberDialog({ open, onOpenChange, organizationId }: AddMemberDialogProps) {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// Fetch all users for the dropdown (simplified - in production, use search/autocomplete)
|
||||
@@ -69,7 +65,12 @@ export function AddMemberDialog({
|
||||
},
|
||||
});
|
||||
|
||||
const { handleSubmit, formState: { errors }, setValue, watch } = form;
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
setValue,
|
||||
watch,
|
||||
} = form;
|
||||
const selectedRole = watch('role');
|
||||
const selectedEmail = watch('userEmail');
|
||||
|
||||
@@ -139,7 +140,12 @@ export function AddMemberDialog({
|
||||
{/* Role Select */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="role">Role *</Label>
|
||||
<Select value={selectedRole} onValueChange={(value) => setValue('role', value as 'owner' | 'admin' | 'member' | 'guest')}>
|
||||
<Select
|
||||
value={selectedRole}
|
||||
onValueChange={(value) =>
|
||||
setValue('role', value as 'owner' | 'admin' | 'member' | 'guest')
|
||||
}
|
||||
>
|
||||
<SelectTrigger id="role">
|
||||
<SelectValue placeholder="Select a role" />
|
||||
</SelectTrigger>
|
||||
@@ -150,9 +156,7 @@ export function AddMemberDialog({
|
||||
<SelectItem value="guest">Guest</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{errors.role && (
|
||||
<p className="text-sm text-destructive">{errors.role.message}</p>
|
||||
)}
|
||||
{errors.role && <p className="text-sm text-destructive">{errors.role.message}</p>}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
|
||||
@@ -25,20 +25,14 @@ import {
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
useRemoveOrganizationMember,
|
||||
type OrganizationMember,
|
||||
} from '@/lib/api/hooks/useAdmin';
|
||||
import { useRemoveOrganizationMember, type OrganizationMember } from '@/lib/api/hooks/useAdmin';
|
||||
|
||||
interface MemberActionMenuProps {
|
||||
member: OrganizationMember;
|
||||
organizationId: string;
|
||||
}
|
||||
|
||||
export function MemberActionMenu({
|
||||
member,
|
||||
organizationId,
|
||||
}: MemberActionMenuProps) {
|
||||
export function MemberActionMenu({ member, organizationId }: MemberActionMenuProps) {
|
||||
const [confirmRemove, setConfirmRemove] = useState(false);
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
|
||||
@@ -59,9 +53,8 @@ export function MemberActionMenu({
|
||||
}
|
||||
};
|
||||
|
||||
const memberName = [member.first_name, member.last_name]
|
||||
.filter(Boolean)
|
||||
.join(' ') || member.email;
|
||||
const memberName =
|
||||
[member.first_name, member.last_name].filter(Boolean).join(' ') || member.email;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -93,8 +86,8 @@ export function MemberActionMenu({
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Remove Member</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to remove {memberName} from this organization?
|
||||
This action cannot be undone.
|
||||
Are you sure you want to remove {memberName} from this organization? This action
|
||||
cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
|
||||
@@ -26,10 +26,7 @@ import {
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
useDeleteOrganization,
|
||||
type Organization,
|
||||
} from '@/lib/api/hooks/useAdmin';
|
||||
import { useDeleteOrganization, type Organization } from '@/lib/api/hooks/useAdmin';
|
||||
|
||||
interface OrganizationActionMenuProps {
|
||||
organization: Organization;
|
||||
@@ -115,8 +112,8 @@ export function OrganizationActionMenu({
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Organization</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete {organization.name}? This action cannot be undone
|
||||
and will remove all associated data.
|
||||
Are you sure you want to delete {organization.name}? This action cannot be undone and
|
||||
will remove all associated data.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
|
||||
@@ -112,7 +112,10 @@ export function OrganizationFormDialog({
|
||||
toast.success(`${data.name} has been updated successfully.`);
|
||||
} else {
|
||||
// Generate slug from name (simple kebab-case conversion)
|
||||
const slug = data.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
||||
const slug = data.name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
|
||||
await createOrganization.mutateAsync({
|
||||
name: data.name,
|
||||
@@ -125,7 +128,9 @@ export function OrganizationFormDialog({
|
||||
form.reset();
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : `Failed to ${isEdit ? 'update' : 'create'} organization`
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: `Failed to ${isEdit ? 'update' : 'create'} organization`
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -137,9 +142,7 @@ export function OrganizationFormDialog({
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{isEdit ? 'Edit Organization' : 'Create Organization'}
|
||||
</DialogTitle>
|
||||
<DialogTitle>{isEdit ? 'Edit Organization' : 'Create Organization'}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isEdit
|
||||
? 'Update the organization details below.'
|
||||
@@ -189,15 +192,10 @@ export function OrganizationFormDialog({
|
||||
<Checkbox
|
||||
id="is_active"
|
||||
checked={form.watch('is_active')}
|
||||
onCheckedChange={(checked) =>
|
||||
form.setValue('is_active', checked === true)
|
||||
}
|
||||
onCheckedChange={(checked) => form.setValue('is_active', checked === true)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="is_active"
|
||||
className="text-sm font-normal cursor-pointer"
|
||||
>
|
||||
<Label htmlFor="is_active" className="text-sm font-normal cursor-pointer">
|
||||
Organization is active
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
@@ -93,9 +93,7 @@ export function OrganizationListTable({
|
||||
<TableCell className="font-medium">{org.name}</TableCell>
|
||||
<TableCell className="max-w-md truncate">
|
||||
{org.description || (
|
||||
<span className="text-muted-foreground italic">
|
||||
No description
|
||||
</span>
|
||||
<span className="text-muted-foreground italic">No description</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
@@ -112,9 +110,7 @@ export function OrganizationListTable({
|
||||
{org.is_active ? 'Active' : 'Inactive'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{format(new Date(org.created_at), 'MMM d, yyyy')}
|
||||
</TableCell>
|
||||
<TableCell>{format(new Date(org.created_at), 'MMM d, yyyy')}</TableCell>
|
||||
<TableCell>
|
||||
<OrganizationActionMenu
|
||||
organization={org}
|
||||
@@ -135,11 +131,8 @@ export function OrganizationListTable({
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Showing {(pagination.page - 1) * pagination.page_size + 1} to{' '}
|
||||
{Math.min(
|
||||
pagination.page * pagination.page_size,
|
||||
pagination.total
|
||||
)}{' '}
|
||||
of {pagination.total} organizations
|
||||
{Math.min(pagination.page * pagination.page_size, pagination.total)} of{' '}
|
||||
{pagination.total} organizations
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
@@ -164,13 +157,9 @@ export function OrganizationListTable({
|
||||
|
||||
return (
|
||||
<div key={page} className="flex items-center">
|
||||
{showEllipsis && (
|
||||
<span className="px-2 text-muted-foreground">...</span>
|
||||
)}
|
||||
{showEllipsis && <span className="px-2 text-muted-foreground">...</span>}
|
||||
<Button
|
||||
variant={
|
||||
page === pagination.page ? 'default' : 'outline'
|
||||
}
|
||||
variant={page === pagination.page ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => onPageChange(page)}
|
||||
className="w-9"
|
||||
|
||||
@@ -89,9 +89,7 @@ export function OrganizationManagementContent() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold tracking-tight">All Organizations</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Manage organizations and their members
|
||||
</p>
|
||||
<p className="text-muted-foreground">Manage organizations and their members</p>
|
||||
</div>
|
||||
<Button onClick={handleCreateOrganization}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
|
||||
@@ -85,9 +85,7 @@ export function OrganizationMembersContent({ organizationId }: OrganizationMembe
|
||||
{/* Header with Add Member Button */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold tracking-tight">
|
||||
{orgName} Members
|
||||
</h2>
|
||||
<h2 className="text-2xl font-bold tracking-tight">{orgName} Members</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Manage members and their roles within the organization
|
||||
</p>
|
||||
|
||||
@@ -119,14 +119,9 @@ export function OrganizationMembersTable({
|
||||
{formatRole(member.role)}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{format(new Date(member.joined_at), 'MMM d, yyyy')}</TableCell>
|
||||
<TableCell>
|
||||
{format(new Date(member.joined_at), 'MMM d, yyyy')}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<MemberActionMenu
|
||||
member={member}
|
||||
organizationId={organizationId}
|
||||
/>
|
||||
<MemberActionMenu member={member} organizationId={organizationId} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
@@ -141,11 +136,8 @@ export function OrganizationMembersTable({
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Showing {(pagination.page - 1) * pagination.page_size + 1} to{' '}
|
||||
{Math.min(
|
||||
pagination.page * pagination.page_size,
|
||||
pagination.total
|
||||
)}{' '}
|
||||
of {pagination.total} members
|
||||
{Math.min(pagination.page * pagination.page_size, pagination.total)} of{' '}
|
||||
{pagination.total} members
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
@@ -170,13 +162,9 @@ export function OrganizationMembersTable({
|
||||
|
||||
return (
|
||||
<div key={page} className="flex items-center">
|
||||
{showEllipsis && (
|
||||
<span className="px-2 text-muted-foreground">...</span>
|
||||
)}
|
||||
{showEllipsis && <span className="px-2 text-muted-foreground">...</span>}
|
||||
<Button
|
||||
variant={
|
||||
page === pagination.page ? 'default' : 'outline'
|
||||
}
|
||||
variant={page === pagination.page ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => onPageChange(page)}
|
||||
className="w-9"
|
||||
|
||||
@@ -62,9 +62,7 @@ export function BulkActionToolbar({
|
||||
onClearSelection();
|
||||
setPendingAction(null);
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : `Failed to ${pendingAction} users`
|
||||
);
|
||||
toast.error(error instanceof Error ? error.message : `Failed to ${pendingAction} users`);
|
||||
setPendingAction(null);
|
||||
}
|
||||
};
|
||||
@@ -161,9 +159,7 @@ export function BulkActionToolbar({
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{getActionTitle()}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{getActionDescription()}
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogDescription>{getActionDescription()}</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
|
||||
@@ -49,9 +49,7 @@ export function UserActionMenu({ user, isCurrentUser, onEdit }: UserActionMenuPr
|
||||
const deactivateUser = useDeactivateUser();
|
||||
const deleteUser = useDeleteUser();
|
||||
|
||||
const fullName = user.last_name
|
||||
? `${user.first_name} ${user.last_name}`
|
||||
: user.first_name;
|
||||
const fullName = user.last_name ? `${user.first_name} ${user.last_name}` : user.first_name;
|
||||
|
||||
// Handle activate action
|
||||
const handleActivate = async () => {
|
||||
|
||||
@@ -23,21 +23,14 @@ import { Label } from '@/components/ui/label';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Alert } from '@/components/ui/alert';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
useCreateUser,
|
||||
useUpdateUser,
|
||||
type User,
|
||||
} from '@/lib/api/hooks/useAdmin';
|
||||
import { useCreateUser, useUpdateUser, type User } from '@/lib/api/hooks/useAdmin';
|
||||
|
||||
// ============================================================================
|
||||
// Validation Schema
|
||||
// ============================================================================
|
||||
|
||||
const userFormSchema = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.min(1, 'Email is required')
|
||||
.email('Please enter a valid email address'),
|
||||
email: z.string().min(1, 'Email is required').email('Please enter a valid email address'),
|
||||
first_name: z
|
||||
.string()
|
||||
.min(1, 'First name is required')
|
||||
@@ -66,12 +59,7 @@ interface UserFormDialogProps {
|
||||
mode: 'create' | 'edit';
|
||||
}
|
||||
|
||||
export function UserFormDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
user,
|
||||
mode,
|
||||
}: UserFormDialogProps) {
|
||||
export function UserFormDialog({ open, onOpenChange, user, mode }: UserFormDialogProps) {
|
||||
const isEdit = mode === 'edit' && user;
|
||||
const createUser = useCreateUser();
|
||||
const updateUser = useUpdateUser();
|
||||
@@ -130,7 +118,9 @@ export function UserFormDialog({
|
||||
return;
|
||||
}
|
||||
if (!/[A-Z]/.test(data.password)) {
|
||||
form.setError('password', { message: 'Password must contain at least one uppercase letter' });
|
||||
form.setError('password', {
|
||||
message: 'Password must contain at least one uppercase letter',
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -147,7 +137,9 @@ export function UserFormDialog({
|
||||
return;
|
||||
}
|
||||
if (!/[A-Z]/.test(data.password)) {
|
||||
form.setError('password', { message: 'Password must contain at least one uppercase letter' });
|
||||
form.setError('password', {
|
||||
message: 'Password must contain at least one uppercase letter',
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -305,10 +297,7 @@ export function UserFormDialog({
|
||||
onCheckedChange={(checked) => setValue('is_active', checked as boolean)}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="is_active"
|
||||
className="text-sm font-normal cursor-pointer"
|
||||
>
|
||||
<Label htmlFor="is_active" className="text-sm font-normal cursor-pointer">
|
||||
Active (user can log in)
|
||||
</Label>
|
||||
</div>
|
||||
@@ -320,10 +309,7 @@ export function UserFormDialog({
|
||||
onCheckedChange={(checked) => setValue('is_superuser', checked as boolean)}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="is_superuser"
|
||||
className="text-sm font-normal cursor-pointer"
|
||||
>
|
||||
<Label htmlFor="is_superuser" className="text-sm font-normal cursor-pointer">
|
||||
Superuser (admin privileges)
|
||||
</Label>
|
||||
</div>
|
||||
@@ -335,8 +321,8 @@ export function UserFormDialog({
|
||||
{createUser.isError && createUser.error instanceof Error
|
||||
? createUser.error.message
|
||||
: updateUser.error instanceof Error
|
||||
? updateUser.error.message
|
||||
: 'An error occurred'}
|
||||
? updateUser.error.message
|
||||
: 'An error occurred'}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
@@ -355,8 +341,8 @@ export function UserFormDialog({
|
||||
? 'Updating...'
|
||||
: 'Creating...'
|
||||
: isEdit
|
||||
? 'Update User'
|
||||
: 'Create User'}
|
||||
? 'Update User'
|
||||
: 'Create User'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
|
||||
@@ -74,8 +74,7 @@ export function UserListTable({
|
||||
[onSearch]
|
||||
);
|
||||
|
||||
const allSelected =
|
||||
users.length > 0 && users.every((user) => selectedUsers.includes(user.id));
|
||||
const allSelected = users.length > 0 && users.every((user) => selectedUsers.includes(user.id));
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -195,28 +194,18 @@ export function UserListTable({
|
||||
</TableCell>
|
||||
<TableCell>{user.email}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Badge
|
||||
variant={user.is_active ? 'default' : 'secondary'}
|
||||
>
|
||||
<Badge variant={user.is_active ? 'default' : 'secondary'}>
|
||||
{user.is_active ? 'Active' : 'Inactive'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{user.is_superuser ? (
|
||||
<Check
|
||||
className="h-4 w-4 mx-auto text-green-600"
|
||||
aria-label="Yes"
|
||||
/>
|
||||
<Check className="h-4 w-4 mx-auto text-green-600" aria-label="Yes" />
|
||||
) : (
|
||||
<X
|
||||
className="h-4 w-4 mx-auto text-muted-foreground"
|
||||
aria-label="No"
|
||||
/>
|
||||
<X className="h-4 w-4 mx-auto text-muted-foreground" aria-label="No" />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{format(new Date(user.created_at), 'MMM d, yyyy')}
|
||||
</TableCell>
|
||||
<TableCell>{format(new Date(user.created_at), 'MMM d, yyyy')}</TableCell>
|
||||
<TableCell>
|
||||
<UserActionMenu
|
||||
user={user}
|
||||
@@ -237,11 +226,8 @@ export function UserListTable({
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Showing {(pagination.page - 1) * pagination.page_size + 1} to{' '}
|
||||
{Math.min(
|
||||
pagination.page * pagination.page_size,
|
||||
pagination.total
|
||||
)}{' '}
|
||||
of {pagination.total} users
|
||||
{Math.min(pagination.page * pagination.page_size, pagination.total)} of{' '}
|
||||
{pagination.total} users
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
@@ -266,13 +252,9 @@ export function UserListTable({
|
||||
|
||||
return (
|
||||
<div key={page} className="flex items-center">
|
||||
{showEllipsis && (
|
||||
<span className="px-2 text-muted-foreground">...</span>
|
||||
)}
|
||||
{showEllipsis && <span className="px-2 text-muted-foreground">...</span>}
|
||||
<Button
|
||||
variant={
|
||||
page === pagination.page ? 'default' : 'outline'
|
||||
}
|
||||
variant={page === pagination.page ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => onPageChange(page)}
|
||||
className="w-9"
|
||||
|
||||
@@ -28,7 +28,8 @@ export function UserManagementContent() {
|
||||
|
||||
// Convert filter strings to booleans for API
|
||||
const isActiveFilter = filterActive === 'true' ? true : filterActive === 'false' ? false : null;
|
||||
const isSuperuserFilter = filterSuperuser === 'true' ? true : filterSuperuser === 'false' ? false : null;
|
||||
const isSuperuserFilter =
|
||||
filterSuperuser === 'true' ? true : filterSuperuser === 'false' ? false : null;
|
||||
|
||||
// Local state
|
||||
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
|
||||
@@ -85,9 +86,7 @@ export function UserManagementContent() {
|
||||
// istanbul ignore next - Event handlers fully tested in E2E (admin-users.spec.ts)
|
||||
const handleSelectAll = (selected: boolean) => {
|
||||
if (selected) {
|
||||
const selectableUsers = users
|
||||
.filter((u) => u.id !== currentUser?.id)
|
||||
.map((u) => u.id);
|
||||
const selectableUsers = users.filter((u) => u.id !== currentUser?.id).map((u) => u.id);
|
||||
setSelectedUsers(selectableUsers);
|
||||
} else {
|
||||
setSelectedUsers([]);
|
||||
@@ -141,9 +140,7 @@ export function UserManagementContent() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold tracking-tight">All Users</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Manage user accounts and permissions
|
||||
</p>
|
||||
<p className="text-muted-foreground">Manage user accounts and permissions</p>
|
||||
</div>
|
||||
<Button onClick={handleCreateUser}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
|
||||
Reference in New Issue
Block a user