refactor(dashboard, participants, sidebar, users-tab, email): Clean up and enhance component logic and UI

- Removed unused code and comments from the Dashboard component to streamline functionality.
- Updated the ParticipantsList component by removing unnecessary skeleton loaders and table headers for improved clarity.
- Adjusted the Sidebar component to redirect to the main dashboard instead of the studies page when no study is selected.
- Enhanced the UsersTab component to conditionally fetch invitations based on user permissions and improved role management UI.
- Revamped the invitation email template for better presentation and clarity, including a more structured HTML format and improved messaging.
- Updated role descriptions in the seed script for better accuracy and clarity.
This commit is contained in:
2024-12-05 13:49:19 -05:00
parent 8405a49d45
commit 5a1e318df7
7 changed files with 225 additions and 92 deletions

View File

@@ -46,7 +46,7 @@ const getNavItems = (studyId?: number) => [
requiresStudy: false,
hideWithStudy: true
},
{
{
name: "Participants",
href: `/dashboard/studies/${studyId}/participants`,
icon: UsersRoundIcon,
@@ -109,7 +109,7 @@ export function Sidebar() {
const handleStudyChange = (value: string) => {
if (value === "all") {
setActiveStudy(null);
router.push("/dashboard/studies");
router.push("/dashboard");
} else {
const study = studies.find(s => s.id.toString() === value);
if (study) {
@@ -131,7 +131,6 @@ export function Sidebar() {
<span className="truncate">
{activeStudy?.title || "All Studies"}
</span>
<ChevronDown className="h-4 w-4 opacity-50" />
</div>
</SelectTrigger>
<SelectContent className="sidebar-dropdown-content">

View File

@@ -91,6 +91,10 @@ export function UsersTab({ studyId, permissions }: UsersTabProps) {
const fetchInvitations = useCallback(async () => {
try {
const response = await fetch(`/api/invitations?studyId=${studyId}`);
if (response.status === 403) {
// Silently handle 403 errors as they're expected for researchers
return;
}
if (!response.ok) throw new Error("Failed to fetch invitations");
const data = await response.json();
setInvitations(data.data || []);
@@ -109,9 +113,7 @@ export function UsersTab({ studyId, permissions }: UsersTabProps) {
const response = await fetch("/api/roles");
if (!response.ok) throw new Error("Failed to fetch roles");
const data = await response.json();
setRoles(data.filter((role: Role) =>
!['admin'].includes(role.name)
));
setRoles(data);
} catch (error) {
console.error("Error fetching roles:", error);
toast({
@@ -125,15 +127,15 @@ export function UsersTab({ studyId, permissions }: UsersTabProps) {
const fetchData = useCallback(async () => {
setIsLoading(true);
try {
await Promise.all([
fetchUsers(),
fetchInvitations(),
fetchRoles(),
]);
const promises = [fetchUsers(), fetchRoles()];
if (canManageRoles) {
promises.push(fetchInvitations());
}
await Promise.all(promises);
} finally {
setIsLoading(false);
}
}, [fetchUsers, fetchInvitations, fetchRoles]);
}, [fetchUsers, fetchInvitations, fetchRoles, canManageRoles]);
useEffect(() => {
fetchData();
@@ -218,10 +220,15 @@ export function UsersTab({ studyId, permissions }: UsersTabProps) {
<div>
<CardTitle>Study Members</CardTitle>
<CardDescription>
Manage users and their roles in this study
{canManageRoles ? 'Manage users and their roles in this study' : 'View study members'}
</CardDescription>
</div>
{canManageRoles && <InviteUserDialog studyId={studyId} onInviteSent={fetchInvitations} />}
{canManageRoles && (
<InviteUserDialog
studyId={studyId}
onInviteSent={() => canManageRoles && fetchInvitations()}
/>
)}
</div>
</CardHeader>
<CardContent>
@@ -248,14 +255,15 @@ export function UsersTab({ studyId, permissions }: UsersTabProps) {
</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>
{canManageRoles ? (
<Select
value={user.roles[0]?.id.toString()}
onValueChange={(value) => handleRoleChange(user.id, value)}
>
<SelectTrigger className="w-[180px]">
<SelectValue />
</SelectTrigger>
<Select
value={user.roles[0]?.id.toString()}
onValueChange={(value) => handleRoleChange(user.id, value)}
disabled={!canManageRoles}
>
<SelectTrigger className="w-[180px]">
<SelectValue />
</SelectTrigger>
{canManageRoles && (
<SelectContent>
{roles.map((role) => (
<SelectItem key={role.id} value={role.id.toString()}>
@@ -263,10 +271,8 @@ export function UsersTab({ studyId, permissions }: UsersTabProps) {
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<span>{formatRoleName(user.roles[0]?.name || '')}</span>
)}
)}
</Select>
</TableCell>
</TableRow>
))}
@@ -275,7 +281,7 @@ export function UsersTab({ studyId, permissions }: UsersTabProps) {
</CardContent>
</Card>
{pendingInvitations.length > 0 && (
{canManageRoles && pendingInvitations.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Pending Invitations</CardTitle>
@@ -290,7 +296,7 @@ export function UsersTab({ studyId, permissions }: UsersTabProps) {
<TableHead>Email</TableHead>
<TableHead>Role</TableHead>
<TableHead>Expires</TableHead>
{canManageRoles && <TableHead className="w-[100px]">Actions</TableHead>}
<TableHead className="w-[100px]">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -299,33 +305,31 @@ export function UsersTab({ studyId, permissions }: UsersTabProps) {
<TableCell>{invitation.email}</TableCell>
<TableCell>{formatRoleName(invitation.roleName)}</TableCell>
<TableCell>{new Date(invitation.expiresAt).toLocaleDateString()}</TableCell>
{canManageRoles && (
<TableCell>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="ghost" size="icon">
<Trash2Icon className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Invitation</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete this invitation? This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => handleDeleteInvitation(invitation.id)}
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</TableCell>
)}
<TableCell>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="ghost" size="icon">
<Trash2Icon className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Invitation</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete this invitation? This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => handleDeleteInvitation(invitation.id)}
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</TableCell>
</TableRow>
))}
</TableBody>