feat(permissions): gate workspace resources by role
This commit is contained in:
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user