From b863ddf27ab9e20fa47ab6c4c064a35796104396 Mon Sep 17 00:00:00 2001 From: Gen Tamura Date: Mon, 9 Dec 2024 16:16:22 +0900 Subject: [PATCH 01/16] Add server action for team member list --- app/(main)/settings/team/actions.ts | 84 +++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/app/(main)/settings/team/actions.ts b/app/(main)/settings/team/actions.ts index 5045c05a..09ed35ae 100644 --- a/app/(main)/settings/team/actions.ts +++ b/app/(main)/settings/team/actions.ts @@ -296,6 +296,90 @@ export async function updateTeamMemberRole(formData: FormData) { } } +export async function deleteTeamMember(formData: FormData) { + try { + const userId = formData.get("userId") as string; + + if (!isUserId(userId)) { + throw new Error("Invalid user ID"); + } + + // 1. Get current team and verify admin permission + const currentTeam = await fetchCurrentTeam(); + const currentUserRoleResult = await getCurrentUserRole(); + + if ( + !currentUserRoleResult.success || + currentUserRoleResult.data !== "admin" + ) { + throw new Error("Only admin users can remove team members"); + } + + // 2. Get user's dbId from users table + const user = await db + .select({ dbId: users.dbId }) + .from(users) + .where(eq(users.id, userId)) + .limit(1); + + if (user.length === 0) { + throw new Error("User not found"); + } + + // 3. Check if user is an admin and if they are the last admin + const targetUserRole = await db + .select({ role: teamMemberships.role }) + .from(teamMemberships) + .where( + and( + eq(teamMemberships.teamDbId, currentTeam.dbId), + eq(teamMemberships.userDbId, user[0].dbId), + ), + ) + .limit(1); + + if (targetUserRole[0].role === "admin") { + const adminCount = await db + .select({ + count: count(), + }) + .from(teamMemberships) + .where( + and( + eq(teamMemberships.teamDbId, currentTeam.dbId), + eq(teamMemberships.role, "admin"), + ), + ); + + if (adminCount[0].count === 1) { + throw new Error("Cannot remove the last admin from the team"); + } + } + + // 4. Delete team membership + await db + .delete(teamMemberships) + .where( + and( + eq(teamMemberships.teamDbId, currentTeam.dbId), + eq(teamMemberships.userDbId, user[0].dbId), + ), + ); + + revalidatePath("/settings/team"); + + return { success: true }; + } catch (error) { + console.error("Failed to delete team member:", error); + + return { + success: false, + error: + error instanceof Error ? error.message : "Failed to delete team member", + }; + } +} + export async function getCurrentUserRole() { try { const supabaseUser = await getUser(); From fc6aa71024cbb6b0447380a4635f3ea8c5d295b1 Mon Sep 17 00:00:00 2001 From: Gen Tamura Date: Mon, 9 Dec 2024 16:19:19 +0900 Subject: [PATCH 02/16] Add deleting ui for member list item --- .../settings/team/team-members-list-item.tsx | 53 +++++++++++++++---- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/app/(main)/settings/team/team-members-list-item.tsx b/app/(main)/settings/team/team-members-list-item.tsx index de664e84..5eb26b8f 100644 --- a/app/(main)/settings/team/team-members-list-item.tsx +++ b/app/(main)/settings/team/team-members-list-item.tsx @@ -9,9 +9,9 @@ import { SelectValue, } from "@/components/ui/select"; import type { TeamRole } from "@/drizzle"; -import { Check, Pencil, X } from "lucide-react"; +import { Check, Pencil, Trash2, X } from "lucide-react"; import { useState } from "react"; -import { updateTeamMemberRole } from "./actions"; +import { deleteTeamMember, updateTeamMemberRole } from "./actions"; type TeamMemberListItemProps = { userId: string; @@ -81,12 +81,38 @@ export function TeamMemberListItem({ setError(""); }; + const handleDelete = async () => { + setError(""); + + try { + setIsLoading(true); + + const formData = new FormData(); + formData.append("userId", userId); + + const { success, error } = await deleteTeamMember(formData); + + if (!success) { + const errorMsg = error || "Failed to delete member"; + setError(errorMsg); + console.error(errorMsg); + } + } catch (error) { + if (error instanceof Error) { + setError(error.message); + } + console.error("Error:", error); + } finally { + setIsLoading(false); + } + }; + return (
{displayName || "No display name"}
{email || "No email"}
-
+
{isEditingRole ? ( <>