mirror of
https://github.com/docmost/docmost.git
synced 2025-11-16 03:41:09 +10:00
frontend permissions
* rework backend workspace permissions
This commit is contained in:
@ -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>
|
||||
</>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -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>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user