feat: allow editing individual permissions
This commit is contained in:
@@ -6,16 +6,20 @@ const Toggle = ({
|
||||
onChange,
|
||||
label,
|
||||
disabled,
|
||||
showLabel = true,
|
||||
}: {
|
||||
isChecked: boolean;
|
||||
onChange: () => void;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
showLabel?: boolean;
|
||||
}) => (
|
||||
<div className="mr-4 flex items-center justify-end">
|
||||
<span className="mr-2 text-xs text-light-900 dark:text-dark-900">
|
||||
{label}
|
||||
</span>
|
||||
{showLabel && (
|
||||
<span className="mr-2 text-xs text-light-900 dark:text-dark-900">
|
||||
{label}
|
||||
</span>
|
||||
)}
|
||||
<Switch
|
||||
checked={isChecked}
|
||||
onChange={onChange}
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { HiXMark } from "react-icons/hi2";
|
||||
|
||||
import type { Permission } from "@kan/shared";
|
||||
import { permissionCategories } from "@kan/shared";
|
||||
|
||||
import Toggle from "~/components/Toggle";
|
||||
import { useModal } from "~/providers/modal";
|
||||
import { usePopup } from "~/providers/popup";
|
||||
import { useWorkspace } from "~/providers/workspace";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
export function EditMemberPermissionsModal() {
|
||||
const { workspace } = useWorkspace();
|
||||
const { modalContentType, entityId, entityLabel, closeModal } = useModal();
|
||||
const { showPopup } = usePopup();
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { data, isLoading } = api.permission.getMemberPermissions.useQuery(
|
||||
{
|
||||
workspacePublicId: workspace.publicId,
|
||||
memberPublicId: entityId,
|
||||
},
|
||||
{
|
||||
enabled:
|
||||
modalContentType === "EDIT_MEMBER_PERMISSIONS" && !!entityId,
|
||||
},
|
||||
);
|
||||
|
||||
const grantMutation = api.permission.grantPermission.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 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<Permission, string> = {
|
||||
"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 (
|
||||
<div className="w-full rounded-md bg-light-50 p-6 text-light-1000 dark:bg-dark-100 dark:text-dark-1000">
|
||||
<div className="mb-3 flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="mb-1 text-sm font-semibold">
|
||||
{t`Edit permissions`}
|
||||
</h2>
|
||||
<p className="min-h-[16px] text-xs text-light-900 dark:text-dark-900">
|
||||
{entityLabel || "\u00A0"}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeModal}
|
||||
className="ml-2 inline-flex h-6 w-6 items-center justify-center rounded-md text-light-900 hover:bg-light-200 focus:outline-none dark:text-dark-900 dark:hover:bg-dark-200"
|
||||
aria-label={t`Close`}
|
||||
>
|
||||
<HiXMark className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<p className="text-xs text-light-900 dark:text-dark-900">
|
||||
{t`Loading permissions...`}
|
||||
</p>
|
||||
) : (
|
||||
<div className="max-h-80 space-y-3 overflow-y-auto pr-1">
|
||||
{Object.values(permissionCategories).map((category, index) => (
|
||||
<div
|
||||
key={category.label}
|
||||
className={`py-2 ${index > 0 ? "border-t border-light-300 dark:border-dark-300" : ""}`}
|
||||
>
|
||||
<div className="my-2 text-[12px] font-semibold text-light-900 dark:text-dark-950">
|
||||
{category.label}
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
{category.permissions.map((permission) => {
|
||||
const label =
|
||||
permissionLabels[permission] ?? (permission as string);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={permission}
|
||||
className="flex items-center justify-between gap-3 py-0.5"
|
||||
>
|
||||
<span className="text-xs text-light-900 dark:text-dark-900 mb-">
|
||||
{label}
|
||||
</span>
|
||||
<Toggle
|
||||
label={label}
|
||||
showLabel={false}
|
||||
isChecked={effectivePermissions.includes(permission)}
|
||||
disabled={isBusy}
|
||||
onChange={() =>
|
||||
handleToggle(
|
||||
permission,
|
||||
!effectivePermissions.includes(permission),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 && (
|
||||
<Dropdown
|
||||
items={[
|
||||
{
|
||||
label: t`Edit permissions`,
|
||||
action: () =>
|
||||
openModal(
|
||||
"EDIT_MEMBER_PERMISSIONS",
|
||||
memberPublicId,
|
||||
memberEmail ?? "",
|
||||
),
|
||||
},
|
||||
{
|
||||
label: t`Remove member`,
|
||||
action: () =>
|
||||
@@ -231,7 +241,7 @@ export default function MembersPage() {
|
||||
>
|
||||
<HiEllipsisHorizontal
|
||||
size={20}
|
||||
className="text-light-900 dark:text-dark-900 sm:size-[25px]"
|
||||
className="text-light-900 dark:text-dark-900 sm:size-[20px]"
|
||||
/>
|
||||
</Dropdown>
|
||||
)}
|
||||
@@ -386,6 +396,13 @@ export default function MembersPage() {
|
||||
>
|
||||
<DeleteMemberConfirmation />
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
modalSize="sm"
|
||||
isVisible={isOpen && modalContentType === "EDIT_MEMBER_PERMISSIONS"}
|
||||
>
|
||||
<EditMemberPermissionsModal />
|
||||
</Modal>
|
||||
</>
|
||||
</div>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user