frontend permissions

* rework backend workspace permissions
This commit is contained in:
Philipinho
2024-06-03 02:54:12 +01:00
parent b88e0b605f
commit 886d9591fa
54 changed files with 715 additions and 385 deletions

View File

@ -13,8 +13,9 @@ const formSchema = z.object({
type FormValues = z.infer<typeof formSchema>;
interface EditSpaceFormProps {
space: ISpace;
readOnly?: boolean;
}
export function EditSpaceForm({ space }: EditSpaceFormProps) {
export function EditSpaceForm({ space, readOnly }: EditSpaceFormProps) {
const updateSpaceMutation = useUpdateSpaceMutation();
const form = useForm<FormValues>({
@ -51,14 +52,16 @@ export function EditSpaceForm({ space }: EditSpaceFormProps) {
<TextInput
id="name"
label="Name"
placeholder="e.g Developers"
placeholder="e.g Sales"
variant="filled"
{...form.getInputProps("name")}
/>
<Textarea
id="description"
label="Description"
placeholder="e.g Space for developers to collaborate"
placeholder="e.g Space for sales team to collaborate"
variant="filled"
autosize
minRows={1}
maxRows={3}
@ -66,11 +69,13 @@ export function EditSpaceForm({ space }: EditSpaceFormProps) {
/>
</Stack>
<Group justify="flex-end" mt="md">
<Button type="submit" disabled={!form.isDirty()}>
Save
</Button>
</Group>
{!readOnly && (
<Group justify="flex-end" mt="md">
<Button type="submit" disabled={!form.isDirty()}>
Save
</Button>
</Group>
)}
</form>
</Box>
</>

View File

@ -1,10 +1,14 @@
import { Modal, Tabs, rem, Group, Divider, ScrollArea } from "@mantine/core";
import SpaceMembersList from "@/features/space/components/space-members.tsx";
import AddSpaceMembersModal from "@/features/space/components/add-space-members-modal.tsx";
import React from "react";
import GroupActionMenu from "@/features/group/components/group-action-menu.tsx";
import React, { useMemo } from "react";
import SpaceDetails from "@/features/space/components/space-details.tsx";
import { useSpaceQuery } from "@/features/space/queries/space-query.ts";
import { useSpaceAbility } from "@/features/space/permissions/use-space-ability.ts";
import {
SpaceCaslAction,
SpaceCaslSubject,
} from "@/features/space/permissions/permissions.type.ts";
interface SpaceSettingsModalProps {
spaceId: string;
@ -19,6 +23,9 @@ export default function SpaceSettingsModal({
}: SpaceSettingsModalProps) {
const { data: space, isLoading } = useSpaceQuery(spaceId);
const spaceRules = space?.membership?.permissions;
const spaceAbility = useMemo(() => useSpaceAbility(spaceRules), [spaceRules]);
return (
<>
<Modal.Root
@ -50,17 +57,30 @@ export default function SpaceSettingsModal({
<ScrollArea h="600" w="100%" scrollbarSize={5}>
<Tabs.Panel value="general">
<SpaceDetails spaceId={space?.id} />
<Divider my="sm" />
<SpaceDetails
spaceId={space?.id}
readOnly={spaceAbility.cannot(
SpaceCaslAction.Manage,
SpaceCaslSubject.Settings,
)}
/>
</Tabs.Panel>
<Tabs.Panel value="members">
<Group my="md" justify="flex-end">
<AddSpaceMembersModal spaceId={space?.id} />
<GroupActionMenu />
{spaceAbility.can(
SpaceCaslAction.Manage,
SpaceCaslSubject.Member,
) && <AddSpaceMembersModal spaceId={space?.id} />}
</Group>
<SpaceMembersList spaceId={space?.id} />
<SpaceMembersList
spaceId={space?.id}
readOnly={spaceAbility.cannot(
SpaceCaslAction.Manage,
SpaceCaslSubject.Member,
)}
/>
</Tabs.Panel>
</ScrollArea>
</Tabs>

View File

@ -9,7 +9,7 @@ export function SpaceName({ spaceName }: SpaceNameProps) {
<UnstyledButton className={classes.spaceName}>
<Group>
<div style={{ flex: 1 }}>
<Text size="md" fw={500}>
<Text size="md" fw={500} lineClamp={1}>
{spaceName}
</Text>
</div>

View File

@ -1,21 +1,21 @@
import {
UnstyledButton,
Text,
Group,
ActionIcon,
Tooltip,
Group,
rem,
Text,
Tooltip,
UnstyledButton,
} from "@mantine/core";
import { spotlight } from "@mantine/spotlight";
import {
IconSearch,
IconPlus,
IconSettings,
IconHome,
IconPlus,
IconSearch,
IconSettings,
} from "@tabler/icons-react";
import classes from "./space-sidebar.module.css";
import React from "react";
import React, { useMemo } from "react";
import { useAtom } from "jotai";
import { SearchSpotlight } from "@/features/search/search-spotlight.tsx";
import { treeApiAtom } from "@/features/page/tree/atoms/tree-api-atom.ts";
@ -27,6 +27,11 @@ import { useGetSpaceBySlugQuery } from "@/features/space/queries/space-query.ts"
import { SpaceName } from "@/features/space/components/sidebar/space-name.tsx";
import { getSpaceUrl } from "@/lib/config.ts";
import SpaceTree from "@/features/page/tree/components/space-tree.tsx";
import { useSpaceAbility } from "@/features/space/permissions/use-space-ability.ts";
import {
SpaceCaslAction,
SpaceCaslSubject,
} from "@/features/space/permissions/permissions.type.ts";
export function SpaceSidebar() {
const [tree] = useAtom(treeApiAtom);
@ -36,14 +41,17 @@ export function SpaceSidebar() {
const { spaceSlug } = useParams();
const { data: space, isLoading, isError } = useGetSpaceBySlugQuery(spaceSlug);
function handleCreatePage() {
tree?.create({ parentId: null, type: "internal", index: 0 });
}
const spaceRules = space?.membership?.permissions;
const spaceAbility = useMemo(() => useSpaceAbility(spaceRules), [spaceRules]);
if (!space) {
return <></>;
}
function handleCreatePage() {
tree?.create({ parentId: null, type: "internal", index: 0 });
}
return (
<>
<div className={classes.navbar}>
@ -110,22 +118,33 @@ export function SpaceSidebar() {
Pages
</Text>
<Tooltip label="Create page" withArrow position="right">
<ActionIcon
variant="default"
size={18}
onClick={handleCreatePage}
>
<IconPlus
style={{ width: rem(12), height: rem(12) }}
stroke={1.5}
/>
</ActionIcon>
</Tooltip>
{spaceAbility.can(
SpaceCaslAction.Manage,
SpaceCaslSubject.Page,
) && (
<Tooltip label="Create page" withArrow position="right">
<ActionIcon
variant="default"
size={18}
onClick={handleCreatePage}
>
<IconPlus
style={{ width: rem(12), height: rem(12) }}
stroke={1.5}
/>
</ActionIcon>
</Tooltip>
)}
</Group>
<div className={classes.pages}>
<SpaceTree spaceId={space.id} />
<SpaceTree
spaceId={space.id}
readOnly={spaceAbility.cannot(
SpaceCaslAction.Manage,
SpaceCaslSubject.Page,
)}
/>
</div>
</div>
</div>

View File

@ -5,8 +5,9 @@ import { Text } from "@mantine/core";
interface SpaceDetailsProps {
spaceId: string;
readOnly?: boolean;
}
export default function SpaceDetails({ spaceId }: SpaceDetailsProps) {
export default function SpaceDetails({ spaceId, readOnly }: SpaceDetailsProps) {
const { data: space, isLoading } = useSpaceQuery(spaceId);
return (
@ -16,7 +17,7 @@ export default function SpaceDetails({ spaceId }: SpaceDetailsProps) {
<Text my="md" fw={600}>
Details
</Text>
<EditSpaceForm space={space} />
<EditSpaceForm space={space} readOnly={readOnly} />
</div>
)}
</>

View File

@ -16,12 +16,17 @@ import {
getSpaceRoleLabel,
spaceRoleData,
} from "@/features/space/types/space-role-data.ts";
import { formatMemberCount } from "@/lib";
type MemberType = "user" | "group";
interface SpaceMembersProps {
spaceId: string;
readOnly?: boolean;
}
export default function SpaceMembersList({ spaceId }: SpaceMembersProps) {
export default function SpaceMembersList({
spaceId,
readOnly,
}: SpaceMembersProps) {
const { data, isLoading } = useSpaceMembersQuery(spaceId);
const removeSpaceMember = useRemoveSpaceMemberMutation();
const changeSpaceMemberRoleMutation = useChangeSpaceMemberRoleMutation();
@ -120,7 +125,7 @@ export default function SpaceMembersList({ spaceId }: SpaceMembersProps) {
{member.type == "user" && member?.email}
{member.type == "group" &&
`Group - ${member?.memberCount === 1 ? "1 member" : `${member?.memberCount} members`}`}
`Group - ${formatMemberCount(member?.memberCount)}`}
</Text>
</div>
</Group>
@ -138,32 +143,37 @@ export default function SpaceMembersList({ spaceId }: SpaceMembersProps) {
member.role,
)
}
disabled={readOnly}
/>
</Table.Td>
<Table.Td>
<Menu
shadow="xl"
position="bottom-end"
offset={20}
width={200}
withArrow
arrowPosition="center"
>
<Menu.Target>
<ActionIcon variant="subtle" c="gray">
<IconDots size={20} stroke={2} />
</ActionIcon>
</Menu.Target>
{!readOnly && (
<Menu
shadow="xl"
position="bottom-end"
offset={20}
width={200}
withArrow
arrowPosition="center"
>
<Menu.Target>
<ActionIcon variant="subtle" c="gray">
<IconDots size={20} stroke={2} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
onClick={() => openRemoveModal(member.id, member.type)}
>
Remove space member
</Menu.Item>
</Menu.Dropdown>
</Menu>
<Menu.Dropdown>
<Menu.Item
onClick={() =>
openRemoveModal(member.id, member.type)
}
>
Remove space member
</Menu.Item>
</Menu.Dropdown>
</Menu>
)}
</Table.Td>
</Table.Tr>
))}