feat(permissions): gate workspace resources by role

This commit is contained in:
2026-04-25 18:48:49 +03:30
parent c8c689e693
commit 7f0e00f09d
11 changed files with 511 additions and 200 deletions

View File

@@ -1,7 +1,13 @@
import { useEffect, useState } from "react"
import { Plus, Building2, Loader2, Pencil, Trash2 } from "lucide-react"
import { useWorkspace } from "../context/WorkspaceContext"
import { useTranslation } from "../hooks/useTranslation"
import { useTranslation } from "../hooks/useTranslation"
import {
CLIENTS_CREATE,
CLIENTS_DELETE,
CLIENTS_EDIT,
canWorkspace,
} from "../lib/permissions"
import { type Client } from "../types/client"
import { getClients } from "../api/clients"
import CreateClientModal from "../components/CreateClientModal"
@@ -32,8 +38,12 @@ export default function Clients() {
const [editClient, setEditClient] = useState<Client | null>(null)
const [deleteClient, setDeleteClient] = useState<Client | null>(null)
const { t, lang } = useTranslation()
const isFa = lang === "fa"
const { t, lang } = useTranslation()
const isFa = lang === "fa"
const workspaceRole = activeWorkspace?.my_role
const canCreateClient = canWorkspace(workspaceRole, CLIENTS_CREATE)
const canEditClient = canWorkspace(workspaceRole, CLIENTS_EDIT)
const canDeleteClient = canWorkspace(workspaceRole, CLIENTS_DELETE)
const orderingOptions = [
{ value: "-created_at", label: t.ordering?.createdAtDesc || "Newest First" },
@@ -114,10 +124,12 @@ export default function Clients() {
</p>
</div>
<Button onClick={() => setIsCreateModalOpen(true)} className="flex items-center gap-2">
<Plus className="w-4 h-4" />
{t.clients.addClient}
</Button>
{canCreateClient && (
<Button onClick={() => setIsCreateModalOpen(true)} className="flex items-center gap-2">
<Plus className="w-4 h-4" />
{t.clients.addClient}
</Button>
)}
</div>
<FilterBar
@@ -159,24 +171,30 @@ export default function Clients() {
</div>
</div>
<div className="flex items-center gap-1 shrink-0">
<Button
variant="ghost"
size="icon"
onClick={() => setEditClient(client)}
className="h-8 w-8 text-slate-400 hover:text-blue-600 hover:bg-blue-50 dark:hover:text-blue-400 dark:hover:bg-blue-900/20"
>
<Pencil className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => setDeleteClient(client)}
className="h-8 w-8 text-slate-400 hover:text-red-600 hover:bg-red-50 dark:hover:text-red-400 dark:hover:bg-red-900/20"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
{(canEditClient || canDeleteClient) && (
<div className="flex items-center gap-1 shrink-0">
{canEditClient && (
<Button
variant="ghost"
size="icon"
onClick={() => setEditClient(client)}
className="h-8 w-8 text-slate-400 hover:text-blue-600 hover:bg-blue-50 dark:hover:text-blue-400 dark:hover:bg-blue-900/20"
>
<Pencil className="w-4 h-4" />
</Button>
)}
{canDeleteClient && (
<Button
variant="ghost"
size="icon"
onClick={() => setDeleteClient(client)}
className="h-8 w-8 text-slate-400 hover:text-red-600 hover:bg-red-50 dark:hover:text-red-400 dark:hover:bg-red-900/20"
>
<Trash2 className="w-4 h-4" />
</Button>
)}
</div>
)}
</li>
))}
</ul>
@@ -194,26 +212,32 @@ export default function Clients() {
/>
)}
<CreateClientModal
isOpen={isCreateModalOpen}
onClose={() => setIsCreateModalOpen(false)}
onSuccess={fetchClientsList}
workspaceId={activeWorkspace.id}
/>
<EditClientModal
isOpen={!!editClient}
onClose={() => setEditClient(null)}
onSuccess={fetchClientsList}
client={editClient}
/>
<DeleteClientModal
isOpen={!!deleteClient}
onClose={() => setDeleteClient(null)}
onSuccess={fetchClientsList}
client={deleteClient}
/>
{canCreateClient && (
<CreateClientModal
isOpen={isCreateModalOpen}
onClose={() => setIsCreateModalOpen(false)}
onSuccess={fetchClientsList}
workspaceId={activeWorkspace.id}
/>
)}
{canEditClient && (
<EditClientModal
isOpen={!!editClient}
onClose={() => setEditClient(null)}
onSuccess={fetchClientsList}
client={editClient}
/>
)}
{canDeleteClient && (
<DeleteClientModal
isOpen={!!deleteClient}
onClose={() => setDeleteClient(null)}
onSuccess={fetchClientsList}
client={deleteClient}
/>
)}
</div>
)
}

