refactor(workspaces): normalize workspace bootstrap and edit flows

This commit is contained in:
2026-04-24 22:22:28 +03:30
parent dfe280d9a1
commit 790e5f1dba
6 changed files with 199 additions and 207 deletions

View File

@@ -1,7 +1,7 @@
import { authFetch } from "./client"; import { authFetch } from "./client";
export interface Workspace { export interface Workspace {
id: string; id: string;
name: string; name: string;
description?: string; description?: string;
owner?: string; owner?: string;
@@ -16,9 +16,9 @@ export interface PaginatedResponse<T> {
results: T[]; results: T[];
} }
export interface WorkspaceMembership { export interface WorkspaceMembership {
id: string; id: string;
workspace: string; workspace: string;
user: { user: {
id: string; id: string;
email: string; email: string;
@@ -29,14 +29,27 @@ export interface WorkspaceMembership {
role: 'owner' | 'admin' | 'member' | 'guest'; role: 'owner' | 'admin' | 'member' | 'guest';
is_active: boolean; is_active: boolean;
joined_at?: string; joined_at?: string;
[key: string]: any; [key: string]: any;
} }
export const fetchWorkspaces = async (params?: Record<string, string>): Promise<PaginatedResponse<Workspace>> => { type QueryValue = string | number | boolean | undefined | null;
const query = params ? new URLSearchParams(params).toString() : '';
const url = `/api/workspaces/${query ? `?${query}` : ''}`; const toQueryString = (params?: Record<string, QueryValue>) => {
const response = await authFetch(url); if (!params) return "";
const query = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== "") {
query.set(key, String(value));
}
});
return query.toString();
};
export const fetchWorkspaces = async (params?: Record<string, QueryValue>): Promise<PaginatedResponse<Workspace>> => {
const query = toQueryString(params);
const url = `/api/workspaces/${query ? `?${query}` : ''}`;
const response = await authFetch(url);
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to fetch workspaces"); throw new Error("Failed to fetch workspaces");
@@ -98,9 +111,9 @@ export const deleteWorkspace = async (id: string): Promise<void> => {
} }
}; };
export const fetchWorkspaceMemberships = async (params?: Record<string, string>): Promise<PaginatedResponse<WorkspaceMembership>> => { export const fetchWorkspaceMemberships = async (params?: Record<string, QueryValue>): Promise<PaginatedResponse<WorkspaceMembership>> => {
const queryParams = new URLSearchParams((params || {})); const queryParams = toQueryString(params);
const response = await authFetch(`/api/workspace-memberships/?${queryParams.toString()}`); const response = await authFetch(`/api/workspace-memberships/?${queryParams.toString()}`);
if (!response.ok) throw new Error("Failed to fetch workspace memberships"); if (!response.ok) throw new Error("Failed to fetch workspace memberships");

View File

@@ -40,22 +40,22 @@ export const WorkspaceSelector: React.FC = () => {
refreshWorkspacesList(); refreshWorkspacesList();
}) as EventListener; }) as EventListener;
const handleWorkspaceCreated = ((e: CustomEvent) => { const handleWorkspaceCreated = ((e: CustomEvent) => {
if (e.detail) { if (e.detail?.id) {
setActiveWorkspace(e.detail); setActiveWorkspace(e.detail);
} }
refreshWorkspacesList(); refreshWorkspacesList();
}) as EventListener; }) as EventListener;
const handleWorkspaceEdited = ((e: CustomEvent) => { const handleWorkspaceEdited = ((e: CustomEvent) => {
// آپدیت نام کارتابل در نوبار در صورتی که کارتابل فعال ویرایش شده باشد // آپدیت نام کارتابل در نوبار در صورتی که کارتابل فعال ویرایش شده باشد
if (activeWorkspace?.id === e.detail?.id) { if (activeWorkspace?.id === e.detail?.id) {
setActiveWorkspace({ setActiveWorkspace({
...activeWorkspace, ...activeWorkspace,
name: e.detail.name, name: e.detail.name,
description: e.detail.description description: e.detail.description
}); } as Workspace);
} }
refreshWorkspacesList(); refreshWorkspacesList();
}) as EventListener; }) as EventListener;

