mirror of
https://github.com/docmost/docmost.git
synced 2026-05-25 20:12:45 +08:00
UI - WIP
This commit is contained in:
@@ -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 (
|
||||||
|
<Group className={classes.generalAccessBox}>
|
||||||
|
<div
|
||||||
|
className={`${classes.generalAccessIcon} ${isRestricted ? classes.generalAccessIconRestricted : ""}`}
|
||||||
|
>
|
||||||
|
<Icon size={18} stroke={1.5} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
{currentOption?.label}
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{currentOption?.description}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu withArrow disabled={disabled}>
|
||||||
|
<Menu.Target>
|
||||||
|
<UnstyledButton className={classes.generalAccessBox}>
|
||||||
|
<div
|
||||||
|
className={`${classes.generalAccessIcon} ${isRestricted ? classes.generalAccessIconRestricted : ""}`}
|
||||||
|
>
|
||||||
|
<Icon size={18} stroke={1.5} />
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<Group gap={4}>
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
{currentOption?.label}
|
||||||
|
</Text>
|
||||||
|
{!disabled && <IconChevronDown size={14} />}
|
||||||
|
</Group>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{currentOption?.description}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</UnstyledButton>
|
||||||
|
</Menu.Target>
|
||||||
|
|
||||||
|
<Menu.Dropdown>
|
||||||
|
{accessOptions.map((option) => (
|
||||||
|
<Menu.Item
|
||||||
|
key={option.value}
|
||||||
|
onClick={() => onChange(option.value)}
|
||||||
|
leftSection={<option.icon size={16} stroke={1.5} />}
|
||||||
|
rightSection={
|
||||||
|
option.value === value ? <IconCheck size={16} /> : null
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Text size="sm">{option.label}</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{option.description}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div className={classes.permissionItem}>
|
||||||
|
<div className={classes.permissionItemInfo}>
|
||||||
|
{member.type === "user" && (
|
||||||
|
<CustomAvatar avatarUrl={member.avatarUrl} name={member.name} />
|
||||||
|
)}
|
||||||
|
{member.type === "group" && <IconGroupCircle />}
|
||||||
|
|
||||||
|
<div className={classes.permissionItemDetails}>
|
||||||
|
<Group gap={4}>
|
||||||
|
<Text fz="sm" fw={500} lineClamp={1}>
|
||||||
|
{member.name}
|
||||||
|
</Text>
|
||||||
|
{isCurrentUser && (
|
||||||
|
<Text fz="sm" c="dimmed">
|
||||||
|
({t("You")})
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
<Text fz="xs" c="dimmed" lineClamp={1}>
|
||||||
|
{member.type === "user" && member.email}
|
||||||
|
{member.type === "group" && formatMemberCount(member.memberCount, t)}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isCurrentUser || disabled ? (
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{t(roleLabel)}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Menu withArrow position="bottom-end">
|
||||||
|
<Menu.Target>
|
||||||
|
<UnstyledButton>
|
||||||
|
<Group gap={4}>
|
||||||
|
<Text size="sm">{t(roleLabel)}</Text>
|
||||||
|
<IconChevronDown size={14} />
|
||||||
|
</Group>
|
||||||
|
</UnstyledButton>
|
||||||
|
</Menu.Target>
|
||||||
|
|
||||||
|
<Menu.Dropdown>
|
||||||
|
{pagePermissionRoleData.map((role) => (
|
||||||
|
<Menu.Item
|
||||||
|
key={role.value}
|
||||||
|
onClick={() => onRoleChange(member.id, member.type, role.value)}
|
||||||
|
rightSection={
|
||||||
|
role.value === member.role ? <IconCheck size={16} /> : null
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Text size="sm">{t(role.label)}</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{t(role.description)}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
<Menu.Divider />
|
||||||
|
<Menu.Item
|
||||||
|
color="red"
|
||||||
|
onClick={() => onRemove(member.id, member.type)}
|
||||||
|
>
|
||||||
|
{t("Remove access")}
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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: (
|
||||||
|
<Text size="sm">
|
||||||
|
{t("Are you sure you want to remove this member's access to the page?")}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
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: (
|
||||||
|
<Text size="sm">
|
||||||
|
{t("Are you sure you want to remove all specific access? This will make the page open to everyone in the space.")}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<div className={classes.specificAccessHeader}>
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
{t("Specific access")}
|
||||||
|
</Text>
|
||||||
|
{canManage && members.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
•
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
className={classes.removeAllLink}
|
||||||
|
onClick={handleRemoveAll}
|
||||||
|
>
|
||||||
|
{t("Remove all")}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Group gap={0} mb="xs">
|
||||||
|
<div className={classes.avatarStack}>
|
||||||
|
{sortedMembers.slice(0, 3).map((member, index) => (
|
||||||
|
<div
|
||||||
|
key={member.id}
|
||||||
|
className={classes.avatarStackItem}
|
||||||
|
style={{ zIndex: sortedMembers.length - index }}
|
||||||
|
>
|
||||||
|
{member.type === "user" ? (
|
||||||
|
<CustomAvatar
|
||||||
|
avatarUrl={member.avatarUrl}
|
||||||
|
name={member.name}
|
||||||
|
size={28}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Avatar size={28} radius="xl">
|
||||||
|
<IconGroupCircle />
|
||||||
|
</Avatar>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Text size="sm" ml="xs">
|
||||||
|
{getSummaryText()}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<ScrollArea.Autosize mah={250}>
|
||||||
|
{sortedMembers.map((member) => (
|
||||||
|
<PagePermissionItem
|
||||||
|
key={`${member.type}-${member.id}`}
|
||||||
|
member={member}
|
||||||
|
onRoleChange={handleRoleChange}
|
||||||
|
onRemove={handleRemove}
|
||||||
|
disabled={!canManage}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ScrollArea.Autosize>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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<string[]>([]);
|
||||||
|
const [role, setRole] = useState<string>(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 (
|
||||||
|
<Stack gap="sm">
|
||||||
|
{isRestricted && canManage && !isInherited && (
|
||||||
|
<>
|
||||||
|
<Group gap="xs" align="flex-end">
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<MultiMemberSelect onChange={setMemberIds} />
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
data={pagePermissionRoleData.map((r) => ({
|
||||||
|
label: t(r.label),
|
||||||
|
value: r.value,
|
||||||
|
}))}
|
||||||
|
value={role}
|
||||||
|
onChange={(value) => value && setRole(value)}
|
||||||
|
allowDeselect={false}
|
||||||
|
variant="filled"
|
||||||
|
w={120}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={handleAddMembers}
|
||||||
|
disabled={memberIds.length === 0}
|
||||||
|
loading={addPermissionMutation.isPending}
|
||||||
|
>
|
||||||
|
{t("Add")}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
<Divider />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Text size="sm" fw={500} mb="xs">
|
||||||
|
{t("General access")}
|
||||||
|
</Text>
|
||||||
|
<GeneralAccessSelect
|
||||||
|
value={isRestricted ? "restricted" : "open"}
|
||||||
|
onChange={handleAccessChange}
|
||||||
|
disabled={!canManage || isInherited}
|
||||||
|
isInherited={isInherited}
|
||||||
|
/>
|
||||||
|
{isInherited && (
|
||||||
|
<div className={classes.inheritedInfo}>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{t("Inherits restrictions from")}
|
||||||
|
</Text>
|
||||||
|
<Link
|
||||||
|
to={buildPageUrl(
|
||||||
|
spaceSlug,
|
||||||
|
restrictionInfo.id,
|
||||||
|
restrictionInfo.title,
|
||||||
|
)}
|
||||||
|
style={{ textDecoration: "none" }}
|
||||||
|
>
|
||||||
|
<Group gap={4}>
|
||||||
|
<Text size="xs" fw={500}>
|
||||||
|
{restrictionInfo.title || t("Untitled")}
|
||||||
|
</Text>
|
||||||
|
<IconArrowRight size={12} />
|
||||||
|
</Group>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isRestricted && (
|
||||||
|
<>
|
||||||
|
{isLoading ? (
|
||||||
|
<Group justify="center" py="md">
|
||||||
|
<Loader size="sm" />
|
||||||
|
</Group>
|
||||||
|
) : (
|
||||||
|
<PagePermissionList
|
||||||
|
pageId={pageId}
|
||||||
|
members={permissionsData?.items || []}
|
||||||
|
canManage={canManage && !isInherited}
|
||||||
|
onRemoveAll={handleRemoveAll}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
.generalAccessBox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--mantine-spacing-sm);
|
||||||
|
padding: var(--mantine-spacing-xs) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generalAccessIcon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: var(--mantine-radius-sm);
|
||||||
|
|
||||||
|
@mixin light {
|
||||||
|
background-color: var(--mantine-color-gray-1);
|
||||||
|
}
|
||||||
|
@mixin dark {
|
||||||
|
background-color: var(--mantine-color-dark-5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.generalAccessIconRestricted {
|
||||||
|
@mixin light {
|
||||||
|
background-color: var(--mantine-color-red-0);
|
||||||
|
color: var(--mantine-color-red-6);
|
||||||
|
}
|
||||||
|
@mixin dark {
|
||||||
|
background-color: rgba(250, 82, 82, 0.1);
|
||||||
|
color: var(--mantine-color-red-5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.permissionItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: var(--mantine-spacing-xs) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permissionItemInfo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--mantine-spacing-sm);
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permissionItemDetails {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatarStack {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatarStackItem {
|
||||||
|
margin-left: -8px;
|
||||||
|
border: 2px solid var(--mantine-color-body);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatarStackItem:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.specificAccessHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--mantine-spacing-xs);
|
||||||
|
margin-top: var(--mantine-spacing-md);
|
||||||
|
margin-bottom: var(--mantine-spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.removeAllLink {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: var(--mantine-font-size-sm);
|
||||||
|
|
||||||
|
@mixin light {
|
||||||
|
color: var(--mantine-color-gray-6);
|
||||||
|
}
|
||||||
|
@mixin dark {
|
||||||
|
color: var(--mantine-color-dark-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inheritedInfo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--mantine-spacing-xs);
|
||||||
|
padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
|
||||||
|
border-radius: var(--mantine-radius-sm);
|
||||||
|
margin-bottom: var(--mantine-spacing-sm);
|
||||||
|
|
||||||
|
@mixin light {
|
||||||
|
background-color: var(--mantine-color-gray-0);
|
||||||
|
}
|
||||||
|
@mixin dark {
|
||||||
|
background-color: var(--mantine-color-dark-6);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Indicator,
|
||||||
|
Loader,
|
||||||
|
Modal,
|
||||||
|
Tabs,
|
||||||
|
Center,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
|
import { IconWorld, IconLock } from "@tabler/icons-react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { extractPageSlugId } from "@/lib";
|
||||||
|
import { usePageQuery } from "@/features/page/queries/page-query";
|
||||||
|
import { usePageRestrictionInfoQuery } from "@/ee/page-permission/queries/page-permission-query";
|
||||||
|
import { PagePermissionTab } from "./page-permission-tab";
|
||||||
|
import { PublishTab } from "./publish-tab";
|
||||||
|
|
||||||
|
type PageShareModalProps = {
|
||||||
|
readOnly?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PageShareModal({ readOnly }: PageShareModalProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { pageSlug } = useParams();
|
||||||
|
const pageSlugId = extractPageSlugId(pageSlug);
|
||||||
|
const [opened, { open, close }] = useDisclosure(false);
|
||||||
|
const [activeTab, setActiveTab] = useState<string | null>("share");
|
||||||
|
|
||||||
|
const { data: page } = usePageQuery({ pageId: pageSlugId });
|
||||||
|
const pageId = page?.id;
|
||||||
|
|
||||||
|
const { data: restrictionInfo, isLoading: restrictionLoading } =
|
||||||
|
usePageRestrictionInfoQuery(pageId);
|
||||||
|
|
||||||
|
const isRestricted =
|
||||||
|
restrictionInfo?.hasDirectRestriction ||
|
||||||
|
restrictionInfo?.hasInheritedRestriction;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
style={{ border: "none" }}
|
||||||
|
size="compact-sm"
|
||||||
|
leftSection={
|
||||||
|
<Indicator
|
||||||
|
color={isRestricted ? "red" : "green"}
|
||||||
|
offset={5}
|
||||||
|
disabled={!restrictionInfo}
|
||||||
|
withBorder
|
||||||
|
>
|
||||||
|
{isRestricted ? (
|
||||||
|
<IconLock size={20} stroke={1.5} />
|
||||||
|
) : (
|
||||||
|
<IconWorld size={20} stroke={1.5} />
|
||||||
|
)}
|
||||||
|
</Indicator>
|
||||||
|
}
|
||||||
|
variant="default"
|
||||||
|
onClick={open}
|
||||||
|
>
|
||||||
|
{t("Share")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
opened={opened}
|
||||||
|
onClose={close}
|
||||||
|
title={t("Share")}
|
||||||
|
size="md"
|
||||||
|
centered
|
||||||
|
>
|
||||||
|
<Tabs value={activeTab} onChange={setActiveTab}>
|
||||||
|
<Tabs.List mb="md">
|
||||||
|
<Tabs.Tab value="share">{t("Share")}</Tabs.Tab>
|
||||||
|
<Tabs.Tab value="publish">{t("Publish")}</Tabs.Tab>
|
||||||
|
</Tabs.List>
|
||||||
|
|
||||||
|
<Tabs.Panel value="share">
|
||||||
|
{restrictionLoading || !pageId ? (
|
||||||
|
<Center py="xl">
|
||||||
|
<Loader size="sm" />
|
||||||
|
</Center>
|
||||||
|
) : (
|
||||||
|
<PagePermissionTab
|
||||||
|
pageId={pageId}
|
||||||
|
restrictionInfo={restrictionInfo}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Tabs.Panel>
|
||||||
|
|
||||||
|
<Tabs.Panel value="publish">
|
||||||
|
<PublishTab pageId={pageId} readOnly={readOnly} />
|
||||||
|
</Tabs.Panel>
|
||||||
|
</Tabs>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Anchor,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Stack,
|
||||||
|
Switch,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { IconExternalLink, IconLock } from "@tabler/icons-react";
|
||||||
|
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { getPageIcon } from "@/lib";
|
||||||
|
import CopyTextButton from "@/components/common/copy";
|
||||||
|
import { getAppUrl, isCloud } from "@/lib/config";
|
||||||
|
import { buildPageUrl } from "@/features/page/page.utils";
|
||||||
|
import {
|
||||||
|
useCreateShareMutation,
|
||||||
|
useDeleteShareMutation,
|
||||||
|
useShareForPageQuery,
|
||||||
|
useUpdateShareMutation,
|
||||||
|
} from "@/features/share/queries/share-query";
|
||||||
|
import useTrial from "@/ee/hooks/use-trial";
|
||||||
|
|
||||||
|
type PublishTabProps = {
|
||||||
|
pageId: string;
|
||||||
|
readOnly?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PublishTab({ pageId, readOnly }: PublishTabProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { pageSlug, spaceSlug } = useParams();
|
||||||
|
const { isTrial } = useTrial();
|
||||||
|
|
||||||
|
const { data: share } = useShareForPageQuery(pageId);
|
||||||
|
const createShareMutation = useCreateShareMutation();
|
||||||
|
const updateShareMutation = useUpdateShareMutation();
|
||||||
|
const deleteShareMutation = useDeleteShareMutation();
|
||||||
|
|
||||||
|
const pageIsShared = share && share.level === 0;
|
||||||
|
const isDescendantShared = share && share.level > 0;
|
||||||
|
|
||||||
|
const publicLink = `${getAppUrl()}/share/${share?.key}/p/${pageSlug}`;
|
||||||
|
|
||||||
|
const [isPagePublic, setIsPagePublic] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsPagePublic(!!share);
|
||||||
|
}, [share, pageId]);
|
||||||
|
|
||||||
|
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = event.currentTarget.checked;
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
createShareMutation.mutateAsync({
|
||||||
|
pageId: pageId,
|
||||||
|
includeSubPages: true,
|
||||||
|
searchIndexing: false,
|
||||||
|
});
|
||||||
|
setIsPagePublic(value);
|
||||||
|
} else {
|
||||||
|
if (share && share.id) {
|
||||||
|
deleteShareMutation.mutateAsync(share.id);
|
||||||
|
setIsPagePublic(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubPagesChange = async (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>,
|
||||||
|
) => {
|
||||||
|
const value = event.currentTarget.checked;
|
||||||
|
updateShareMutation.mutateAsync({
|
||||||
|
shareId: share.id,
|
||||||
|
includeSubPages: value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleIndexSearchChange = async (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>,
|
||||||
|
) => {
|
||||||
|
const value = event.currentTarget.checked;
|
||||||
|
updateShareMutation.mutateAsync({
|
||||||
|
shareId: share.id,
|
||||||
|
searchIndexing: value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const shareLink = useMemo(
|
||||||
|
() => (
|
||||||
|
<Group my="sm" gap={4} wrap="nowrap">
|
||||||
|
<TextInput
|
||||||
|
variant="filled"
|
||||||
|
value={publicLink}
|
||||||
|
readOnly
|
||||||
|
rightSection={<CopyTextButton text={publicLink} />}
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
/>
|
||||||
|
<ActionIcon
|
||||||
|
component="a"
|
||||||
|
variant="default"
|
||||||
|
target="_blank"
|
||||||
|
href={publicLink}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<IconExternalLink size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Group>
|
||||||
|
),
|
||||||
|
[publicLink],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isCloud() && isTrial) {
|
||||||
|
return (
|
||||||
|
<Stack align="center" py="md">
|
||||||
|
<IconLock size={20} stroke={1.5} />
|
||||||
|
<Text size="sm" ta="center" fw={500}>
|
||||||
|
{t("Upgrade to share pages")}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed" ta="center">
|
||||||
|
{t(
|
||||||
|
"Page sharing is available on paid plans. Upgrade to share your pages publicly.",
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
<Button size="xs" onClick={() => navigate("/settings/billing")}>
|
||||||
|
{t("Upgrade Plan")}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDescendantShared) {
|
||||||
|
return (
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Text size="sm">{t("Inherits public sharing from")}</Text>
|
||||||
|
<Anchor
|
||||||
|
size="sm"
|
||||||
|
underline="never"
|
||||||
|
style={{
|
||||||
|
cursor: "pointer",
|
||||||
|
color: "var(--mantine-color-text)",
|
||||||
|
}}
|
||||||
|
component={Link}
|
||||||
|
to={buildPageUrl(
|
||||||
|
spaceSlug,
|
||||||
|
share.sharedPage.slugId,
|
||||||
|
share.sharedPage.title,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Group gap="4" wrap="nowrap">
|
||||||
|
{getPageIcon(share.sharedPage.icon)}
|
||||||
|
<Text fz="sm" fw={500} lineClamp={1}>
|
||||||
|
{share.sharedPage.title || t("untitled")}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Anchor>
|
||||||
|
{shareLink}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||||
|
<div>
|
||||||
|
<Text size="sm">
|
||||||
|
{isPagePublic ? t("Shared to web") : t("Share to web")}
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{isPagePublic
|
||||||
|
? t("Anyone with the link can view this page")
|
||||||
|
: t("Make this page publicly accessible")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
onChange={handleChange}
|
||||||
|
checked={isPagePublic}
|
||||||
|
disabled={readOnly}
|
||||||
|
size="xs"
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{pageIsShared && (
|
||||||
|
<>
|
||||||
|
{shareLink}
|
||||||
|
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||||
|
<div>
|
||||||
|
<Text size="sm">{t("Include sub-pages")}</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{t("Make sub-pages public too")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
onChange={handleSubPagesChange}
|
||||||
|
checked={share.includeSubPages}
|
||||||
|
size="xs"
|
||||||
|
disabled={readOnly}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||||
|
<div>
|
||||||
|
<Text size="sm">{t("Search engine indexing")}</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{t("Allow search engines to index page")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
onChange={handleIndexSearchChange}
|
||||||
|
checked={share.searchIndexing}
|
||||||
|
size="xs"
|
||||||
|
disabled={readOnly}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export * from "./components/page-share-modal";
|
||||||
|
export * from "./components/page-permission-tab";
|
||||||
|
export * from "./components/publish-tab";
|
||||||
|
export * from "./components/page-permission-list";
|
||||||
|
export * from "./components/page-permission-item";
|
||||||
|
export * from "./components/general-access-select";
|
||||||
|
export * from "./queries/page-permission-query";
|
||||||
|
export * from "./services/page-permission-service";
|
||||||
|
export * from "./types/page-permission.types";
|
||||||
|
export * from "./types/page-permission-role-data";
|
||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
} from "@tanstack/react-query";
|
} from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
IAddPagePermission,
|
IAddPagePermission,
|
||||||
IPagePermission,
|
IPagePermissionMember,
|
||||||
IPageRestrictionInfo,
|
IPageRestrictionInfo,
|
||||||
IRemovePagePermission,
|
IRemovePagePermission,
|
||||||
IUpdatePagePermissionRole,
|
IUpdatePagePermissionRole,
|
||||||
@@ -38,7 +38,7 @@ export function usePageRestrictionInfoQuery(
|
|||||||
export function usePagePermissionsQuery(
|
export function usePagePermissionsQuery(
|
||||||
pageId: string,
|
pageId: string,
|
||||||
params?: QueryParams,
|
params?: QueryParams,
|
||||||
): UseQueryResult<IPagination<IPagePermission>, Error> {
|
): UseQueryResult<IPagination<IPagePermissionMember>, Error> {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["page-permissions", pageId, params],
|
queryKey: ["page-permissions", pageId, params],
|
||||||
queryFn: () => getPagePermissions(pageId, params),
|
queryFn: () => getPagePermissions(pageId, params),
|
||||||
@@ -88,7 +88,7 @@ export function useUnrestrictPageMutation() {
|
|||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
const errorMessage = error["response"]?.data?.message;
|
const errorMessage = error["response"]?.data?.message;
|
||||||
notifications.show({
|
notifications.show({
|
||||||
message: errorMessage || t("Failed to unrestrict page"),
|
message: errorMessage || t("Failed to remove page restriction"),
|
||||||
color: "red",
|
color: "red",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -109,7 +109,7 @@ export function useAddPagePermissionMutation() {
|
|||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
const errorMessage = error["response"]?.data?.message;
|
const errorMessage = error["response"]?.data?.message;
|
||||||
notifications.show({
|
notifications.show({
|
||||||
message: errorMessage || t("Failed to add page permission"),
|
message: errorMessage || t("Failed to add permission"),
|
||||||
color: "red",
|
color: "red",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -130,7 +130,7 @@ export function useRemovePagePermissionMutation() {
|
|||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
const errorMessage = error["response"]?.data?.message;
|
const errorMessage = error["response"]?.data?.message;
|
||||||
notifications.show({
|
notifications.show({
|
||||||
message: errorMessage || t("Failed to remove page permission"),
|
message: errorMessage || t("Failed to remove permission"),
|
||||||
color: "red",
|
color: "red",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -151,7 +151,7 @@ export function useUpdatePagePermissionRoleMutation() {
|
|||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
const errorMessage = error["response"]?.data?.message;
|
const errorMessage = error["response"]?.data?.message;
|
||||||
notifications.show({
|
notifications.show({
|
||||||
message: errorMessage || t("Failed to update page permission role"),
|
message: errorMessage || t("Failed to update permission"),
|
||||||
color: "red",
|
color: "red",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import api from "@/lib/api-client";
|
|||||||
import { IPagination, QueryParams } from "@/lib/types";
|
import { IPagination, QueryParams } from "@/lib/types";
|
||||||
import {
|
import {
|
||||||
IAddPagePermission,
|
IAddPagePermission,
|
||||||
IPagePermission,
|
IPagePermissionMember,
|
||||||
IPageRestrictionInfo,
|
IPageRestrictionInfo,
|
||||||
IRemovePagePermission,
|
IRemovePagePermission,
|
||||||
IUpdatePagePermissionRole,
|
IUpdatePagePermissionRole,
|
||||||
@@ -37,8 +37,8 @@ export async function unrestrictPage(pageId: string): Promise<void> {
|
|||||||
export async function getPagePermissions(
|
export async function getPagePermissions(
|
||||||
pageId: string,
|
pageId: string,
|
||||||
params?: QueryParams,
|
params?: QueryParams,
|
||||||
): Promise<IPagination<IPagePermission>> {
|
): Promise<IPagination<IPagePermissionMember>> {
|
||||||
const req = await api.post<IPagination<IPagePermission>>(
|
const req = await api.post<IPagination<IPagePermissionMember>>(
|
||||||
"/pages/permissions/members",
|
"/pages/permissions/members",
|
||||||
{ pageId, ...params },
|
{ pageId, ...params },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { IRoleData } from "@/lib/types";
|
||||||
|
import { PagePermissionRole } from "./page-permission.types";
|
||||||
|
|
||||||
|
export const pagePermissionRoleData: IRoleData[] = [
|
||||||
|
{
|
||||||
|
label: "Can edit",
|
||||||
|
value: PagePermissionRole.WRITER,
|
||||||
|
description: "Can edit page and manage access",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Can view",
|
||||||
|
value: PagePermissionRole.READER,
|
||||||
|
description: "Can only view page",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getPagePermissionRoleLabel(value: string): string | undefined {
|
||||||
|
const role = pagePermissionRoleData.find((item) => item.value === value);
|
||||||
|
return role ? role.label : undefined;
|
||||||
|
}
|
||||||
@@ -3,10 +3,6 @@ export enum PagePermissionRole {
|
|||||||
WRITER = "writer",
|
WRITER = "writer",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IRestrictPage = {
|
|
||||||
pageId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type IAddPagePermission = {
|
export type IAddPagePermission = {
|
||||||
pageId: string;
|
pageId: string;
|
||||||
role: PagePermissionRole;
|
role: PagePermissionRole;
|
||||||
@@ -27,29 +23,35 @@ export type IUpdatePagePermissionRole = {
|
|||||||
groupId?: string;
|
groupId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IRemovePageRestriction = {
|
|
||||||
pageId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type IPagePermission = {
|
|
||||||
id: string;
|
|
||||||
pageId: string;
|
|
||||||
role: PagePermissionRole;
|
|
||||||
userId?: string;
|
|
||||||
groupId?: string;
|
|
||||||
user?: {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
avatarUrl: string;
|
|
||||||
};
|
|
||||||
group?: {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type IPageRestrictionInfo = {
|
export type IPageRestrictionInfo = {
|
||||||
isRestricted: boolean;
|
id: string;
|
||||||
hasAccess: boolean;
|
title: string;
|
||||||
role?: PagePermissionRole;
|
hasDirectRestriction: boolean;
|
||||||
|
hasInheritedRestriction: boolean;
|
||||||
|
userAccess: {
|
||||||
|
canView: boolean;
|
||||||
|
canEdit: boolean;
|
||||||
|
canManage: boolean;
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type IPagePermissionBase = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
createdAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IPagePermissionUser = IPagePermissionBase & {
|
||||||
|
type: "user";
|
||||||
|
email: string;
|
||||||
|
avatarUrl: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IPagePermissionGroup = IPagePermissionBase & {
|
||||||
|
type: "group";
|
||||||
|
memberCount: number;
|
||||||
|
isDefault: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IPagePermissionMember = IPagePermissionUser | IPagePermissionGroup;
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import { PageStateSegmentedControl } from "@/features/user/components/page-state
|
|||||||
import MovePageModal from "@/features/page/components/move-page-modal.tsx";
|
import MovePageModal from "@/features/page/components/move-page-modal.tsx";
|
||||||
import { useTimeAgo } from "@/hooks/use-time-ago.tsx";
|
import { useTimeAgo } from "@/hooks/use-time-ago.tsx";
|
||||||
import ShareModal from "@/features/share/components/share-modal.tsx";
|
import ShareModal from "@/features/share/components/share-modal.tsx";
|
||||||
|
import { PageShareModal } from "@/ee/page-permission";
|
||||||
|
|
||||||
interface PageHeaderMenuProps {
|
interface PageHeaderMenuProps {
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
@@ -89,7 +90,9 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) {
|
|||||||
|
|
||||||
{!readOnly && <PageStateSegmentedControl size="xs" />}
|
{!readOnly && <PageStateSegmentedControl size="xs" />}
|
||||||
|
|
||||||
<ShareModal readOnly={readOnly} />
|
{/*<ShareModal readOnly={readOnly} />*/}
|
||||||
|
<PageShareModal readOnly={readOnly}/>
|
||||||
|
|
||||||
|
|
||||||
<Tooltip label={t("Comments")} openDelay={250} withArrow>
|
<Tooltip label={t("Comments")} openDelay={250} withArrow>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
|
|||||||
Reference in New Issue
Block a user