diff --git a/apps/client/src/ee/page-permission/components/general-access-select.tsx b/apps/client/src/ee/page-permission/components/general-access-select.tsx new file mode 100644 index 00000000..de2f78af --- /dev/null +++ b/apps/client/src/ee/page-permission/components/general-access-select.tsx @@ -0,0 +1,112 @@ +import { Group, Menu, Text, UnstyledButton } from "@mantine/core"; +import { + IconChevronDown, + IconLock, + IconWorld, + IconCheck, +} from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import classes from "./page-permission.module.css"; + +type AccessLevel = "open" | "restricted"; + +type GeneralAccessSelectProps = { + value: AccessLevel; + onChange: (value: AccessLevel) => void; + disabled?: boolean; + isInherited?: boolean; +}; + +export function GeneralAccessSelect({ + value, + onChange, + disabled, + isInherited, +}: GeneralAccessSelectProps) { + const { t } = useTranslation(); + + const isRestricted = value === "restricted"; + + const accessOptions = [ + { + value: "open" as const, + label: t("Open"), + description: t("Everyone in this space can access"), + icon: IconWorld, + }, + { + value: "restricted" as const, + label: t("Restricted"), + description: t("Only specific people can view or edit"), + icon: IconLock, + }, + ]; + + const currentOption = accessOptions.find((opt) => opt.value === value); + const Icon = currentOption?.icon || IconWorld; + + if (isInherited) { + return ( + +
+ +
+
+ + {currentOption?.label} + + + {currentOption?.description} + +
+
+ ); + } + + return ( + + + +
+ +
+
+ + + {currentOption?.label} + + {!disabled && } + + + {currentOption?.description} + +
+
+
+ + + {accessOptions.map((option) => ( + onChange(option.value)} + leftSection={} + rightSection={ + option.value === value ? : null + } + > +
+ {option.label} + + {option.description} + +
+
+ ))} +
+
+ ); +} diff --git a/apps/client/src/ee/page-permission/components/page-permission-item.tsx b/apps/client/src/ee/page-permission/components/page-permission-item.tsx new file mode 100644 index 00000000..01aa7ade --- /dev/null +++ b/apps/client/src/ee/page-permission/components/page-permission-item.tsx @@ -0,0 +1,107 @@ +import { Group, Menu, Text, UnstyledButton } from "@mantine/core"; +import { IconChevronDown, IconCheck } from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import { useAtomValue } from "jotai"; +import { CustomAvatar } from "@/components/ui/custom-avatar"; +import { IconGroupCircle } from "@/components/icons/icon-people-circle"; +import { userAtom } from "@/features/user/atoms/current-user-atom"; +import { formatMemberCount } from "@/lib"; +import { + IPagePermissionMember, + PagePermissionRole, +} from "@/ee/page-permission/types/page-permission.types"; +import { + pagePermissionRoleData, + getPagePermissionRoleLabel, +} from "@/ee/page-permission/types/page-permission-role-data"; +import classes from "./page-permission.module.css"; + +type PagePermissionItemProps = { + member: IPagePermissionMember; + onRoleChange: (memberId: string, type: "user" | "group", role: string) => void; + onRemove: (memberId: string, type: "user" | "group") => void; + disabled?: boolean; +}; + +export function PagePermissionItem({ + member, + onRoleChange, + onRemove, + disabled, +}: PagePermissionItemProps) { + const { t } = useTranslation(); + const currentUser = useAtomValue(userAtom); + const isCurrentUser = member.type === "user" && member.id === currentUser?.id; + const roleLabel = getPagePermissionRoleLabel(member.role); + + return ( +
+
+ {member.type === "user" && ( + + )} + {member.type === "group" && } + +
+ + + {member.name} + + {isCurrentUser && ( + + ({t("You")}) + + )} + + + {member.type === "user" && member.email} + {member.type === "group" && formatMemberCount(member.memberCount, t)} + +
+
+ + {isCurrentUser || disabled ? ( + + {t(roleLabel)} + + ) : ( + + + + + {t(roleLabel)} + + + + + + + {pagePermissionRoleData.map((role) => ( + onRoleChange(member.id, member.type, role.value)} + rightSection={ + role.value === member.role ? : null + } + > +
+ {t(role.label)} + + {t(role.description)} + +
+
+ ))} + + onRemove(member.id, member.type)} + > + {t("Remove access")} + +
+
+ )} +
+ ); +} diff --git a/apps/client/src/ee/page-permission/components/page-permission-list.tsx b/apps/client/src/ee/page-permission/components/page-permission-list.tsx new file mode 100644 index 00000000..64788756 --- /dev/null +++ b/apps/client/src/ee/page-permission/components/page-permission-list.tsx @@ -0,0 +1,179 @@ +import { Avatar, Group, ScrollArea, Text } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { useAtomValue } from "jotai"; +import { modals } from "@mantine/modals"; +import { userAtom } from "@/features/user/atoms/current-user-atom"; +import { CustomAvatar } from "@/components/ui/custom-avatar"; +import { IconGroupCircle } from "@/components/icons/icon-people-circle"; +import { + IPagePermissionMember, + PagePermissionRole, +} from "@/ee/page-permission/types/page-permission.types"; +import { + useRemovePagePermissionMutation, + useUpdatePagePermissionRoleMutation, +} from "@/ee/page-permission/queries/page-permission-query"; +import { PagePermissionItem } from "./page-permission-item"; +import classes from "./page-permission.module.css"; + +type PagePermissionListProps = { + pageId: string; + members: IPagePermissionMember[]; + canManage: boolean; + onRemoveAll?: () => void; +}; + +export function PagePermissionList({ + pageId, + members, + canManage, + onRemoveAll, +}: PagePermissionListProps) { + const { t } = useTranslation(); + const currentUser = useAtomValue(userAtom); + const updateRoleMutation = useUpdatePagePermissionRoleMutation(); + const removeMutation = useRemovePagePermissionMutation(); + + const handleRoleChange = async ( + memberId: string, + type: "user" | "group", + newRole: string, + ) => { + await updateRoleMutation.mutateAsync({ + pageId, + role: newRole as PagePermissionRole, + ...(type === "user" ? { userId: memberId } : { groupId: memberId }), + }); + }; + + const handleRemove = (memberId: string, type: "user" | "group") => { + modals.openConfirmModal({ + title: t("Remove access"), + children: ( + + {t("Are you sure you want to remove this member's access to the page?")} + + ), + centered: true, + labels: { confirm: t("Remove"), cancel: t("Cancel") }, + confirmProps: { color: "red" }, + onConfirm: async () => { + await removeMutation.mutateAsync({ + pageId, + ...(type === "user" ? { userIds: [memberId] } : { groupIds: [memberId] }), + }); + }, + }); + }; + + const handleRemoveAll = () => { + modals.openConfirmModal({ + title: t("Remove all access"), + children: ( + + {t("Are you sure you want to remove all specific access? This will make the page open to everyone in the space.")} + + ), + centered: true, + labels: { confirm: t("Remove all"), cancel: t("Cancel") }, + confirmProps: { color: "red" }, + onConfirm: () => onRemoveAll?.(), + }); + }; + + const sortedMembers = [...members].sort((a, b) => { + if (a.type === "user" && a.id === currentUser?.id) return -1; + if (b.type === "user" && b.id === currentUser?.id) return 1; + if (a.type === "group" && b.type === "user") return -1; + if (a.type === "user" && b.type === "group") return 1; + return 0; + }); + + const getSummaryText = () => { + const names: string[] = []; + let remaining = 0; + + for (const member of sortedMembers) { + if (names.length < 2) { + if (member.type === "user" && member.id === currentUser?.id) { + names.push(t("You")); + } else { + names.push(member.name); + } + } else { + remaining++; + } + } + + if (remaining > 0) { + return `${names.join(", ")}, ${t("and {{count}} other", { count: remaining })}`; + } + return names.join(", "); + }; + + if (members.length === 0) { + return null; + } + + return ( + <> +
+ + {t("Specific access")} + + {canManage && members.length > 0 && ( + <> + + • + + + {t("Remove all")} + + + )} +
+ + +
+ {sortedMembers.slice(0, 3).map((member, index) => ( +
+ {member.type === "user" ? ( + + ) : ( + + + + )} +
+ ))} +
+ + {getSummaryText()} + +
+ + + {sortedMembers.map((member) => ( + + ))} + + + ); +} diff --git a/apps/client/src/ee/page-permission/components/page-permission-tab.tsx b/apps/client/src/ee/page-permission/components/page-permission-tab.tsx new file mode 100644 index 00000000..50c24cbe --- /dev/null +++ b/apps/client/src/ee/page-permission/components/page-permission-tab.tsx @@ -0,0 +1,166 @@ +import { useState } from "react"; +import { Button, Divider, Group, Loader, Select, Stack, Text } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { Link, useParams } from "react-router-dom"; +import { IconArrowRight } from "@tabler/icons-react"; +import { MultiMemberSelect } from "@/features/space/components/multi-member-select"; +import { + IPageRestrictionInfo, + PagePermissionRole, +} from "@/ee/page-permission/types/page-permission.types"; +import { + useAddPagePermissionMutation, + usePagePermissionsQuery, + useRestrictPageMutation, + useUnrestrictPageMutation, +} from "@/ee/page-permission/queries/page-permission-query"; +import { pagePermissionRoleData } from "@/ee/page-permission/types/page-permission-role-data"; +import { GeneralAccessSelect } from "./general-access-select"; +import { PagePermissionList } from "./page-permission-list"; +import classes from "./page-permission.module.css"; +import { buildPageUrl } from "@/features/page/page.utils"; + +type PagePermissionTabProps = { + pageId: string; + restrictionInfo: IPageRestrictionInfo; +}; + +export function PagePermissionTab({ + pageId, + restrictionInfo, +}: PagePermissionTabProps) { + const { t } = useTranslation(); + const { spaceSlug } = useParams(); + const [memberIds, setMemberIds] = useState([]); + const [role, setRole] = useState(PagePermissionRole.WRITER); + + const { data: permissionsData, isLoading } = usePagePermissionsQuery(pageId); + const restrictMutation = useRestrictPageMutation(); + const unrestrictMutation = useUnrestrictPageMutation(); + const addPermissionMutation = useAddPagePermissionMutation(); + + const isRestricted = + restrictionInfo.hasDirectRestriction || + restrictionInfo.hasInheritedRestriction; + const isInherited = + restrictionInfo.hasInheritedRestriction && + !restrictionInfo.hasDirectRestriction; + const canManage = restrictionInfo.userAccess.canManage; + + const handleAccessChange = async (value: "open" | "restricted") => { + if (value === "restricted" && !isRestricted) { + await restrictMutation.mutateAsync(pageId); + } else if (value === "open" && isRestricted) { + await unrestrictMutation.mutateAsync(pageId); + } + }; + + const handleAddMembers = async () => { + if (memberIds.length === 0) return; + + const userIds = memberIds + .filter((id) => id.startsWith("user-")) + .map((id) => id.replace("user-", "")); + + const groupIds = memberIds + .filter((id) => id.startsWith("group-")) + .map((id) => id.replace("group-", "")); + + await addPermissionMutation.mutateAsync({ + pageId, + role: role as PagePermissionRole, + ...(userIds.length > 0 && { userIds }), + ...(groupIds.length > 0 && { groupIds }), + }); + + setMemberIds([]); + }; + + const handleRemoveAll = async () => { + await unrestrictMutation.mutateAsync(pageId); + }; + + return ( + + {isRestricted && canManage && !isInherited && ( + <> + +
+ +
+