diff --git a/server/routers/user/listInvitations.ts b/server/routers/user/listInvitations.ts index 317a8e72..76e82db5 100644 --- a/server/routers/user/listInvitations.ts +++ b/server/routers/user/listInvitations.ts @@ -72,7 +72,6 @@ export async function listInvitations( next: NextFunction ): Promise { try { - // Validate query parameters const parsedQuery = listInvitationsQuerySchema.safeParse(req.query); if (!parsedQuery.success) { return next( @@ -84,7 +83,6 @@ export async function listInvitations( } const { limit, offset } = parsedQuery.data; - // Validate path parameters const parsedParams = listInvitationsParamsSchema.safeParse(req.params); if (!parsedParams.success) { return next( @@ -96,16 +94,13 @@ export async function listInvitations( } const { orgId } = parsedParams.data; - // Query invitations const invitations = await queryInvitations(orgId, limit, offset); - // Get total count of invitations const [{ count }] = await db .select({ count: sql`count(*)` }) .from(userInvites) .where(sql`${userInvites.orgId} = ${orgId}`); - // Return response return response(res, { data: { invitations, diff --git a/server/routers/user/removeInvitation.ts b/server/routers/user/removeInvitation.ts index 4b4d361b..c825df6d 100644 --- a/server/routers/user/removeInvitation.ts +++ b/server/routers/user/removeInvitation.ts @@ -22,7 +22,6 @@ export async function removeInvitation( next: NextFunction ): Promise { try { - // Validate path parameters const parsedParams = removeInvitationParamsSchema.safeParse(req.params); if (!parsedParams.success) { return next( @@ -35,7 +34,6 @@ export async function removeInvitation( const { orgId, inviteId } = parsedParams.data; - // Delete the invitation from the database const deletedInvitation = await db .delete(userInvites) .where( @@ -46,7 +44,6 @@ export async function removeInvitation( ) .returning(); - // If no rows were deleted, the invitation was not found if (deletedInvitation.length === 0) { return next( createHttpError( @@ -56,7 +53,6 @@ export async function removeInvitation( ); } - // Return success response return response(res, { data: null, success: true, diff --git a/src/app/[orgId]/settings/access/AccessPageHeaderAndNav.tsx b/src/app/[orgId]/settings/access/AccessPageHeaderAndNav.tsx index 98c47f96..84cb659d 100644 --- a/src/app/[orgId]/settings/access/AccessPageHeaderAndNav.tsx +++ b/src/app/[orgId]/settings/access/AccessPageHeaderAndNav.tsx @@ -5,19 +5,25 @@ import { SidebarSettings } from "@app/components/SidebarSettings"; type AccessPageHeaderAndNavProps = { children: React.ReactNode; + hasInvitations: boolean; }; export default function AccessPageHeaderAndNav({ - children + children, + hasInvitations }: AccessPageHeaderAndNavProps) { const sidebarNavItems = [ { title: "Users", - href: `/{orgId}/settings/access/users` - }, - { - title: "Invitations", - href: `/{orgId}/settings/access/invitations` + href: `/{orgId}/settings/access/users`, + children: hasInvitations + ? [ + { + title: "• Invitations", + href: `/{orgId}/settings/access/invitations` + } + ] + : [] }, { title: "Roles", @@ -29,8 +35,7 @@ export default function AccessPageHeaderAndNav({ <> diff --git a/src/app/[orgId]/settings/access/invitations/InvitationsTable.tsx b/src/app/[orgId]/settings/access/invitations/InvitationsTable.tsx index 25f17ad7..74f9aef6 100644 --- a/src/app/[orgId]/settings/access/invitations/InvitationsTable.tsx +++ b/src/app/[orgId]/settings/access/invitations/InvitationsTable.tsx @@ -20,7 +20,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext"; export type InvitationRow = { id: string; email: string; - expiresAt: string; // ISO string or timestamp + expiresAt: string; role: string; }; diff --git a/src/app/[orgId]/settings/access/invitations/page.tsx b/src/app/[orgId]/settings/access/invitations/page.tsx index 02c76a8c..55952773 100644 --- a/src/app/[orgId]/settings/access/invitations/page.tsx +++ b/src/app/[orgId]/settings/access/invitations/page.tsx @@ -28,16 +28,20 @@ export default async function InvitationsPage(props: InvitationsPageProps) { roleId: string; roleName?: string; }[] = []; + let hasInvitations = false; + const res = await internal .get< AxiosResponse<{ invitations: typeof invitations; + pagination: { total: number }; }> >(`/org/${params.orgId}/invitations`, await authCookieHeader()) .catch((e) => {}); if (res && res.status === 200) { invitations = res.data.data.invitations; + hasInvitations = res.data.data.pagination.total > 0; } let org: GetOrgResponse | null = null; @@ -61,13 +65,13 @@ export default async function InvitationsPage(props: InvitationsPageProps) { id: invite.inviteId, email: invite.email, expiresAt: new Date(Number(invite.expiresAt)).toISOString(), - role: invite.roleName || "Unknown Role" // Use roleName if available + role: invite.roleName || "Unknown Role" }; }); return ( <> - + diff --git a/src/app/[orgId]/settings/access/roles/page.tsx b/src/app/[orgId]/settings/access/roles/page.tsx index b0915978..2548257c 100644 --- a/src/app/[orgId]/settings/access/roles/page.tsx +++ b/src/app/[orgId]/settings/access/roles/page.tsx @@ -19,6 +19,8 @@ export default async function RolesPage(props: RolesPageProps) { const params = await props.params; let roles: ListRolesResponse["roles"] = []; + let hasInvitations = false; + const res = await internal .get< AxiosResponse @@ -29,6 +31,21 @@ export default async function RolesPage(props: RolesPageProps) { roles = res.data.data.roles; } + const invitationsRes = await internal + .get< + AxiosResponse<{ + pagination: { total: number }; + }> + >( + `/org/${params.orgId}/invitations?limit=1&offset=0`, + await authCookieHeader() + ) + .catch((e) => {}); + + if (invitationsRes && invitationsRes.status === 200) { + hasInvitations = invitationsRes.data.data.pagination.total > 0; + } + let org: GetOrgResponse | null = null; const getOrg = cache(async () => internal @@ -47,7 +64,7 @@ export default async function RolesPage(props: RolesPageProps) { return ( <> - + diff --git a/src/app/[orgId]/settings/access/users/page.tsx b/src/app/[orgId]/settings/access/users/page.tsx index 68832f0e..a39a4e3a 100644 --- a/src/app/[orgId]/settings/access/users/page.tsx +++ b/src/app/[orgId]/settings/access/users/page.tsx @@ -23,6 +23,8 @@ export default async function UsersPage(props: UsersPageProps) { const user = await getUser(); let users: ListUsersResponse["users"] = []; + let hasInvitations = false; + const res = await internal .get< AxiosResponse @@ -33,6 +35,21 @@ export default async function UsersPage(props: UsersPageProps) { users = res.data.data.users; } + const invitationsRes = await internal + .get< + AxiosResponse<{ + pagination: { total: number }; + }> + >( + `/org/${params.orgId}/invitations?limit=1&offset=0`, + await authCookieHeader() + ) + .catch((e) => {}); + + if (invitationsRes && invitationsRes.status === 200) { + hasInvitations = invitationsRes.data.data.pagination.total > 0; + } + let org: GetOrgResponse | null = null; const getOrg = cache(async () => internal @@ -61,7 +78,7 @@ export default async function UsersPage(props: UsersPageProps) { return ( <> - + diff --git a/src/components/SidebarNav.tsx b/src/components/SidebarNav.tsx index 542327bf..83824243 100644 --- a/src/components/SidebarNav.tsx +++ b/src/components/SidebarNav.tsx @@ -10,15 +10,18 @@ import { SelectContent, SelectItem, SelectTrigger, - SelectValue, + SelectValue } from "@/components/ui/select"; +interface SidebarNavItem { + href: string; + title: string; + icon?: React.ReactNode; + children?: SidebarNavItem[]; +} + interface SidebarNavProps extends React.HTMLAttributes { - items: { - href: string; - title: string; - icon?: React.ReactNode; - }[]; + items: SidebarNavItem[]; disabled?: boolean; } @@ -35,7 +38,8 @@ export function SidebarNav({ const resourceId = params.resourceId as string; const userId = params.userId as string; - const [selectedValue, setSelectedValue] = React.useState(getSelectedValue()); + const [selectedValue, setSelectedValue] = + React.useState(getSelectedValue()); useEffect(() => { setSelectedValue(getSelectedValue()); @@ -62,6 +66,43 @@ export function SidebarNav({ .replace("{userId}", userId); } + function renderItems(items: SidebarNavItem[]) { + return items.map((item) => ( +
+ e.preventDefault() : undefined} + tabIndex={disabled ? -1 : undefined} + aria-disabled={disabled} + > + {item.icon ? ( +
+ {item.icon} + {item.title} +
+ ) : ( + item.title + )} + + {item.children && ( +
+ {renderItems(item.children)}{" "} + {/* Recursively render children */} +
+ )} +
+ )); + } + return (
@@ -94,35 +135,7 @@ export function SidebarNav({ )} {...props} > - {items.map((item) => ( - e.preventDefault() : undefined - } - tabIndex={disabled ? -1 : undefined} - aria-disabled={disabled} - > - {item.icon ? ( -
- {item.icon} - {item.title} -
- ) : ( - item.title - )} - - ))} + {renderItems(items)}
);