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 && (
+ <>
+
+
+
+
+ ({
+ label: t(r.label),
+ value: r.value,
+ }))}
+ value={role}
+ onChange={(value) => value && setRole(value)}
+ allowDeselect={false}
+ variant="filled"
+ w={120}
+ />
+
+ {t("Add")}
+
+
+
+ >
+ )}
+
+
+
+ {t("General access")}
+
+
+ {isInherited && (
+
+
+ {t("Inherits restrictions from")}
+
+
+
+
+ {restrictionInfo.title || t("Untitled")}
+
+
+
+
+
+ )}
+
+
+ {isRestricted && (
+ <>
+ {isLoading ? (
+
+
+
+ ) : (
+
+ )}
+ >
+ )}
+
+ );
+}
diff --git a/apps/client/src/ee/page-permission/components/page-permission.module.css b/apps/client/src/ee/page-permission/components/page-permission.module.css
new file mode 100644
index 00000000..545b9155
--- /dev/null
+++ b/apps/client/src/ee/page-permission/components/page-permission.module.css
@@ -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);
+ }
+}
diff --git a/apps/client/src/ee/page-permission/components/page-share-modal.tsx b/apps/client/src/ee/page-permission/components/page-share-modal.tsx
new file mode 100644
index 00000000..eb69fd47
--- /dev/null
+++ b/apps/client/src/ee/page-permission/components/page-share-modal.tsx
@@ -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("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 (
+ <>
+
+ {isRestricted ? (
+
+ ) : (
+
+ )}
+
+ }
+ variant="default"
+ onClick={open}
+ >
+ {t("Share")}
+
+
+
+
+
+ {t("Share")}
+ {t("Publish")}
+
+
+
+ {restrictionLoading || !pageId ? (
+
+
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/apps/client/src/ee/page-permission/components/publish-tab.tsx b/apps/client/src/ee/page-permission/components/publish-tab.tsx
new file mode 100644
index 00000000..e75918cc
--- /dev/null
+++ b/apps/client/src/ee/page-permission/components/publish-tab.tsx
@@ -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(false);
+
+ useEffect(() => {
+ setIsPagePublic(!!share);
+ }, [share, pageId]);
+
+ const handleChange = async (event: React.ChangeEvent) => {
+ 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,
+ ) => {
+ const value = event.currentTarget.checked;
+ updateShareMutation.mutateAsync({
+ shareId: share.id,
+ includeSubPages: value,
+ });
+ };
+
+ const handleIndexSearchChange = async (
+ event: React.ChangeEvent,
+ ) => {
+ const value = event.currentTarget.checked;
+ updateShareMutation.mutateAsync({
+ shareId: share.id,
+ searchIndexing: value,
+ });
+ };
+
+ const shareLink = useMemo(
+ () => (
+
+ }
+ style={{ width: "100%" }}
+ />
+
+
+
+
+ ),
+ [publicLink],
+ );
+
+ if (isCloud() && isTrial) {
+ return (
+
+
+
+ {t("Upgrade to share pages")}
+
+
+ {t(
+ "Page sharing is available on paid plans. Upgrade to share your pages publicly.",
+ )}
+
+ navigate("/settings/billing")}>
+ {t("Upgrade Plan")}
+
+
+ );
+ }
+
+ if (isDescendantShared) {
+ return (
+
+ {t("Inherits public sharing from")}
+
+
+ {getPageIcon(share.sharedPage.icon)}
+
+ {share.sharedPage.title || t("untitled")}
+
+
+
+ {shareLink}
+
+ );
+ }
+
+ return (
+
+
+
+
+ {isPagePublic ? t("Shared to web") : t("Share to web")}
+
+
+ {isPagePublic
+ ? t("Anyone with the link can view this page")
+ : t("Make this page publicly accessible")}
+
+
+
+
+
+ {pageIsShared && (
+ <>
+ {shareLink}
+
+
+ {t("Include sub-pages")}
+
+ {t("Make sub-pages public too")}
+
+
+
+
+
+
+ {t("Search engine indexing")}
+
+ {t("Allow search engines to index page")}
+
+
+
+
+ >
+ )}
+
+ );
+}
diff --git a/apps/client/src/ee/page-permission/index.ts b/apps/client/src/ee/page-permission/index.ts
new file mode 100644
index 00000000..32597373
--- /dev/null
+++ b/apps/client/src/ee/page-permission/index.ts
@@ -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";
diff --git a/apps/client/src/ee/page-permission/queries/page-permission-query.ts b/apps/client/src/ee/page-permission/queries/page-permission-query.ts
index d76fd0d7..a2b53520 100644
--- a/apps/client/src/ee/page-permission/queries/page-permission-query.ts
+++ b/apps/client/src/ee/page-permission/queries/page-permission-query.ts
@@ -7,7 +7,7 @@ import {
} from "@tanstack/react-query";
import {
IAddPagePermission,
- IPagePermission,
+ IPagePermissionMember,
IPageRestrictionInfo,
IRemovePagePermission,
IUpdatePagePermissionRole,
@@ -38,7 +38,7 @@ export function usePageRestrictionInfoQuery(
export function usePagePermissionsQuery(
pageId: string,
params?: QueryParams,
-): UseQueryResult, Error> {
+): UseQueryResult, Error> {
return useQuery({
queryKey: ["page-permissions", pageId, params],
queryFn: () => getPagePermissions(pageId, params),
@@ -88,7 +88,7 @@ export function useUnrestrictPageMutation() {
onError: (error) => {
const errorMessage = error["response"]?.data?.message;
notifications.show({
- message: errorMessage || t("Failed to unrestrict page"),
+ message: errorMessage || t("Failed to remove page restriction"),
color: "red",
});
},
@@ -109,7 +109,7 @@ export function useAddPagePermissionMutation() {
onError: (error) => {
const errorMessage = error["response"]?.data?.message;
notifications.show({
- message: errorMessage || t("Failed to add page permission"),
+ message: errorMessage || t("Failed to add permission"),
color: "red",
});
},
@@ -130,7 +130,7 @@ export function useRemovePagePermissionMutation() {
onError: (error) => {
const errorMessage = error["response"]?.data?.message;
notifications.show({
- message: errorMessage || t("Failed to remove page permission"),
+ message: errorMessage || t("Failed to remove permission"),
color: "red",
});
},
@@ -151,7 +151,7 @@ export function useUpdatePagePermissionRoleMutation() {
onError: (error) => {
const errorMessage = error["response"]?.data?.message;
notifications.show({
- message: errorMessage || t("Failed to update page permission role"),
+ message: errorMessage || t("Failed to update permission"),
color: "red",
});
},
diff --git a/apps/client/src/ee/page-permission/services/page-permission-service.ts b/apps/client/src/ee/page-permission/services/page-permission-service.ts
index 86bef081..087b9b7c 100644
--- a/apps/client/src/ee/page-permission/services/page-permission-service.ts
+++ b/apps/client/src/ee/page-permission/services/page-permission-service.ts
@@ -2,7 +2,7 @@ import api from "@/lib/api-client";
import { IPagination, QueryParams } from "@/lib/types";
import {
IAddPagePermission,
- IPagePermission,
+ IPagePermissionMember,
IPageRestrictionInfo,
IRemovePagePermission,
IUpdatePagePermissionRole,
@@ -37,8 +37,8 @@ export async function unrestrictPage(pageId: string): Promise {
export async function getPagePermissions(
pageId: string,
params?: QueryParams,
-): Promise> {
- const req = await api.post>(
+): Promise> {
+ const req = await api.post>(
"/pages/permissions/members",
{ pageId, ...params },
);
diff --git a/apps/client/src/ee/page-permission/types/page-permission-role-data.ts b/apps/client/src/ee/page-permission/types/page-permission-role-data.ts
new file mode 100644
index 00000000..057bc42a
--- /dev/null
+++ b/apps/client/src/ee/page-permission/types/page-permission-role-data.ts
@@ -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;
+}
diff --git a/apps/client/src/ee/page-permission/types/page-permission.types.ts b/apps/client/src/ee/page-permission/types/page-permission.types.ts
index 92955bbf..bd00de67 100644
--- a/apps/client/src/ee/page-permission/types/page-permission.types.ts
+++ b/apps/client/src/ee/page-permission/types/page-permission.types.ts
@@ -3,10 +3,6 @@ export enum PagePermissionRole {
WRITER = "writer",
}
-export type IRestrictPage = {
- pageId: string;
-};
-
export type IAddPagePermission = {
pageId: string;
role: PagePermissionRole;
@@ -27,29 +23,35 @@ export type IUpdatePagePermissionRole = {
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 = {
- isRestricted: boolean;
- hasAccess: boolean;
- role?: PagePermissionRole;
+ id: string;
+ title: string;
+ 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;
diff --git a/apps/client/src/features/page/components/header/page-header-menu.tsx b/apps/client/src/features/page/components/header/page-header-menu.tsx
index cf61ac39..b9663427 100644
--- a/apps/client/src/features/page/components/header/page-header-menu.tsx
+++ b/apps/client/src/features/page/components/header/page-header-menu.tsx
@@ -44,6 +44,7 @@ import { PageStateSegmentedControl } from "@/features/user/components/page-state
import MovePageModal from "@/features/page/components/move-page-modal.tsx";
import { useTimeAgo } from "@/hooks/use-time-ago.tsx";
import ShareModal from "@/features/share/components/share-modal.tsx";
+import { PageShareModal } from "@/ee/page-permission";
interface PageHeaderMenuProps {
readOnly?: boolean;
@@ -89,7 +90,9 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) {
{!readOnly && }
-
+ {/* */}
+
+