View File

@@ -1,71 +1,47 @@
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react'; import { createContext, useContext, useState, useEffect, type ReactNode } from 'react';
import { getUserProfile } from '../api/users'; import { getUserProfile } from '../api/users';
import { fetchWorkspaces } from '../api/workspaces';
interface User {
interface User { id: string;
id: string; mobile: string;
phone_number: string; first_name: string;
first_name: string; last_name: string;
last_name: string; email?: string;
} profile_picture?: string | null;
}
interface Workspace {
id: string; interface AppContextType {
name: string; user: User | null;
} refreshUser: () => Promise<void>;
}
interface AppContextType {
user: User | null; const AppContext = createContext<AppContextType | null>(null);
workspaces: Workspace[];
activeWorkspace: Workspace | null; export const AppProvider = ({ children }: { children: ReactNode }) => {
setActiveWorkspace: (ws: Workspace) => void; const [user, setUser] = useState<User | null>(null);
fetchInitialData: () => Promise<void>;
} const refreshUser = async () => {
try {
const AppContext = createContext<AppContextType | null>(null); const userData = await getUserProfile();
setUser(userData);
export const AppProvider = ({ children }: { children: ReactNode }) => { } catch (error) {
const [user, setUser] = useState<User | null>(null); console.error("Failed to fetch user context data:", error);
const [workspaces, setWorkspaces] = useState<Workspace[]>([]); setUser(null);
const [activeWorkspace, setActiveWorkspace] = useState<Workspace | null>(null); }
};
const fetchInitialData = async () => {
try { useEffect(() => {
const [userData, wsData] = await Promise.all([ if (localStorage.getItem('accessToken')) {
getUserProfile(), void refreshUser();
fetchWorkspaces() // fetchWorkspaces({ limit: 50 }) }
]); }, []);
setUser(userData); return (
<AppContext.Provider value={{ user, refreshUser }}>
const workspacesList = Array.isArray(wsData.data) ? wsData.data : (wsData?.data?.results || []); {children}
</AppContext.Provider>
setWorkspaces(workspacesList); );
};
const savedWsId = localStorage.getItem('active_workspace');
const targetWs = workspacesList.find((w: Workspace) => w.id === savedWsId) || workspacesList[0];
if (targetWs) {
setActiveWorkspace(targetWs);
localStorage.setItem('active_workspace', targetWs.id);
}
} catch (error) {
console.error("Failed to fetch initial context data:", error);
}
};
useEffect(() => {
if (localStorage.getItem('accessToken')) {
fetchInitialData();
}
}, []);
return (
<AppContext.Provider value={{ user, workspaces, activeWorkspace, setActiveWorkspace, fetchInitialData }}>
{children}
</AppContext.Provider>
);
};
export const useAppContext = () => { export const useAppContext = () => {
const context = useContext(AppContext); const context = useContext(AppContext);

View File

@@ -1,17 +1,18 @@
import { createContext, useContext, useState, useEffect, ReactNode } from "react" import { createContext, useContext, useState, useEffect, type ReactNode } from "react"
import { fetchWorkspaces, createWorkspace, type Workspace } from "../api/workspaces" import { fetchWorkspaces, createWorkspace, type Workspace } from "../api/workspaces"
import { useTranslation } from "../hooks/useTranslation" import { useTranslation } from "../hooks/useTranslation"
import { toast } from "sonner" import { toast } from "sonner"
import { Button } from "../components/ui/button" import { Button } from "../components/ui/button"
import { Input } from "../components/ui/input" import { Input } from "../components/ui/input"
interface WorkspaceContextType { interface WorkspaceContextType {
workspaces: Workspace[] workspaces: Workspace[]
activeWorkspace: Workspace | null activeWorkspace: Workspace | null
setActiveWorkspace: (workspace: Workspace) => void setActiveWorkspace: (workspace: Workspace | null) => void
addWorkspace: (name: string) => Promise<void> addWorkspace: (name: string) => Promise<void>
isLoading: boolean refreshWorkspaces: () => Promise<void>
} isLoading: boolean
}
const WorkspaceContext = createContext<WorkspaceContextType | undefined>(undefined) const WorkspaceContext = createContext<WorkspaceContextType | undefined>(undefined)
@@ -29,45 +30,58 @@ export const WorkspaceProvider = ({ children }: { children: ReactNode }) => {
const [newWorkspaceName, setNewWorkspaceName] = useState("") const [newWorkspaceName, setNewWorkspaceName] = useState("")
const [isCreatingFirst, setIsCreatingFirst] = useState(false) const [isCreatingFirst, setIsCreatingFirst] = useState(false)
const isAuthenticated = !!localStorage.getItem("accessToken") const isAuthenticated = !!localStorage.getItem("accessToken")
useEffect(() => { const refreshWorkspaces = async () => {
if (!isAuthenticated) { if (!isAuthenticated) {
setIsLoading(false) setIsLoading(false)
return return
} }
const loadWorkspaces = async () => { try {
try { setIsLoading(true)
const response = await fetchWorkspaces() const response = await fetchWorkspaces()
const data = Array.isArray(response) ? response : (response?.results || []) const data = Array.isArray(response) ? response : (response?.results || [])
setWorkspaces(data) setWorkspaces(data)
if (data.length > 0) { if (data.length > 0) {
const storedId = localStorage.getItem("activeWorkspaceId") const storedId = localStorage.getItem("activeWorkspaceId")
const stored = data.find((w: Workspace) => w.id === storedId) const stored = data.find((w: Workspace) => w.id === storedId)
if (stored) { if (stored) {
setActiveWorkspaceState(stored) setActiveWorkspaceState(stored)
} else { } else {
setActiveWorkspaceState(data[0]) setActiveWorkspaceState(data[0])
localStorage.setItem("activeWorkspaceId", data[0].id) localStorage.setItem("activeWorkspaceId", data[0].id)
} }
} } else {
} catch (error) { setActiveWorkspaceState(null)
console.error(error) localStorage.removeItem("activeWorkspaceId")
} finally { }
setIsLoading(false) } catch (error) {
} console.error(error)
} } finally {
setIsLoading(false)
loadWorkspaces() }
}, [isAuthenticated]) }
const setActiveWorkspace = (workspace: Workspace) => { useEffect(() => {
setActiveWorkspaceState(workspace) if (!isAuthenticated) {
localStorage.setItem("activeWorkspaceId", workspace.id) setIsLoading(false)
} return
}
void refreshWorkspaces()
}, [isAuthenticated])
const setActiveWorkspace = (workspace: Workspace | null) => {
setActiveWorkspaceState(workspace)
if (workspace) {
localStorage.setItem("activeWorkspaceId", workspace.id)
} else {
localStorage.removeItem("activeWorkspaceId")
}
}
const addWorkspace = async (name: string) => { const addWorkspace = async (name: string) => {
try { try {
@@ -75,10 +89,10 @@ export const WorkspaceProvider = ({ children }: { children: ReactNode }) => {
const newWs = await createWorkspace({ name, description: "" }) const newWs = await createWorkspace({ name, description: "" })
setWorkspaces((prev) => [...prev, newWs]) setWorkspaces((prev) => [...prev, newWs])
setActiveWorkspace(newWs) setActiveWorkspace(newWs)
toast.success(t.workspace?.createSuccess || "Workspace created!") toast.success(t.workspace?.successCreate || t.workspace?.toast?.successCreate || "Workspace created!")
} catch (error) { } catch (error) {
toast.error(t.workspace?.createError || "Failed to create workspace") toast.error(t.workspace?.toast?.errorCreate || "Failed to create workspace")
throw error throw error
} finally { } finally {
setIsCreatingFirst(false) setIsCreatingFirst(false)
setNewWorkspaceName("") setNewWorkspaceName("")
@@ -123,10 +137,10 @@ export const WorkspaceProvider = ({ children }: { children: ReactNode }) => {
} }
return ( return (
<WorkspaceContext.Provider <WorkspaceContext.Provider
value={{ workspaces, activeWorkspace, setActiveWorkspace, addWorkspace, isLoading }} value={{ workspaces, activeWorkspace, setActiveWorkspace, addWorkspace, refreshWorkspaces, isLoading }}
> >
{children} {children}
</WorkspaceContext.Provider> </WorkspaceContext.Provider>
) )
} }

View File

@@ -54,7 +54,7 @@ export default function WorkspaceCreate() {
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [memberIdToDelete, setMemberIdToDelete] = useState<string | null>(null); const [memberIdToDelete, setMemberIdToDelete] = useState<string | null>(null);
const searchTimeoutRef = useRef<NodeJS.Timeout>(); const searchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const hasUnsavedChanges = name.trim() !== '' || description.trim() !== '' || members.length > 0; const hasUnsavedChanges = name.trim() !== '' || description.trim() !== '' || members.length > 0;
useEffect(() => { useEffect(() => {

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef, Fragment, useMemo } from 'react'; import React, { useState, useEffect, useRef, Fragment, useMemo, useCallback } from 'react';
import { useBlocker, useNavigate, useParams } from 'react-router-dom'; import { useBlocker, useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from '../hooks/useTranslation'; import { useTranslation } from '../hooks/useTranslation';
import { AlertCircle, UserPlus, Trash2, Shield } from 'lucide-react'; import { AlertCircle, UserPlus, Trash2, Shield } from 'lucide-react';
@@ -15,7 +15,7 @@ import {
import { searchUserByExactMobile, type SearchedUser } from '../api/users'; import { searchUserByExactMobile, type SearchedUser } from '../api/users';
import { useAppContext } from '../context/AppContext'; import { useAppContext } from '../context/AppContext';
import { Button } from '../components/ui/button'; import { Button } from '../components/ui/button';
import { InfiniteScroll } from '../components/infiniteScroll'; import { InfiniteScroll } from '../components/InfiniteScroll';
import { Select } from '../components/ui/Select'; import { Select } from '../components/ui/Select';
import { Input } from '../components/ui/input'; import { Input } from '../components/ui/input';
import { TextAreaInput } from '../components/ui/TextAreaInput'; import { TextAreaInput } from '../components/ui/TextAreaInput';
@@ -67,12 +67,13 @@ export default function EditWorkspace() {
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [memberIdToDelete, setMemberIdToDelete] = useState<string | null>(null); const [memberIdToDelete, setMemberIdToDelete] = useState<string | null>(null);
const searchTimeoutRef = useRef<NodeJS.Timeout>(); const searchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const [initialData, setInitialData] = useState({ const [initialData, setInitialData] = useState({
name: '', name: '',
description: '', description: '',
}); });
const hasUnsavedChanges = useMemo(() => { const hasUnsavedChanges = useMemo(() => {
if (isLoading) return false; if (isLoading) return false;
@@ -100,13 +101,6 @@ export default function EditWorkspace() {
return false; return false;
}); });
useBlocker(({ currentLocation, nextLocation }) => {
if (hasUnsavedChanges && !isSaving && currentLocation.pathname !== nextLocation.pathname) {
return !window.confirm(t.confirmLeave || "You have unsaved changes. Are you sure you want to leave?");
}
return false;
});
useEffect(() => { useEffect(() => {
if (id) loadData(); if (id) loadData();
}, [id]); }, [id]);
@@ -125,7 +119,9 @@ export default function EditWorkspace() {
setMembers(results); setMembers(results);
setOffset(LIMIT); setOffset(LIMIT);
setHasMore(!!membersData.next);
// Robust hasMore check: use `.next` if available, otherwise check if array filled the limit
setHasMore(membersData.next ? true : results.length >= LIMIT);
setInitialData({ setInitialData({
name: workspaceData.name, name: workspaceData.name,
@@ -139,22 +135,35 @@ export default function EditWorkspace() {
} }
}; };
const loadMoreMembers = async () => { const loadMoreMembers = useCallback(async () => {
if (isLoadingMembers || !hasMore || !id) return; if (isLoadingMembers || !hasMore || !id) return;
try { try {
setIsLoadingMembers(true); setIsLoadingMembers(true);
const membersData = await fetchWorkspaceMemberships({ workspace: id, limit: `${LIMIT}`, offset: `${offset}` });
const results = membersData.results || [];
setMembers((prev) => [...prev, ...results]); // Send as pure numbers, axios handles them cleanly
setOffset((prev) => prev + LIMIT); const membersData = await fetchWorkspaceMemberships({
setHasMore(!!membersData.next); workspace: id,
limit: LIMIT,
offset: offset
});
const results = membersData.results || (Array.isArray(membersData) ? membersData : []);
setMembers((prev) => {
// Safe deduplication to avoid React key warnings
const existingIds = new Set(prev.map(m => m.id));
const newItems = results.filter((item: any) => !existingIds.has(item.id));
return [...prev, ...newItems];
});
setOffset(offset + LIMIT);
setHasMore(membersData.next ? true : results.length >= LIMIT);
} catch (error) { } catch (error) {
console.error("Failed to load more members", error); console.error("Failed to load more members", error);
} finally { } finally {
setIsLoadingMembers(false); setIsLoadingMembers(false);
} }
}; }, [id, isLoadingMembers, hasMore, offset]);
useEffect(() => { useEffect(() => {
if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current); if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current);
@@ -191,14 +200,11 @@ export default function EditWorkspace() {
if (!name.trim() || !id) return; if (!name.trim() || !id) return;
try { try {
setIsSaving(true); setIsSaving(true);
await updateWorkspace(id, { name, description }); await updateWorkspace(id, { name, description });
toast.success(t.workspace?.toast?.successUpdate || "Workspace updated successfully."); toast.success(t.workspace?.toast?.successUpdate || "Workspace updated successfully.");
window.dispatchEvent(new CustomEvent('workspace_edited', { window.dispatchEvent(new CustomEvent('workspace_edited', {
detail: { id, name, description } detail: { id, name, description }
})); }));
navigate('/workspaces'); navigate('/workspaces');
} catch (error) { } catch (error) {
toast.error(t.workspace?.toast?.errorUpdate || "Failed to update workspace."); toast.error(t.workspace?.toast?.errorUpdate || "Failed to update workspace.");
@@ -210,11 +216,11 @@ export default function EditWorkspace() {
const handleAddMember = async () => { const handleAddMember = async () => {
if (!searchResult || !id) return; if (!searchResult || !id) return;
try { try {
const newMembership = await addWorkspaceMembership({ const newMembership = await addWorkspaceMembership({
workspace: id, workspace: id,
user: searchResult.id, user: String(searchResult.id),
role: newMemberRole role: newMemberRole
}); });
setMembers([newMembership, ...members]); setMembers([newMembership, ...members]);
toast.success(t.workspace?.toast?.successAdd || "Member added successfully."); toast.success(t.workspace?.toast?.successAdd || "Member added successfully.");
setSearchQuery(''); setSearchQuery('');
@@ -264,8 +270,6 @@ export default function EditWorkspace() {
</h1> </h1>
<div className="flex flex-col lg:flex-row gap-4 sm:gap-6 flex-1 min-h-0"> <div className="flex flex-col lg:flex-row gap-4 sm:gap-6 flex-1 min-h-0">
{/* --- ستون سمت چپ: فرم ویرایش --- */}
<div className="w-full lg:w-1/3 lg:max-w-md flex flex-col shrink-0 overflow-y-auto bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 shadow-sm"> <div className="w-full lg:w-1/3 lg:max-w-md flex flex-col shrink-0 overflow-y-auto bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 shadow-sm">
<form onSubmit={handleSubmit} className="flex flex-col h-full p-6"> <form onSubmit={handleSubmit} className="flex flex-col h-full p-6">
<div className="mb-4"> <div className="mb-4">
@@ -292,27 +296,17 @@ export default function EditWorkspace() {
/> />
</div> </div>
<div className="mt-auto pt-6 flex justify-end gap-3 border-t border-slate-100 dark:border-slate-800 shrink-0"> <div className="mt-auto pt-6 flex justify-end gap-3 border-t border-slate-100 dark:border-slate-800 shrink-0">
<Button <Button type="button" variant="ghost" onClick={() => navigate('/workspaces')}>
type="button"
variant="ghost"
onClick={() => navigate('/workspaces')}
>
{t.actions?.cancel || "Cancel"} {t.actions?.cancel || "Cancel"}
</Button> </Button>
<Button <Button type="submit" disabled={isSaving || !name.trim()}>
type="submit"
disabled={isSaving || !name.trim()}
>
{isSaving ? (t.workspace?.loading || "Saving...") : (t.workspace?.save || "Save")} {isSaving ? (t.workspace?.loading || "Saving...") : (t.workspace?.save || "Save")}
</Button> </Button>
</div> </div>
</form> </form>
</div> </div>
{/* --- ستون سمت راست: لیست اعضا --- */}
<div className="w-full lg:w-2/3 flex-1 flex flex-col min-h-100 lg:min-h-0 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 shadow-sm overflow-hidden"> <div className="w-full lg:w-2/3 flex-1 flex flex-col min-h-100 lg:min-h-0 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 shadow-sm overflow-hidden">
{/* بخش جستجو و هدر (ثابت در بالا) */}
<div className="p-6 shrink-0 border-b border-slate-100 dark:border-slate-800 bg-white dark:bg-slate-900 z-10"> <div className="p-6 shrink-0 border-b border-slate-100 dark:border-slate-800 bg-white dark:bg-slate-900 z-10">
<h2 className="text-lg font-semibold text-slate-900 dark:text-white mb-4"> <h2 className="text-lg font-semibold text-slate-900 dark:text-white mb-4">
{ t.workspace?.members || "Members" } { t.workspace?.members || "Members" }
@@ -391,7 +385,6 @@ export default function EditWorkspace() {
)} )}
</div> </div>
{/* لیست اعضا (با قابلیت اسکرول مجزا) */}
<div className="flex-1 overflow-y-auto p-6 space-y-3 bg-slate-50/30 dark:bg-slate-900/30"> <div className="flex-1 overflow-y-auto p-6 space-y-3 bg-slate-50/30 dark:bg-slate-900/30">
<InfiniteScroll <InfiniteScroll
onLoadMore={loadMoreMembers} onLoadMore={loadMoreMembers}
@@ -438,8 +431,10 @@ export default function EditWorkspace() {
) : ( ) : (
<span className="text-xs px-2 py-1 bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 rounded-md capitalize flex items-center gap-1"> <span className="text-xs px-2 py-1 bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 rounded-md capitalize flex items-center gap-1">
{m.role === 'owner' && <Shield className="w-3 h-3" />} {m.role === 'owner' && <Shield className="w-3 h-3" />}
{m.role ? t.workspace?.roles?.[m.role] || m.role : "-"} {m.role && m.role in t.workspace.roles
</span> ? t.workspace.roles[m.role as keyof typeof t.workspace.roles]
: m.role || "-"}
</span>
)} )}
{canManageMembers && !isThisMemberTheFirstOwner && (isFirstOwner || m.role !== 'owner') && ( {canManageMembers && !isThisMemberTheFirstOwner && (isFirstOwner || m.role !== 'owner') && (
@@ -504,16 +499,10 @@ export default function EditWorkspace() {
{t.workspace?.confirmDeleteMessage || "Are you sure you want to remove this member from the workspace?"} {t.workspace?.confirmDeleteMessage || "Are you sure you want to remove this member from the workspace?"}
</p> </p>
<div className="mt-6 flex justify-end gap-3"> <div className="mt-6 flex justify-end gap-3">
<Button <Button variant="secondary" onClick={() => setIsDeleteDialogOpen(false)}>
variant="secondary"
onClick={() => setIsDeleteDialogOpen(false)}
>
{t.actions?.cancel || "Cancel"} {t.actions?.cancel || "Cancel"}
</Button> </Button>
<Button <Button variant="destructive" onClick={handleDeleteMember}>
variant="destructive"
onClick={handleDeleteMember}
>
{t.actions?.delete || "Delete"} {t.actions?.delete || "Delete"}
</Button> </Button>
</div> </div>