clear inheritance

This commit is contained in:
Philipinho
2026-01-18 23:35:56 +00:00
parent 1b13f80fb8
commit b4e8a5af9e
4 changed files with 150 additions and 106 deletions
@@ -2,7 +2,7 @@ import { Group, Menu, Text, UnstyledButton } from "@mantine/core";
import {
IconChevronDown,
IconLock,
IconWorld,
IconShieldLock,
IconCheck,
} from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
@@ -14,75 +14,75 @@ type GeneralAccessSelectProps = {
value: AccessLevel;
onChange: (value: AccessLevel) => void;
disabled?: boolean;
isInherited?: boolean;
hasInheritedRestriction?: boolean;
};
export function GeneralAccessSelect({
value,
onChange,
disabled,
isInherited,
hasInheritedRestriction,
}: GeneralAccessSelectProps) {
const { t } = useTranslation();
const isRestricted = value === "restricted";
const isDirectlyRestricted = value === "restricted";
const showInheritedState = hasInheritedRestriction && !isDirectlyRestricted;
const currentLabel = showInheritedState
? t("Restricted by parent")
: isDirectlyRestricted
? t("Restricted")
: t("Open");
const currentDescription = showInheritedState
? t("Inherits restrictions from ancestor page")
: isDirectlyRestricted
? t("Only specific people can access")
: t("Everyone in this space can access");
const CurrentIcon = showInheritedState
? IconShieldLock
: isDirectlyRestricted
? IconLock
: IconShieldLock;
const accessOptions = [
{
value: "open" as const,
label: t("Open"),
description: t("Everyone in this space can access"),
icon: IconWorld,
label: hasInheritedRestriction ? t("Restricted by parent") : t("Open"),
description: hasInheritedRestriction
? t("Use only inherited restrictions")
: t("Everyone in this space can access"),
icon: IconShieldLock,
},
{
value: "restricted" as const,
label: t("Restricted"),
description: t("Only specific people can view or edit"),
description: hasInheritedRestriction
? t("Add restrictions on top of inherited")
: t("Only specific people can access"),
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}>
<UnstyledButton className={classes.generalAccessBox} disabled={disabled}>
<div
className={`${classes.generalAccessIcon} ${isRestricted ? classes.generalAccessIconRestricted : ""}`}
className={`${classes.generalAccessIcon} ${isDirectlyRestricted || showInheritedState ? classes.generalAccessIconRestricted : ""}`}
>
<Icon size={18} stroke={1.5} />
<CurrentIcon size={18} stroke={1.5} />
</div>
<div style={{ flex: 1 }}>
<Group gap={4}>
<Text size="sm" fw={500}>
{currentOption?.label}
{currentLabel}
</Text>
{!disabled && <IconChevronDown size={14} />}
</Group>
<Text size="xs" c="dimmed">
{currentOption?.description}
{currentDescription}
</Text>
</div>
</UnstyledButton>
@@ -1,8 +1,19 @@
import { useState } from "react";
import { Button, Divider, Group, Loader, Select, Stack, Text } from "@mantine/core";
import {
Box,
Button,
Divider,
Group,
Loader,
Paper,
Select,
Stack,
Text,
ThemeIcon,
} from "@mantine/core";
import { useTranslation } from "react-i18next";
import { Link, useParams } from "react-router-dom";
import { IconArrowRight } from "@tabler/icons-react";
import { IconArrowRight, IconLock, IconShieldLock } from "@tabler/icons-react";
import { MultiMemberSelect } from "@/features/space/components/multi-member-select";
import {
IPageRestrictionInfo,
@@ -39,18 +50,14 @@ export function PagePermissionTab({
const unrestrictMutation = useUnrestrictPageMutation();
const addPermissionMutation = useAddPagePermissionMutation();
const isRestricted =
restrictionInfo.hasDirectRestriction ||
restrictionInfo.hasInheritedRestriction;
const isInherited =
restrictionInfo.hasInheritedRestriction &&
!restrictionInfo.hasDirectRestriction;
const hasInheritedRestriction = restrictionInfo.hasInheritedRestriction;
const hasDirectRestriction = restrictionInfo.hasDirectRestriction;
const canManage = restrictionInfo.userAccess.canManage;
const handleAccessChange = async (value: "open" | "restricted") => {
if (value === "restricted" && !isRestricted) {
const handleDirectAccessChange = async (value: "open" | "restricted") => {
if (value === "restricted" && !hasDirectRestriction) {
await restrictMutation.mutateAsync(pageId);
} else if (value === "open" && isRestricted) {
} else if (value === "open" && hasDirectRestriction) {
await unrestrictMutation.mutateAsync(pageId);
}
};
@@ -81,72 +88,99 @@ export function PagePermissionTab({
};
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}
<Stack gap="md">
{hasInheritedRestriction && (
<Paper className={classes.inheritedSection} p="sm" radius="sm">
<Group gap="sm" wrap="nowrap">
<ThemeIcon
size="lg"
radius="sm"
variant="light"
color="orange"
>
{t("Add")}
</Button>
<IconShieldLock size={18} stroke={1.5} />
</ThemeIcon>
<Box style={{ flex: 1 }}>
<Text size="sm" fw={500}>
{t("Inherited restriction")}
</Text>
<Group gap={4}>
<Text size="xs" c="dimmed">
{t("Access limited by")}
</Text>
<Link
to={buildPageUrl(
spaceSlug,
restrictionInfo.id,
restrictionInfo.title,
)}
style={{ textDecoration: "none" }}
>
<Group gap={2}>
<Text size="xs" fw={500} c="blue">
{restrictionInfo.title || t("Untitled")}
</Text>
<IconArrowRight size={12} color="var(--mantine-color-blue-6)" />
</Group>
</Link>
</Group>
</Box>
</Group>
<Divider />
</>
</Paper>
)}
<div>
<Box>
<Text size="sm" fw={500} mb="xs">
{t("General access")}
{t("This page")}
</Text>
<GeneralAccessSelect
value={isRestricted ? "restricted" : "open"}
onChange={handleAccessChange}
disabled={!canManage || isInherited}
isInherited={isInherited}
value={hasDirectRestriction ? "restricted" : "open"}
onChange={handleDirectAccessChange}
disabled={!canManage}
hasInheritedRestriction={hasInheritedRestriction}
/>
{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>
{!hasDirectRestriction && !hasInheritedRestriction && (
<Text size="xs" c="dimmed" mt={4}>
{t("Everyone in this space can access this page")}
</Text>
)}
</div>
{!hasDirectRestriction && hasInheritedRestriction && (
<Text size="xs" c="dimmed" mt={4}>
{t("Add additional restrictions specific to this page")}
</Text>
)}
</Box>
{isRestricted && (
{hasDirectRestriction && (
<>
<Divider />
{canManage && (
<Group gap="xs" align="flex-end">
<Box style={{ flex: 1 }}>
<MultiMemberSelect onChange={setMemberIds} />
</Box>
<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>
)}
{isLoading ? (
<Group justify="center" py="md">
<Loader size="sm" />
@@ -155,7 +189,7 @@ export function PagePermissionTab({
<PagePermissionList
pageId={pageId}
members={permissionsData?.items || []}
canManage={canManage && !isInherited}
canManage={canManage}
onRemoveAll={handleRemoveAll}
/>
)}
@@ -106,3 +106,14 @@
background-color: var(--mantine-color-dark-6);
}
}
.inheritedSection {
@mixin light {
background-color: var(--mantine-color-orange-0);
border: 1px solid var(--mantine-color-orange-2);
}
@mixin dark {
background-color: rgba(255, 146, 43, 0.08);
border: 1px solid rgba(255, 146, 43, 0.2);
}
}
@@ -14,7 +14,7 @@ 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 { PagePermissionTab } from "@/ee/page-permission";
import { PublishTab } from "./publish-tab";
type PageShareModalProps = {
@@ -67,8 +67,7 @@ export function PageShareModal({ readOnly }: PageShareModalProps) {
opened={opened}
onClose={close}
title={t("Share")}
size="md"
centered
size={600}
>
<Tabs value={activeTab} onChange={setActiveTab}>
<Tabs.List mb="md">