From 5a1e318df7aabe0ab9683be0b988d70761dfec99 Mon Sep 17 00:00:00 2001 From: Sean O'Connor Date: Thu, 5 Dec 2024 13:49:19 -0500 Subject: [PATCH] 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. --- src/app/dashboard/page.tsx | 13 +- .../studies/[id]/participants/page.tsx | 4 - src/components/sidebar.tsx | 5 +- src/components/studies/users-tab.tsx | 108 ++++++----- src/db/seed.ts | 2 +- src/lib/email.ts | 183 ++++++++++++++++-- src/lib/roles.ts | 2 - 7 files changed, 225 insertions(+), 92 deletions(-) diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 4fa0b67..813df6f 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -34,17 +34,6 @@ export default function Dashboard() { studyCount: data.length, activeInvitationCount: 0 }); - - // If there's only one study and we're on the main dashboard, select it - if (data.length === 1) { - const study = { - ...data[0], - createdAt: new Date(data[0].createdAt), - updatedAt: data[0].updatedAt ? new Date(data[0].updatedAt) : null - }; - setActiveStudy(study); - router.push(`/dashboard/studies/${study.id}`); - } } catch (error) { console.error("Error fetching stats:", error); toast({ @@ -55,7 +44,7 @@ export default function Dashboard() { } finally { setIsLoading(false); } - }, [toast, router, setActiveStudy]); + }, [toast]); useEffect(() => { fetchStats(); diff --git a/src/app/dashboard/studies/[id]/participants/page.tsx b/src/app/dashboard/studies/[id]/participants/page.tsx index 41cb7c7..f3d89e0 100644 --- a/src/app/dashboard/studies/[id]/participants/page.tsx +++ b/src/app/dashboard/studies/[id]/participants/page.tsx @@ -166,7 +166,6 @@ export default function ParticipantsList() { - @@ -175,7 +174,6 @@ export default function ParticipantsList() { {[1, 2, 3].map((i) => ( - @@ -250,7 +248,6 @@ export default function ParticipantsList() {
- ID Name Added {canDeleteParticipant && Actions} @@ -259,7 +256,6 @@ export default function ParticipantsList() { {participants.map((participant) => ( - {participant.id} {canViewNames ? participant.name : `Participant ${participant.id}`} diff --git a/src/components/sidebar.tsx b/src/components/sidebar.tsx index a986d9f..0363b98 100644 --- a/src/components/sidebar.tsx +++ b/src/components/sidebar.tsx @@ -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() { {activeStudy?.title || "All Studies"} - diff --git a/src/components/studies/users-tab.tsx b/src/components/studies/users-tab.tsx index 4dfacb7..9eb2803 100644 --- a/src/components/studies/users-tab.tsx +++ b/src/components/studies/users-tab.tsx @@ -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) {
Study Members - Manage users and their roles in this study + {canManageRoles ? 'Manage users and their roles in this study' : 'View study members'}
- {canManageRoles && } + {canManageRoles && ( + canManageRoles && fetchInvitations()} + /> + )} @@ -248,14 +255,15 @@ export function UsersTab({ studyId, permissions }: UsersTabProps) { {user.email} - {canManageRoles ? ( - handleRoleChange(user.id, value)} + disabled={!canManageRoles} + > + + + + {canManageRoles && ( {roles.map((role) => ( @@ -263,10 +271,8 @@ export function UsersTab({ studyId, permissions }: UsersTabProps) { ))} - - ) : ( - {formatRoleName(user.roles[0]?.name || '')} - )} + )} +
))} @@ -275,7 +281,7 @@ export function UsersTab({ studyId, permissions }: UsersTabProps) { - {pendingInvitations.length > 0 && ( + {canManageRoles && pendingInvitations.length > 0 && ( Pending Invitations @@ -290,7 +296,7 @@ export function UsersTab({ studyId, permissions }: UsersTabProps) { Email Role Expires - {canManageRoles && Actions} + Actions
@@ -299,33 +305,31 @@ export function UsersTab({ studyId, permissions }: UsersTabProps) { {invitation.email} {formatRoleName(invitation.roleName)} {new Date(invitation.expiresAt).toLocaleDateString()} - {canManageRoles && ( - - - - - - - - Delete Invitation - - Are you sure you want to delete this invitation? This action cannot be undone. - - - - Cancel - handleDeleteInvitation(invitation.id)} - > - Delete - - - - - - )} + + + + + + + + Delete Invitation + + Are you sure you want to delete this invitation? This action cannot be undone. + + + + Cancel + handleDeleteInvitation(invitation.id)} + > + Delete + + + + + ))} diff --git a/src/db/seed.ts b/src/db/seed.ts index 6b352a0..0dd4093 100644 --- a/src/db/seed.ts +++ b/src/db/seed.ts @@ -110,7 +110,7 @@ function getRoleDescription(roleKey: string): string { PRINCIPAL_INVESTIGATOR: "Lead researcher responsible for study design and oversight", RESEARCHER: - "Study team member with data collection and analysis capabilities", + "Study team member with access to anonymized data and experiment monitoring capabilities", WIZARD: "Operator controlling robot behavior during experiments", OBSERVER: "Team member observing and annotating experiments", ASSISTANT: "Support staff with limited view access", diff --git a/src/lib/email.ts b/src/lib/email.ts index 734df13..7bc548d 100644 --- a/src/lib/email.ts +++ b/src/lib/email.ts @@ -33,32 +33,179 @@ export async function sendInvitationEmail({ token, }: SendInvitationEmailParams) { const inviteUrl = `${process.env.NEXT_PUBLIC_APP_URL}/invite/accept/${token}`; + const roleDisplay = role + .split('_') + .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(' '); const html = ` -

You've been invited to join HRIStudio

-

${inviterName} has invited you to join their study "${studyTitle}" as a ${role}.

-

HRIStudio is a platform for managing human-robot interaction studies and Wizard-of-Oz experiments.

-

Click the button below to accept the invitation and join the study:

- Accept Invitation -

Or copy and paste this URL into your browser:

-

${inviteUrl}

-

This invitation will expire in 7 days.

+ + + + + + HRIStudio Invitation + + + +
+ + + +
+ + + + + + + + + + + + + +
+
+ + + + + + + + +

+ HRI + Studio +

+
+

A platform for managing human-robot interaction studies

+
+

You've been invited to join a research study

+ +

+ ${inviterName} has invited you to join "${studyTitle}" as a ${roleDisplay}. +

+ +

+ HRIStudio helps research teams manage human-robot interaction studies and conduct Wizard-of-Oz experiments efficiently. +

+ + + + + + +
+ Accept Invitation +
+ +

+ If you're having trouble with the button above, copy and paste the URL below into your web browser: +

+

+ ${inviteUrl} +

+ +

+ This invitation will expire in 7 days. +

+
+

+ This is an automated message from HRIStudio. Please do not reply to this email. +

+
+
+ + `; const text = ` You've been invited to join HRIStudio -${inviterName} has invited you to join their study "${studyTitle}" as a ${role}. +${inviterName} has invited you to join "${studyTitle}" as a ${roleDisplay}. -HRIStudio is a platform for managing human-robot interaction studies and Wizard-of-Oz experiments. +HRIStudio helps research teams manage human-robot interaction studies and conduct Wizard-of-Oz experiments efficiently. To accept the invitation, visit this URL: ${inviteUrl} diff --git a/src/lib/roles.ts b/src/lib/roles.ts index accbd73..4f6cf20 100644 --- a/src/lib/roles.ts +++ b/src/lib/roles.ts @@ -34,8 +34,6 @@ export const ROLE_PERMISSIONS: Record> RESEARCHER: [ 'VIEW_STUDY', - 'CREATE_PARTICIPANT', - 'EDIT_PARTICIPANT', 'VIEW_ROBOT_STATUS', 'VIEW_EXPERIMENT', 'VIEW_EXPERIMENT_DATA',