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 { import {
IconChevronDown, IconChevronDown,
IconLock, IconLock,
IconWorld, IconShieldLock,
IconCheck, IconCheck,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -14,75 +14,75 @@ type GeneralAccessSelectProps = {
value: AccessLevel; value: AccessLevel;
onChange: (value: AccessLevel) => void; onChange: (value: AccessLevel) => void;
disabled?: boolean; disabled?: boolean;
isInherited?: boolean; hasInheritedRestriction?: boolean;
}; };
export function GeneralAccessSelect({ export function GeneralAccessSelect({
value, value,
onChange, onChange,
disabled, disabled,
isInherited, hasInheritedRestriction,
}: GeneralAccessSelectProps) { }: GeneralAccessSelectProps) {
const { t } = useTranslation(); 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 = [ const accessOptions = [
{ {
value: "open" as const, value: "open" as const,
label: t("Open"), label: hasInheritedRestriction ? t("Restricted by parent") : t("Open"),
description: t("Everyone in this space can access"), description: hasInheritedRestriction
icon: IconWorld, ? t("Use only inherited restrictions")
: t("Everyone in this space can access"),
icon: IconShieldLock,
}, },
{ {
value: "restricted" as const, value: "restricted" as const,
label: t("Restricted"), 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, 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 ( return (
<Menu withArrow disabled={disabled}> <Menu withArrow disabled={disabled}>
<Menu.Target> <Menu.Target>
<UnstyledButton className={classes.generalAccessBox}> <UnstyledButton className={classes.generalAccessBox} disabled={disabled}>
<div <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>
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<Group gap={4}> <Group gap={4}>
<Text size="sm" fw={500}> <Text size="sm" fw={500}>
{currentOption?.label} {currentLabel}
</Text> </Text>
{!disabled && <IconChevronDown size={14} />} {!disabled && <IconChevronDown size={14} />}
</Group> </Group>
<Text size="xs" c="dimmed"> <Text size="xs" c="dimmed">
{currentOption?.description} {currentDescription}
</Text> </Text>
</div> </div>
</UnstyledButton> </UnstyledButton>
@@ -1,8 +1,19 @@
import { useState } from "react"; 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 { useTranslation } from "react-i18next";
import { Link, useParams } from "react-router-dom"; 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 { MultiMemberSelect } from "@/features/space/components/multi-member-select";
import { import {
IPageRestrictionInfo, IPageRestrictionInfo,
@@ -39,18 +50,14 @@ export function PagePermissionTab({
const unrestrictMutation = useUnrestrictPageMutation(); const unrestrictMutation = useUnrestrictPageMutation();
const addPermissionMutation = useAddPagePermissionMutation(); const addPermissionMutation = useAddPagePermissionMutation();
const isRestricted = const hasInheritedRestriction = restrictionInfo.hasInheritedRestriction;
restrictionInfo.hasDirectRestriction || const hasDirectRestriction = restrictionInfo.hasDirectRestriction;
restrictionInfo.hasInheritedRestriction;
const isInherited =
restrictionInfo.hasInheritedRestriction &&
!restrictionInfo.hasDirectRestriction;
const canManage = restrictionInfo.userAccess.canManage; const canManage = restrictionInfo.userAccess.canManage;
const handleAccessChange = async (value: "open" | "restricted") => { const handleDirectAccessChange = async (value: "open" | "restricted") => {
if (value === "restricted" && !isRestricted) { if (value === "restricted" && !hasDirectRestriction) {
await restrictMutation.mutateAsync(pageId); await restrictMutation.mutateAsync(pageId);
} else if (value === "open" && isRestricted) { } else if (value === "open" && hasDirectRestriction) {
await unrestrictMutation.mutateAsync(pageId); await unrestrictMutation.mutateAsync(pageId);
} }
}; };
@@ -81,72 +88,99 @@ export function PagePermissionTab({
}; };
return ( return (
<Stack gap="sm"> <Stack gap="md">
{isRestricted && canManage && !isInherited && ( {hasInheritedRestriction && (
<> <Paper className={classes.inheritedSection} p="sm" radius="sm">
<Group gap="xs" align="flex-end"> <Group gap="sm" wrap="nowrap">
<div style={{ flex: 1 }}> <ThemeIcon
<MultiMemberSelect onChange={setMemberIds} /> size="lg"
</div> radius="sm"
<Select variant="light"
data={pagePermissionRoleData.map((r) => ({ color="orange"
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")} <IconShieldLock size={18} stroke={1.5} />
</Button> </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> </Group>
<Divider /> </Paper>
</>
)} )}
<div> <Box>
<Text size="sm" fw={500} mb="xs"> <Text size="sm" fw={500} mb="xs">
{t("General access")} {t("This page")}
</Text> </Text>
<GeneralAccessSelect <GeneralAccessSelect
value={isRestricted ? "restricted" : "open"} value={hasDirectRestriction ? "restricted" : "open"}
onChange={handleAccessChange} onChange={handleDirectAccessChange}
disabled={!canManage || isInherited} disabled={!canManage}
isInherited={isInherited} hasInheritedRestriction={hasInheritedRestriction}
/> />
{isInherited && ( {!hasDirectRestriction && !hasInheritedRestriction && (
<div className={classes.inheritedInfo}> <Text size="xs" c="dimmed" mt={4}>
<Text size="xs" c="dimmed"> {t("Everyone in this space can access this page")}
{t("Inherits restrictions from")} </Text>
</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> {!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 ? ( {isLoading ? (
<Group justify="center" py="md"> <Group justify="center" py="md">
<Loader size="sm" /> <Loader size="sm" />
@@ -155,7 +189,7 @@ export function PagePermissionTab({
<PagePermissionList <PagePermissionList
pageId={pageId} pageId={pageId}
members={permissionsData?.items || []} members={permissionsData?.items || []}
canManage={canManage && !isInherited} canManage={canManage}
onRemoveAll={handleRemoveAll} onRemoveAll={handleRemoveAll}
/> />
)} )}
@@ -106,3 +106,14 @@
background-color: var(--mantine-color-dark-6); 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 { extractPageSlugId } from "@/lib";
import { usePageQuery } from "@/features/page/queries/page-query"; import { usePageQuery } from "@/features/page/queries/page-query";
import { usePageRestrictionInfoQuery } from "@/ee/page-permission/queries/page-permission-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"; import { PublishTab } from "./publish-tab";
type PageShareModalProps = { type PageShareModalProps = {
@@ -67,8 +67,7 @@ export function PageShareModal({ readOnly }: PageShareModalProps) {
opened={opened} opened={opened}
onClose={close} onClose={close}
title={t("Share")} title={t("Share")}
size="md" size={600}
centered
> >
<Tabs value={activeTab} onChange={setActiveTab}> <Tabs value={activeTab} onChange={setActiveTab}>
<Tabs.List mb="md"> <Tabs.List mb="md">