diff --git a/frontend/src/app/invite/page.tsx b/frontend/src/app/invite/page.tsx index 6441cd3..e1a61bc 100644 --- a/frontend/src/app/invite/page.tsx +++ b/frontend/src/app/invite/page.tsx @@ -2,7 +2,7 @@ export const dynamic = "force-dynamic"; -import { useEffect, useMemo, useState } from "react"; +import { Suspense, useEffect, useMemo, useState } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; @@ -13,7 +13,7 @@ import { BrandMark } from "@/components/atoms/BrandMark"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -export default function InvitePage() { +function InviteContent() { const router = useRouter(); const searchParams = useSearchParams(); const { isSignedIn } = useAuth(); @@ -146,3 +146,26 @@ export default function InvitePage() { ); } + +export default function InvitePage() { + return ( + +
+
+ +
+
+
+
+
Loading invite…
+
+
+ + } + > + +
+ ); +} diff --git a/frontend/src/app/organization/page.tsx b/frontend/src/app/organization/page.tsx index 2c26bdc..6932e13 100644 --- a/frontend/src/app/organization/page.tsx +++ b/frontend/src/app/organization/page.tsx @@ -2,7 +2,7 @@ export const dynamic = "force-dynamic"; -import { useEffect, useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { useQueryClient } from "@tanstack/react-query"; @@ -328,12 +328,11 @@ export default function OrganizationPage() { const [accessDialogOpen, setAccessDialogOpen] = useState(false); const [activeMemberId, setActiveMemberId] = useState(null); - const [accessScope, setAccessScope] = useState("all"); - const [accessAllRead, setAccessAllRead] = useState(false); - const [accessAllWrite, setAccessAllWrite] = useState(false); - const [accessRole, setAccessRole] = useState("member"); - const [accessMap, setAccessMap] = - useState(defaultBoardAccess); + const [accessScope, setAccessScope] = useState(null); + const [accessAllRead, setAccessAllRead] = useState(null); + const [accessAllWrite, setAccessAllWrite] = useState(null); + const [accessRole, setAccessRole] = useState(null); + const [accessMap, setAccessMap] = useState(null); const [accessError, setAccessError] = useState(null); const orgQuery = useGetMyOrgApiV1OrganizationsMeGet< @@ -426,6 +425,45 @@ export default function OrganizationPage() { }, }); + const memberDetails = + memberDetailsQuery.data?.status === 200 + ? memberDetailsQuery.data.data + : null; + + const defaultAccess = useMemo(() => { + if (!memberDetails) { + return { + role: "member", + scope: "all" as AccessScope, + allRead: false, + allWrite: false, + access: {}, + }; + } + const isAll = + memberDetails.all_boards_read || memberDetails.all_boards_write; + const nextAccess: BoardAccessState = {}; + for (const entry of memberDetails.board_access ?? []) { + nextAccess[entry.board_id] = { + read: entry.can_read || entry.can_write, + write: entry.can_write, + }; + } + return { + role: memberDetails.role, + scope: isAll ? "all" : ("custom" as AccessScope), + allRead: memberDetails.all_boards_read, + allWrite: memberDetails.all_boards_write, + access: nextAccess, + }; + }, [memberDetails]); + + const resolvedAccessRole = accessRole ?? defaultAccess.role; + const resolvedAccessScope = accessScope ?? defaultAccess.scope; + const resolvedAccessAllRead = accessAllRead ?? defaultAccess.allRead; + const resolvedAccessAllWrite = accessAllWrite ?? defaultAccess.allWrite; + const resolvedAccessMap = accessMap ?? defaultAccess.access; + const createInviteMutation = useCreateOrgInviteApiV1OrganizationsMeInvitesPost({ mutation: { @@ -505,37 +543,31 @@ export default function OrganizationPage() { }, }); - useEffect(() => { - if (memberDetailsQuery.data?.status !== 200) return; - const member = memberDetailsQuery.data.data; - setAccessRole(member.role); - const isAll = member.all_boards_read || member.all_boards_write; - setAccessScope(isAll ? "all" : "custom"); - setAccessAllRead(member.all_boards_read); - setAccessAllWrite(member.all_boards_write); - const nextAccess: BoardAccessState = {}; - for (const entry of member.board_access ?? []) { - nextAccess[entry.board_id] = { - read: entry.can_read || entry.can_write, - write: entry.can_write, - }; - } - setAccessMap(nextAccess); + const resetAccessState = () => { + setAccessRole(null); + setAccessScope(null); + setAccessAllRead(null); + setAccessAllWrite(null); + setAccessMap(null); setAccessError(null); - }, [memberDetailsQuery.data]); + }; - useEffect(() => { - if (!accessDialogOpen) { + const handleAccessDialogChange = (open: boolean) => { + setAccessDialogOpen(open); + if (!open) { setActiveMemberId(null); setAccessError(null); + return; } - }, [accessDialogOpen]); + resetAccessState(); + }; - useEffect(() => { - if (!inviteDialogOpen) { + const handleInviteDialogChange = (open: boolean) => { + setInviteDialogOpen(open); + if (!open) { setInviteError(null); } - }, [inviteDialogOpen]); + }; const orgName = orgQuery.data?.status === 200 ? orgQuery.data.data.name : "Organization"; @@ -620,15 +652,18 @@ export default function OrganizationPage() { const openAccessDialog = (memberId: string) => { setActiveMemberId(memberId); setAccessDialogOpen(true); + resetAccessState(); }; const handleSaveAccess = async () => { if (!activeMemberId || !isAdmin) return; const hasAllAccess = - accessScope === "all" && (accessAllRead || accessAllWrite); - const accessList = buildAccessList(accessMap); - const hasCustomAccess = accessScope === "custom" && accessList.length > 0; + resolvedAccessScope === "all" && + (resolvedAccessAllRead || resolvedAccessAllWrite); + const accessList = buildAccessList(resolvedAccessMap); + const hasCustomAccess = + resolvedAccessScope === "custom" && accessList.length > 0; if (!hasAllAccess && !hasCustomAccess) { setAccessError("Select read or write access for at least one board."); @@ -638,12 +673,11 @@ export default function OrganizationPage() { setAccessError(null); try { - if (memberDetailsQuery.data?.status === 200) { - const member = memberDetailsQuery.data.data; - if (member.role !== accessRole) { + if (memberDetails) { + if (memberDetails.role !== resolvedAccessRole) { await updateMemberRoleMutation.mutateAsync({ - memberId: member.id, - data: { role: accessRole }, + memberId: memberDetails.id, + data: { role: resolvedAccessRole }, }); } } @@ -651,9 +685,11 @@ export default function OrganizationPage() { await updateMemberAccessMutation.mutateAsync({ memberId: activeMemberId, data: { - all_boards_read: accessScope === "all" ? accessAllRead : false, - all_boards_write: accessScope === "all" ? accessAllWrite : false, - board_access: accessScope === "custom" ? accessList : [], + all_boards_read: + resolvedAccessScope === "all" ? resolvedAccessAllRead : false, + all_boards_write: + resolvedAccessScope === "all" ? resolvedAccessAllWrite : false, + board_access: resolvedAccessScope === "custom" ? accessList : [], }, }); @@ -953,7 +989,7 @@ export default function OrganizationPage() { - + Invite a member @@ -1037,7 +1073,7 @@ export default function OrganizationPage() { - + Manage member access @@ -1069,7 +1105,10 @@ export default function OrganizationPage() { - @@ -1083,13 +1122,13 @@ export default function OrganizationPage() {