feat: allow editing individual permissions
This commit is contained in:
@@ -6,16 +6,20 @@ const Toggle = ({
|
|||||||
onChange,
|
onChange,
|
||||||
label,
|
label,
|
||||||
disabled,
|
disabled,
|
||||||
|
showLabel = true,
|
||||||
}: {
|
}: {
|
||||||
isChecked: boolean;
|
isChecked: boolean;
|
||||||
onChange: () => void;
|
onChange: () => void;
|
||||||
label: string;
|
label: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
showLabel?: boolean;
|
||||||
}) => (
|
}) => (
|
||||||
<div className="mr-4 flex items-center justify-end">
|
<div className="mr-4 flex items-center justify-end">
|
||||||
<span className="mr-2 text-xs text-light-900 dark:text-dark-900">
|
{showLabel && (
|
||||||
{label}
|
<span className="mr-2 text-xs text-light-900 dark:text-dark-900">
|
||||||
</span>
|
{label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<Switch
|
<Switch
|
||||||
checked={isChecked}
|
checked={isChecked}
|
||||||
onChange={onChange}
|
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 { getAvatarUrl } from "~/utils/helpers";
|
||||||
import { DeleteMemberConfirmation } from "./components/DeleteMemberConfirmation";
|
import { DeleteMemberConfirmation } from "./components/DeleteMemberConfirmation";
|
||||||
import { InviteMemberForm } from "./components/InviteMemberForm";
|
import { InviteMemberForm } from "./components/InviteMemberForm";
|
||||||
|
import { EditMemberPermissionsModal } from "./components/EditMemberPermissionsModal";
|
||||||
|
|
||||||
export default function MembersPage() {
|
export default function MembersPage() {
|
||||||
const { modalContentType, openModal, isOpen } = useModal();
|
const { modalContentType, openModal, isOpen } = useModal();
|
||||||
@@ -218,6 +219,15 @@ export default function MembersPage() {
|
|||||||
{session?.user.id !== memberId && (
|
{session?.user.id !== memberId && (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
items={[
|
items={[
|
||||||
|
{
|
||||||
|
label: t`Edit permissions`,
|
||||||
|
action: () =>
|
||||||
|
openModal(
|
||||||
|
"EDIT_MEMBER_PERMISSIONS",
|
||||||
|
memberPublicId,
|
||||||
|
memberEmail ?? "",
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: t`Remove member`,
|
label: t`Remove member`,
|
||||||
action: () =>
|
action: () =>
|
||||||
@@ -231,7 +241,7 @@ export default function MembersPage() {
|
|||||||
>
|
>
|
||||||
<HiEllipsisHorizontal
|
<HiEllipsisHorizontal
|
||||||
size={20}
|
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>
|
</Dropdown>
|
||||||
)}
|
)}
|
||||||
@@ -386,6 +396,13 @@ export default function MembersPage() {
|
|||||||
>
|
>
|
||||||
<DeleteMemberConfirmation />
|
<DeleteMemberConfirmation />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
modalSize="sm"
|
||||||
|
isVisible={isOpen && modalContentType === "EDIT_MEMBER_PERMISSIONS"}
|
||||||
|
>
|
||||||
|
<EditMemberPermissionsModal />
|
||||||
|
</Modal>
|
||||||
</>
|
</>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user