From 3369092509bdc793557cb81e38097be59a3db40a Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 28 Jan 2026 22:42:04 +0000 Subject: [PATCH] feat: allow editing individual permissions --- apps/web/src/components/Toggle.tsx | 10 +- .../components/EditMemberPermissionsModal.tsx | 202 ++++++++++++++++++ apps/web/src/views/members/index.tsx | 19 +- 3 files changed, 227 insertions(+), 4 deletions(-) create mode 100644 apps/web/src/views/members/components/EditMemberPermissionsModal.tsx diff --git a/apps/web/src/components/Toggle.tsx b/apps/web/src/components/Toggle.tsx index 0c0e657c..3efe3b5b 100644 --- a/apps/web/src/components/Toggle.tsx +++ b/apps/web/src/components/Toggle.tsx @@ -6,16 +6,20 @@ const Toggle = ({ onChange, label, disabled, + showLabel = true, }: { isChecked: boolean; onChange: () => void; label: string; disabled?: boolean; + showLabel?: boolean; }) => (
- - {label} - + {showLabel && ( + + {label} + + )} { + showPopup({ + header: t`Permissions updated`, + message: t`The member's permissions have been updated.`, + icon: "success", + }); + }, + onError: () => { + showPopup({ + header: t`Unable to update permissions`, + message: t`Please try again later, or contact customer support.`, + icon: "error", + }); + }, + onSettled: async () => { + await utils.permission.getMemberPermissions.invalidate({ + workspacePublicId: workspace.publicId, + memberPublicId: entityId, + }); + }, + }); + + const revokeMutation = api.permission.revokePermission.useMutation({ + onSuccess: () => { + showPopup({ + header: t`Permissions updated`, + message: t`The member's permissions have been updated.`, + icon: "success", + }); + }, + onError: () => { + showPopup({ + header: t`Unable to update permissions`, + message: t`Please try again later, or contact customer support.`, + icon: "error", + }); + }, + onSettled: async () => { + await utils.permission.getMemberPermissions.invalidate({ + workspacePublicId: workspace.publicId, + memberPublicId: entityId, + }); + }, + }); + + const effectivePermissions = (data?.permissions ?? []) as Permission[]; + const isBusy = grantMutation.isPending || revokeMutation.isPending; + + const handleToggle = (permission: Permission, nextState: boolean) => { + if (!workspace.publicId || !entityId) return; + + if (nextState) { + grantMutation.mutate({ + workspacePublicId: workspace.publicId, + memberPublicId: entityId, + permission, + }); + } else { + revokeMutation.mutate({ + workspacePublicId: workspace.publicId, + memberPublicId: entityId, + permission, + }); + } + }; + + const permissionLabels: Record = { + "workspace:view": t`Can view workspace`, + "workspace:edit": t`Can edit workspace`, + "workspace:delete": t`Can delete workspace`, + "workspace:manage": t`Can manage workspace settings and billing`, + + "board:view": t`Can view boards`, + "board:create": t`Can create boards`, + "board:edit": t`Can edit boards`, + "board:delete": t`Can delete boards`, + + "list:view": t`Can view lists`, + "list:create": t`Can create lists`, + "list:edit": t`Can edit lists`, + "list:delete": t`Can delete lists`, + + "card:view": t`Can view cards`, + "card:create": t`Can create cards`, + "card:edit": t`Can edit cards`, + "card:delete": t`Can delete cards`, + + "comment:view": t`Can view comments`, + "comment:create": t`Can add comments`, + "comment:edit": t`Can edit comments`, + "comment:delete": t`Can delete comments`, + + "member:view": t`Can view members`, + "member:invite": t`Can invite members`, + "member:edit": t`Can edit member roles and permissions`, + "member:remove": t`Can remove members`, + }; + + return ( +
+
+
+

+ {t`Edit permissions`} +

+

+ {entityLabel || "\u00A0"} +

+
+ +
+ + {isLoading ? ( +

+ {t`Loading permissions...`} +

+ ) : ( +
+ {Object.values(permissionCategories).map((category, index) => ( +
0 ? "border-t border-light-300 dark:border-dark-300" : ""}`} + > +
+ {category.label} +
+
+ {category.permissions.map((permission) => { + const label = + permissionLabels[permission] ?? (permission as string); + + return ( +
+ + {label} + + + handleToggle( + permission, + !effectivePermissions.includes(permission), + ) + } + /> +
+ ); + })} +
+
+ ))} +
+ )} + +
+ ); +} + + diff --git a/apps/web/src/views/members/index.tsx b/apps/web/src/views/members/index.tsx index e40d5578..7afb0f6c 100644 --- a/apps/web/src/views/members/index.tsx +++ b/apps/web/src/views/members/index.tsx @@ -28,6 +28,7 @@ import { api } from "~/utils/api"; import { getAvatarUrl } from "~/utils/helpers"; import { DeleteMemberConfirmation } from "./components/DeleteMemberConfirmation"; import { InviteMemberForm } from "./components/InviteMemberForm"; +import { EditMemberPermissionsModal } from "./components/EditMemberPermissionsModal"; export default function MembersPage() { const { modalContentType, openModal, isOpen } = useModal(); @@ -218,6 +219,15 @@ export default function MembersPage() { {session?.user.id !== memberId && ( + openModal( + "EDIT_MEMBER_PERMISSIONS", + memberPublicId, + memberEmail ?? "", + ), + }, { label: t`Remove member`, action: () => @@ -231,7 +241,7 @@ export default function MembersPage() { > )} @@ -386,6 +396,13 @@ export default function MembersPage() { > + + + +