View File

@@ -15,7 +15,8 @@ import { fetchWorkspaceMemberships } from "../api/workspaces";
import { searchUserByExactMobile, type SearchedUser } from "../api/users";
import { useAppContext } from "../context/AppContext";
import { useWorkspace } from "../context/WorkspaceContext";
import { useTranslation } from "../hooks/useTranslation";
import { useTranslation } from "../hooks/useTranslation";
import { PROJECTS_CREATE, canWorkspace } from "../lib/permissions";
import { Button } from "../components/ui/button";
import { Input } from "../components/ui/input";
import { Select } from "../components/ui/Select";
@@ -57,7 +58,8 @@ export default function ProjectCreate() {
const { t } = useTranslation();
const { user } = useAppContext();
const { activeWorkspace } = useWorkspace();
const currentUserId = user?.id || "";
const currentUserId = user?.id || "";
const canCreateProject = canWorkspace(activeWorkspace?.my_role, PROJECTS_CREATE);
// Project Detail States
const [name, setName] = useState("");
@@ -89,7 +91,14 @@ export default function ProjectCreate() {
const [memberIdToDelete, setMemberIdToDelete] = useState<string | null>(null);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const hasUnsavedChanges = name.trim() !== "" || description.trim() !== "" || members.length > 1;
const hasUnsavedChanges = name.trim() !== "" || description.trim() !== "" || members.length > 1;
useEffect(() => {
if (activeWorkspace && !canCreateProject) {
toast.error("You do not have permission to create projects.");
navigate("/projects");
}
}, [activeWorkspace, canCreateProject, navigate]);
useBlocker(({ currentLocation, nextLocation }) => {
if (hasUnsavedChanges && !isSaving && currentLocation.pathname !== nextLocation.pathname) {
@@ -352,9 +361,9 @@ export default function ProjectCreate() {
...filteredWorkspaceMembers.map((m) => ({ listId: m.id || m.user.id, user: m.user }))
];
if (!activeWorkspace) {
return null;
}
if (!activeWorkspace) {
return null;
}
return (
<div className="absolute inset-0 flex flex-col p-4 sm:p-6 bg-slate-50 dark:bg-slate-900 overflow-hidden">

View File

@@ -15,7 +15,8 @@ import { fetchWorkspaceMemberships } from "../api/workspaces";
import { searchUserByExactMobile, type SearchedUser } from "../api/users";
import { useAppContext } from "../context/AppContext";
import { useWorkspace } from "../context/WorkspaceContext";
import { useTranslation } from "../hooks/useTranslation";
import { useTranslation } from "../hooks/useTranslation";
import { PROJECTS_EDIT, canWorkspace } from "../lib/permissions";
import { Button } from "../components/ui/button";
import { Input } from "../components/ui/input";
import { Select } from "../components/ui/Select";
@@ -58,7 +59,8 @@ export default function ProjectEdit() {
const { t } = useTranslation();
const { user } = useAppContext();
const { activeWorkspace } = useWorkspace();
const currentUserId = user?.id || "";
const currentUserId = user?.id || "";
const canEditProject = canWorkspace(activeWorkspace?.my_role, PROJECTS_EDIT);
const [name, setName] = useState("");
const [description, setDescription] = useState("");
@@ -87,7 +89,14 @@ export default function ProjectEdit() {
const [memberIdToDelete, setMemberIdToDelete] = useState<string | null>(null);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const hasUnsavedChanges = name.trim() !== "";
const hasUnsavedChanges = name.trim() !== "";
useEffect(() => {
if (activeWorkspace && !canEditProject) {
toast.error("You do not have permission to edit projects.");
navigate("/projects");
}
}, [activeWorkspace, canEditProject, navigate]);
useBlocker(({ currentLocation, nextLocation }) => {
if (hasUnsavedChanges && !isSaving && !isProjectLoading && currentLocation.pathname !== nextLocation.pathname) {

View File

@@ -11,12 +11,24 @@ import FilterBar from "../components/FilterBar";
import { Button } from "../components/ui/button";
import { Card, CardHeader, CardTitle, CardContent } from "../components/ui/card";
import { Modal } from "../components/Modal";
import { toast } from "sonner";
import { Input } from "../components/ui/input";
import { toast } from "sonner";
import { Input } from "../components/ui/input";
import {
PROJECTS_ARCHIVE,
PROJECTS_CREATE,
PROJECTS_DELETE,
PROJECTS_EDIT,
canWorkspace,
} from "../lib/permissions";
export const Projects: React.FC = () => {
const { t } = useTranslation();
const { activeWorkspace } = useWorkspace();
const { t } = useTranslation();
const { activeWorkspace } = useWorkspace();
const workspaceRole = activeWorkspace?.my_role;
const canCreateProject = canWorkspace(workspaceRole, PROJECTS_CREATE);
const canEditProject = canWorkspace(workspaceRole, PROJECTS_EDIT);
const canDeleteProject = canWorkspace(workspaceRole, PROJECTS_DELETE);
const canArchiveProject = canWorkspace(workspaceRole, PROJECTS_ARCHIVE);
const [projects, setProjects] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
@@ -117,21 +129,25 @@ export const Projects: React.FC = () => {
<p className="text-slate-500 dark:text-slate-400 mt-1">{t.projects?.description(activeWorkspace?.name || "-") || 'Manage your projects'}</p>
</div>
<div className="flex items-center gap-3 w-full sm:w-auto">
<Button
variant={isArchived ? "default" : "secondary"}
onClick={() => setIsArchived(!isArchived)}
className="gap-2 shadow-sm flex-1 sm:flex-none"
>
<Archive className="h-4 w-4" />
{isArchived ? (t.projects?.active || 'Active Projects') : (t.projects?.archived || 'Archived Projects')}
</Button>
<Button
onClick={() => setIsCreateModalOpen(true)}
className="gap-2 shadow-sm flex-1 sm:flex-none"
>
<Plus className="h-5 w-5" />
{t.projects?.createNew || 'Create New'}
</Button>
{canArchiveProject && (
<Button
variant={isArchived ? "default" : "secondary"}
onClick={() => setIsArchived(!isArchived)}
className="gap-2 shadow-sm flex-1 sm:flex-none"
>
<Archive className="h-4 w-4" />
{isArchived ? (t.projects?.active || 'Active Projects') : (t.projects?.archived || 'Archived Projects')}
</Button>
)}
{canCreateProject && (
<Button
onClick={() => setIsCreateModalOpen(true)}
className="gap-2 shadow-sm flex-1 sm:flex-none"
>
<Plus className="h-5 w-5" />
{t.projects?.createNew || 'Create New'}
</Button>
)}
</div>
</div>
@@ -172,27 +188,33 @@ export const Projects: React.FC = () => {
</div>
<div className="flex items-center gap-2 shrink-0">
<Button
variant="ghost"
size="icon"
onClick={() => setEditingProject(project)}
className="h-8 w-8 text-slate-400 hover:text-blue-600 hover:bg-blue-50 dark:hover:text-blue-400 dark:hover:bg-blue-900/20"
title={t.actions?.edit || 'Edit'}
>
<Pencil className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => setDeleteModal({ isOpen: true, project })}
className="h-8 w-8 text-slate-400 hover:text-red-600 hover:bg-red-50 dark:hover:text-red-400 dark:hover:bg-red-900/20"
title={t.actions?.delete || 'Delete'}
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
{(canEditProject || canDeleteProject) && (
<div className="flex items-center gap-2 shrink-0">
{canEditProject && (
<Button
variant="ghost"
size="icon"
onClick={() => setEditingProject(project)}
className="h-8 w-8 text-slate-400 hover:text-blue-600 hover:bg-blue-50 dark:hover:text-blue-400 dark:hover:bg-blue-900/20"
title={t.actions?.edit || 'Edit'}
>
<Pencil className="w-4 h-4" />
</Button>
)}
{canDeleteProject && (
<Button
variant="ghost"
size="icon"
onClick={() => setDeleteModal({ isOpen: true, project })}
className="h-8 w-8 text-slate-400 hover:text-red-600 hover:bg-red-50 dark:hover:text-red-400 dark:hover:bg-red-900/20"
title={t.actions?.delete || 'Delete'}
>
<Trash2 className="w-4 h-4" />
</Button>
)}
</div>
)}
</CardContent>
</Card>
))}
@@ -216,15 +238,15 @@ export const Projects: React.FC = () => {
)}
{/* Modals */}
{isCreateModalOpen && (
<ProjectCreateModal
isOpen={isCreateModalOpen}
onClose={() => setIsCreateModalOpen(false)}
/>
)}
{editingProject && (
<ProjectEditModal
{canCreateProject && isCreateModalOpen && (
<ProjectCreateModal
isOpen={isCreateModalOpen}
onClose={() => setIsCreateModalOpen(false)}
/>
)}
{canEditProject && editingProject && (
<ProjectEditModal
project={editingProject}
isOpen={!!editingProject}
onClose={() => setEditingProject(null)}

View File

@@ -5,6 +5,7 @@ import { toast } from "sonner";
import { createTag, deleteTag, getTags, type Tag, updateTag } from "../api/tags";
import { useWorkspace } from "../context/WorkspaceContext";
import { useTranslation } from "../hooks/useTranslation";
import { TAGS_CREATE, TAGS_DELETE, TAGS_EDIT, canWorkspace } from "../lib/permissions";
import FilterBar from "../components/FilterBar";
import { Modal } from "../components/Modal";
import { Pagination } from "../components/Pagination";
@@ -17,6 +18,10 @@ const DEFAULT_COLOR = "#3B82F6";
export default function Tags() {
const { t } = useTranslation();
const { activeWorkspace } = useWorkspace();
const workspaceRole = activeWorkspace?.my_role;
const canCreateTag = canWorkspace(workspaceRole, TAGS_CREATE);
const canEditTag = canWorkspace(workspaceRole, TAGS_EDIT);
const canDeleteTag = canWorkspace(workspaceRole, TAGS_DELETE);
const [tags, setTags] = useState<Tag[]>([]);
const [isLoading, setIsLoading] = useState(false);
@@ -145,10 +150,12 @@ export default function Tags() {
{t.tags?.description?.(activeWorkspace.name) || `Manage tags for ${activeWorkspace.name}`}
</p>
</div>
<Button onClick={openCreateModal} className="gap-2 shadow-sm">
<Plus className="h-4 w-4" />
{t.tags?.create || "Create Tag"}
</Button>
{canCreateTag && (
<Button onClick={openCreateModal} className="gap-2 shadow-sm">
<Plus className="h-4 w-4" />
{t.tags?.create || "Create Tag"}
</Button>
)}
</div>
<FilterBar
@@ -176,14 +183,20 @@ export default function Tags() {
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="ghost" size="icon" onClick={() => openEditModal(tag)} title={t.actions?.edit || "Edit"}>
<Edit2 className="w-4 h-4" />
</Button>
<Button variant="ghost" size="icon" onClick={() => void handleDelete(tag)} title={t.actions?.delete || "Delete"}>
<Trash2 className="w-4 h-4 text-red-500" />
</Button>
</div>
{(canEditTag || canDeleteTag) && (
<div className="flex items-center gap-2">
{canEditTag && (
<Button variant="ghost" size="icon" onClick={() => openEditModal(tag)} title={t.actions?.edit || "Edit"}>
<Edit2 className="w-4 h-4" />
</Button>
)}
{canDeleteTag && (
<Button variant="ghost" size="icon" onClick={() => void handleDelete(tag)} title={t.actions?.delete || "Delete"}>
<Trash2 className="w-4 h-4 text-red-500" />
</Button>
)}
</div>
)}
</CardContent>
</Card>
))}

View File

@@ -1,8 +1,9 @@
import { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { ArrowLeft, ArrowRight, Edit2, Trash2 } from 'lucide-react';
import { useTranslation } from '../hooks/useTranslation';
import { getWorkspace, deleteWorkspace, type Workspace } from '../api/workspaces';
import { ArrowLeft, ArrowRight, Edit2, Trash2 } from 'lucide-react';
import { useTranslation } from '../hooks/useTranslation';
import { getWorkspace, deleteWorkspace, type Workspace } from '../api/workspaces';
import { WORKSPACE_DELETE, WORKSPACE_EDIT, canWorkspace } from '../lib/permissions';
export default function WorkspaceDetail() {
const { id } = useParams<{ id: string }>();
@@ -44,7 +45,8 @@ export default function WorkspaceDetail() {
return <div className="p-8 text-center">{t.workspace?.loading}</div>;
}
const canEdit = workspace.my_role === 'owner' || workspace.my_role === 'admin';
const canEdit = canWorkspace(workspace.my_role, WORKSPACE_EDIT);
const canDelete = canWorkspace(workspace.my_role, WORKSPACE_DELETE);
return (
<div className="max-w-4xl mx-auto p-6">
@@ -75,8 +77,8 @@ export default function WorkspaceDetail() {
>
<Edit2 className="h-5 w-5" />
</button>
{workspace.my_role === 'owner' && (
<button
{canDelete && (
<button
onClick={handleDelete}
className="p-2 text-slate-500 hover:text-red-600 bg-slate-50 dark:bg-slate-800 rounded-lg"
>

View File

@@ -13,7 +13,15 @@ import {
getWorkspace
} from '../api/workspaces';
import { searchUserByExactMobile, type SearchedUser } from '../api/users';
import { useAppContext } from '../context/AppContext';
import { useAppContext } from '../context/AppContext';
import {
WORKSPACE_EDIT,
WORKSPACE_MEMBERS_ADD,
WORKSPACE_MEMBERS_CHANGE_ROLE,
canChangeWorkspaceMember,
canWorkspace,
type WorkspaceRole,
} from '../lib/permissions';
import { Button } from '../components/ui/button';
import { InfiniteScroll } from '../components/InfiniteScroll';
import { Select } from '../components/ui/Select';
@@ -45,7 +53,7 @@ export default function EditWorkspace() {
// Workspace Info States
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [myRole, setMyRole] = useState<'owner' | 'admin' | 'member' | 'guest'>('member');
const [myRole, setMyRole] = useState<WorkspaceRole>('member');
const [workspaceOwnerId, setWorkspaceOwnerId] = useState<string>('');
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
@@ -101,9 +109,16 @@ export default function EditWorkspace() {
return false;
});
useEffect(() => {
if (id) loadData();
}, [id]);
useEffect(() => {
if (id) loadData();
}, [id]);
useEffect(() => {
if (!isLoading && id && !canWorkspace(myRole, WORKSPACE_EDIT)) {
toast.error("You do not have permission to edit this workspace.");
navigate(`/workspaces/${id}`);
}
}, [id, isLoading, myRole, navigate]);
const loadData = async () => {
try {
@@ -258,10 +273,17 @@ export default function EditWorkspace() {
}
};
const canManageMembers = myRole === 'owner' || myRole === 'admin';
const isFirstOwner = currentUserId === workspaceOwnerId;
const canManageMembers = canWorkspace(myRole, WORKSPACE_MEMBERS_CHANGE_ROLE);
const isFirstOwner = currentUserId === workspaceOwnerId;
if (isLoading) return <div className="p-8 text-center">{t.workspace?.loading || "Loading..."}</div>;
const roleOptions = (allowOwner: boolean) => [
...(allowOwner ? [{ value: "owner", label: t.workspace?.roles?.owner || "Owner" }] : []),
{ value: "admin", label: t.workspace?.roles?.admin || "Admin" },
{ value: "member", label: t.workspace?.roles?.member || "Member" },
{ value: "guest", label: t.workspace?.roles?.guest || "Guest" },
];
if (isLoading) return <div className="p-8 text-center">{t.workspace?.loading || "Loading..."}</div>;
return (
<div className="absolute inset-0 flex flex-col p-4 sm:p-6 bg-slate-50 dark:bg-slate-900 overflow-hidden">
@@ -312,8 +334,8 @@ export default function EditWorkspace() {
{ t.workspace?.members || "Members" }
</h2>
{canManageMembers && (
<div className="space-y-3">
{canWorkspace(myRole, WORKSPACE_MEMBERS_ADD) && (
<div className="space-y-3">
<Input
type="text"
placeholder={t.workspace?.searchMemberPlaceholder || "Search user by exact mobile number..."}
@@ -357,11 +379,8 @@ export default function EditWorkspace() {
value={newMemberRole}
onChange={(val) => setNewMemberRole(val as any)}
options={[
...(isFirstOwner ? [{ value: "owner", label: t.workspace?.roles?.owner || "Owner" }] : []),
{ value: "admin", label: t.workspace?.roles?.admin || "Admin" },
{ value: "member", label: t.workspace?.roles?.member || "Member" },
{ value: "guest", label: t.workspace?.roles?.guest || "Guest" },
]}
...roleOptions(isFirstOwner),
]}
className="flex-1 sm:flex-none"
buttonClassName="w-full sm:w-[110px] px-3 py-1.5 text-sm"
/>
@@ -395,7 +414,13 @@ export default function EditWorkspace() {
>
{members.map((m) => {
const isThisMemberTheFirstOwner = m.user?.id === workspaceOwnerId;
const canChangeThisUserRole = canManageMembers && !isThisMemberTheFirstOwner && (isFirstOwner || m.role !== 'owner');
const canChangeThisUserRole = canChangeWorkspaceMember({
actorRole: myRole,
actorUserId: currentUserId,
targetRole: m.role,
targetUserId: m.user?.id,
ownerUserId: workspaceOwnerId,
});
return (
<div key={m.id} className="flex flex-col sm:flex-row sm:items-center justify-between p-3 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-lg gap-3 shadow-sm hover:border-blue-200 dark:hover:border-blue-800 transition-colors">
@@ -420,12 +445,7 @@ export default function EditWorkspace() {
<Select
value={m.role}
onChange={(val) => handleChangeRole(m.id, val)}
options={[
...(isFirstOwner ? [{ value: "owner", label: t.workspace?.roles?.owner || "Owner" }] : []),
{ value: "admin", label: t.workspace?.roles?.admin || "Admin" },
{ value: "member", label: t.workspace?.roles?.member || "Member" },
{ value: "guest", label: t.workspace?.roles?.guest || "Guest" },
]}
options={roleOptions(isFirstOwner)}
buttonClassName="w-[110px] px-3 py-1.5 text-sm"
/>
) : (
@@ -437,7 +457,7 @@ export default function EditWorkspace() {
</span>
)}
{canManageMembers && !isThisMemberTheFirstOwner && (isFirstOwner || m.role !== 'owner') && (
{canChangeThisUserRole && (
<Button
type="button"
variant="ghost"

View File

@@ -2,9 +2,14 @@ import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Plus, Trash2, Pencil, ChevronRight } from 'lucide-react';
import { toast } from 'sonner';
import { fetchWorkspaces, deleteWorkspace, type Workspace } from '../api/workspaces';
import { useAppContext } from '../context/AppContext';
import { useTranslation } from '../hooks/useTranslation';
import { fetchWorkspaces, deleteWorkspace, type Workspace } from '../api/workspaces';
import { useTranslation } from '../hooks/useTranslation';
import {
WORKSPACE_DELETE,
WORKSPACE_EDIT,
canWorkspace,
type WorkspaceRole,
} from '../lib/permissions';
import FilterBar from '../components/FilterBar';
import { Button } from '../components/ui/button';
import { Input } from '../components/ui/input';
@@ -12,9 +17,7 @@ import { Card, CardContent, CardTitle } from '../components/ui/card';
import { Pagination } from '../components/Pagination';
import { Modal } from '../components/Modal';
type WorkspaceRole = "owner" | "admin" | "member" | "guest";
const RoleBadge = ({ role }: { role?: WorkspaceRole }) => {
const RoleBadge = ({ role }: { role?: WorkspaceRole }) => {
const { t } = useTranslation();
if (!role) return null;
@@ -45,9 +48,8 @@ export default function Workspaces() {
const [deleteModal, setDeleteModal] = useState<{isOpen: boolean; workspace: Workspace | null}>({isOpen: false, workspace: null});
const [deleteInput, setDeleteInput] = useState('');
const navigate = useNavigate();
const { user } = useAppContext();
const { t } = useTranslation();
const navigate = useNavigate();
const { t } = useTranslation();
const orderingOptions = [
{ value: '-created_at', label: t.ordering?.createdAtDesc || 'Newest First' },
@@ -120,13 +122,13 @@ export default function Workspaces() {
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">{t.workspace?.title || 'Workspaces'}</h1>
<p className="text-slate-500 dark:text-slate-400 mt-1">{t.workspace?.subtitle || 'Manage your workspaces'}</p>
</div>
<Button
onClick={() => navigate('/workspaces/create')}
className="gap-2 shadow-sm"
>
<Plus className="h-5 w-5" />
{t.workspace?.createNew || 'Create New'}
</Button>
<Button
onClick={() => navigate('/workspaces/create')}
className="gap-2 shadow-sm"
>
<Plus className="h-5 w-5" />
{t.workspace?.createNew || 'Create New'}
</Button>
</div>
<FilterBar
@@ -145,11 +147,11 @@ export default function Workspaces() {
) : (
<div className="flex flex-col flex-1">
<div className="flex flex-col gap-4 mb-6">
{workspaces.map((workspace) => {
const isOwner = workspace.owner === user?.id || workspace.my_role === 'owner';
const isAdmin = workspace.my_role === 'admin' || isOwner;
return (
{workspaces.map((workspace) => {
const canDeleteWorkspace = canWorkspace(workspace.my_role, WORKSPACE_DELETE);
const canEditWorkspace = canWorkspace(workspace.my_role, WORKSPACE_EDIT);
return (
<Card key={workspace.id} className="flex flex-col text-slate-800 dark:text-slate-100 dark:bg-slate-800 dark:border-slate-700 shadow-sm">
<CardContent className="flex flex-col sm:flex-row items-start sm:items-center justify-between py-4 px-6 gap-4">
<div className="flex-1">
@@ -165,8 +167,8 @@ export default function Workspaces() {
</div>
<div className="flex items-center gap-2 shrink-0">
{isOwner && (
<Button
{canDeleteWorkspace && (
<Button
variant="ghost"
size="icon"
onClick={() => setDeleteModal({ isOpen: true, workspace })}
@@ -177,8 +179,8 @@ export default function Workspaces() {
</Button>
)}
{isAdmin && (
<Button
{canEditWorkspace && (
<Button
variant="ghost"
size="icon"
onClick={() => navigate(`/workspaces/${workspace.id}/edit`)}