mirror of
https://github.com/docmost/docmost.git
synced 2025-11-13 09:42:37 +10:00
Allow creation of space
* other fixes and cleanups
This commit is contained in:
@ -2,7 +2,8 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Docmost</title>
|
<title>Docmost</title>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
BIN
apps/client/public/favicon-16x16.png
Normal file
BIN
apps/client/public/favicon-16x16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 562 B |
BIN
apps/client/public/favicon-32x32.png
Normal file
BIN
apps/client/public/favicon-32x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
@ -64,7 +64,7 @@ export default function RecentChanges({ spaceId }: Props) {
|
|||||||
</Table>
|
</Table>
|
||||||
) : (
|
) : (
|
||||||
<Text size="md" ta="center">
|
<Text size="md" ta="center">
|
||||||
No records to show
|
No pages yet
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,10 +29,11 @@ export default function TopMenu() {
|
|||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<UnstyledButton>
|
<UnstyledButton>
|
||||||
<Group gap={7} wrap={"nowrap"}>
|
<Group gap={7} wrap={"nowrap"}>
|
||||||
<Avatar
|
<UserAvatar
|
||||||
src={workspace.logo}
|
avatarUrl={workspace.logo}
|
||||||
alt={workspace.name}
|
name={workspace.name}
|
||||||
radius="xl"
|
radius="xl"
|
||||||
|
color="blue"
|
||||||
size={20}
|
size={20}
|
||||||
/>
|
/>
|
||||||
<Text fw={500} size="sm" lh={1} mr={3}>
|
<Text fw={500} size="sm" lh={1} mr={3}>
|
||||||
|
|||||||
@ -1,22 +1,17 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as z from "zod";
|
import * as z from "zod";
|
||||||
|
|
||||||
import { useForm, zodResolver } from "@mantine/form";
|
import { useForm, zodResolver } from "@mantine/form";
|
||||||
import useAuth from "@/features/auth/hooks/use-auth";
|
import useAuth from "@/features/auth/hooks/use-auth";
|
||||||
import { ILogin } from "@/features/auth/types/auth.types";
|
import { ILogin } from "@/features/auth/types/auth.types";
|
||||||
import {
|
import {
|
||||||
Container,
|
Container,
|
||||||
Title,
|
Title,
|
||||||
Anchor,
|
|
||||||
TextInput,
|
TextInput,
|
||||||
Button,
|
Button,
|
||||||
Text,
|
|
||||||
PasswordInput,
|
PasswordInput,
|
||||||
Box,
|
Box,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
|
||||||
import classes from "./auth.module.css";
|
import classes from "./auth.module.css";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts";
|
import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts";
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
|
|||||||
110
apps/client/src/features/space/components/create-space-form.tsx
Normal file
110
apps/client/src/features/space/components/create-space-form.tsx
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import { Group, Box, Button, TextInput, Stack, Textarea } from "@mantine/core";
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { useForm, zodResolver } from "@mantine/form";
|
||||||
|
import * as z from "zod";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useCreateSpaceMutation } from "@/features/space/queries/space-query.ts";
|
||||||
|
import { computeSpaceSlug } from "@/lib";
|
||||||
|
import { getSpaceUrl } from "@/lib/config.ts";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
name: z.string().min(2).max(50),
|
||||||
|
slug: z
|
||||||
|
.string()
|
||||||
|
.min(2)
|
||||||
|
.max(50)
|
||||||
|
.regex(
|
||||||
|
/^[a-zA-Z0-9]+$/,
|
||||||
|
"Space slug must be alphanumeric. No special characters",
|
||||||
|
),
|
||||||
|
description: z.string().max(500),
|
||||||
|
});
|
||||||
|
type FormValues = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
export function CreateSpaceForm() {
|
||||||
|
const createSpaceMutation = useCreateSpaceMutation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const form = useForm<FormValues>({
|
||||||
|
validate: zodResolver(formSchema),
|
||||||
|
validateInputOnChange: ["slug"],
|
||||||
|
initialValues: {
|
||||||
|
name: "",
|
||||||
|
slug: "",
|
||||||
|
description: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const name = form.values.name;
|
||||||
|
const words = name.trim().split(/\s+/);
|
||||||
|
|
||||||
|
// Check if the last character is a space or if the last word is a single character (indicating it's in progress)
|
||||||
|
const lastChar = name[name.length - 1];
|
||||||
|
const lastWordIsIncomplete =
|
||||||
|
words.length > 1 && words[words.length - 1].length === 1;
|
||||||
|
|
||||||
|
if (lastChar !== " " || lastWordIsIncomplete) {
|
||||||
|
const slug = computeSpaceSlug(name);
|
||||||
|
form.setFieldValue("slug", slug);
|
||||||
|
}
|
||||||
|
}, [form.values.name]);
|
||||||
|
|
||||||
|
const handleSubmit = async (data: {
|
||||||
|
name?: string;
|
||||||
|
slug?: string;
|
||||||
|
description?: string;
|
||||||
|
}) => {
|
||||||
|
const spaceData = {
|
||||||
|
name: data.name,
|
||||||
|
slug: data.slug,
|
||||||
|
description: data.description,
|
||||||
|
};
|
||||||
|
|
||||||
|
const createdSpace = await createSpaceMutation.mutateAsync(spaceData);
|
||||||
|
navigate(getSpaceUrl(createdSpace.slug));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box maw="500" mx="auto">
|
||||||
|
<form onSubmit={form.onSubmit((values) => handleSubmit(values))}>
|
||||||
|
<Stack>
|
||||||
|
<TextInput
|
||||||
|
withAsterisk
|
||||||
|
id="name"
|
||||||
|
label="Space name"
|
||||||
|
placeholder="e.g Product Team"
|
||||||
|
variant="filled"
|
||||||
|
{...form.getInputProps("name")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
withAsterisk
|
||||||
|
id="slug"
|
||||||
|
label="Space slug"
|
||||||
|
placeholder="e.g product"
|
||||||
|
variant="filled"
|
||||||
|
{...form.getInputProps("slug")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
id="description"
|
||||||
|
label="Space description"
|
||||||
|
placeholder="e.g Space for product team"
|
||||||
|
variant="filled"
|
||||||
|
autosize
|
||||||
|
minRows={2}
|
||||||
|
maxRows={8}
|
||||||
|
{...form.getInputProps("description")}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Group justify="flex-end" mt="md">
|
||||||
|
<Button type="submit">Create</Button>
|
||||||
|
</Group>
|
||||||
|
</form>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
import { Button, Divider, Modal } from "@mantine/core";
|
||||||
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
|
import { CreateSpaceForm } from "@/features/space/components/create-space-form.tsx";
|
||||||
|
|
||||||
|
export default function CreateSpaceModal() {
|
||||||
|
const [opened, { open, close }] = useDisclosure(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button onClick={open}>Create space</Button>
|
||||||
|
|
||||||
|
<Modal opened={opened} onClose={close} title="Create space">
|
||||||
|
<Divider size="xs" mb="xs" />
|
||||||
|
<CreateSpaceForm />
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -54,14 +54,24 @@ export function EditSpaceForm({ space, readOnly }: EditSpaceFormProps) {
|
|||||||
label="Name"
|
label="Name"
|
||||||
placeholder="e.g Sales"
|
placeholder="e.g Sales"
|
||||||
variant="filled"
|
variant="filled"
|
||||||
|
readOnly={readOnly}
|
||||||
{...form.getInputProps("name")}
|
{...form.getInputProps("name")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
id="slug"
|
||||||
|
label="Slug"
|
||||||
|
variant="filled"
|
||||||
|
readOnly
|
||||||
|
value={space.slug}
|
||||||
|
/>
|
||||||
|
|
||||||
<Textarea
|
<Textarea
|
||||||
id="description"
|
id="description"
|
||||||
label="Description"
|
label="Description"
|
||||||
placeholder="e.g Space for sales team to collaborate"
|
placeholder="e.g Space for sales team to collaborate"
|
||||||
variant="filled"
|
variant="filled"
|
||||||
|
readOnly={readOnly}
|
||||||
autosize
|
autosize
|
||||||
minRows={1}
|
minRows={1}
|
||||||
maxRows={3}
|
maxRows={3}
|
||||||
|
|||||||
@ -59,7 +59,6 @@ export function SpaceSidebar() {
|
|||||||
className={classes.section}
|
className={classes.section}
|
||||||
style={{
|
style={{
|
||||||
border: "none",
|
border: "none",
|
||||||
paddingTop: "8px",
|
|
||||||
marginBottom: "0",
|
marginBottom: "0",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -109,6 +108,25 @@ export function SpaceSidebar() {
|
|||||||
<span>Space settings</span>
|
<span>Space settings</span>
|
||||||
</div>
|
</div>
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
|
|
||||||
|
{spaceAbility.can(
|
||||||
|
SpaceCaslAction.Manage,
|
||||||
|
SpaceCaslSubject.Page,
|
||||||
|
) && (
|
||||||
|
<UnstyledButton
|
||||||
|
className={classes.menu}
|
||||||
|
onClick={handleCreatePage}
|
||||||
|
>
|
||||||
|
<div className={classes.menuItemInner}>
|
||||||
|
<IconPlus
|
||||||
|
size={18}
|
||||||
|
className={classes.menuItemIcon}
|
||||||
|
stroke={2}
|
||||||
|
/>
|
||||||
|
<span>New page</span>
|
||||||
|
</div>
|
||||||
|
</UnstyledButton>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import {
|
|||||||
getSpaceMembers,
|
getSpaceMembers,
|
||||||
getSpaces,
|
getSpaces,
|
||||||
removeSpaceMember,
|
removeSpaceMember,
|
||||||
|
createSpace,
|
||||||
updateSpace,
|
updateSpace,
|
||||||
} from "@/features/space/services/space-service.ts";
|
} from "@/features/space/services/space-service.ts";
|
||||||
import { notifications } from "@mantine/notifications";
|
import { notifications } from "@mantine/notifications";
|
||||||
@ -35,18 +36,36 @@ export function useGetSpacesQuery(): UseQueryResult<
|
|||||||
|
|
||||||
export function useSpaceQuery(spaceId: string): UseQueryResult<ISpace, Error> {
|
export function useSpaceQuery(spaceId: string): UseQueryResult<ISpace, Error> {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["space", spaceId],
|
queryKey: ["spaces", spaceId],
|
||||||
queryFn: () => getSpaceById(spaceId),
|
queryFn: () => getSpaceById(spaceId),
|
||||||
enabled: !!spaceId,
|
enabled: !!spaceId,
|
||||||
staleTime: 5 * 60 * 1000,
|
staleTime: 5 * 60 * 1000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useCreateSpaceMutation() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<ISpace, Error, Partial<ISpace>>({
|
||||||
|
mutationFn: (data) => createSpace(data),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["spaces"],
|
||||||
|
});
|
||||||
|
notifications.show({ message: "Space created successfully" });
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
const errorMessage = error["response"]?.data?.message;
|
||||||
|
notifications.show({ message: errorMessage, color: "red" });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function useGetSpaceBySlugQuery(
|
export function useGetSpaceBySlugQuery(
|
||||||
spaceId: string,
|
spaceId: string,
|
||||||
): UseQueryResult<ISpace, Error> {
|
): UseQueryResult<ISpace, Error> {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["space", spaceId],
|
queryKey: ["spaces", spaceId],
|
||||||
queryFn: () => getSpaceById(spaceId),
|
queryFn: () => getSpaceById(spaceId),
|
||||||
enabled: !!spaceId,
|
enabled: !!spaceId,
|
||||||
staleTime: 5 * 60 * 1000,
|
staleTime: 5 * 60 * 1000,
|
||||||
@ -103,11 +122,9 @@ export function useAddSpaceMemberMutation() {
|
|||||||
queryKey: ["spaceMembers", variables.spaceId],
|
queryKey: ["spaceMembers", variables.spaceId],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (error) => {
|
||||||
notifications.show({
|
const errorMessage = error["response"]?.data?.message;
|
||||||
message: "Failed to add space members",
|
notifications.show({ message: errorMessage, color: "red" });
|
||||||
color: "red",
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,11 @@ export async function getSpaceById(spaceId: string): Promise<ISpace> {
|
|||||||
return req.data;
|
return req.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createSpace(data: Partial<ISpace>): Promise<ISpace> {
|
||||||
|
const req = await api.post<ISpace>("/spaces/create", data);
|
||||||
|
return req.data;
|
||||||
|
}
|
||||||
|
|
||||||
export async function updateSpace(data: Partial<ISpace>): Promise<ISpace> {
|
export async function updateSpace(data: Partial<ISpace>): Promise<ISpace> {
|
||||||
const req = await api.post<ISpace>("/spaces/update", data);
|
const req = await api.post<ISpace>("/spaces/update", data);
|
||||||
return req.data;
|
return req.data;
|
||||||
|
|||||||
@ -32,8 +32,6 @@ export default function AccountAvatar() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(currentUser?.user.avatarUrl);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FileButton onChange={handleFileChange} accept="image/png,image/jpeg">
|
<FileButton onChange={handleFileChange} accept="image/png,image/jpeg">
|
||||||
|
|||||||
@ -58,6 +58,7 @@ export default function WorkspaceNameForm() {
|
|||||||
label="Name"
|
label="Name"
|
||||||
placeholder="e.g ACME"
|
placeholder="e.g ACME"
|
||||||
variant="filled"
|
variant="filled"
|
||||||
|
readOnly={!isAdmin}
|
||||||
{...form.getInputProps("name")}
|
{...form.getInputProps("name")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -82,7 +82,7 @@ export function useCreateInvitationMutation() {
|
|||||||
onSuccess: (data, variables) => {
|
onSuccess: (data, variables) => {
|
||||||
notifications.show({ message: "Invitation sent" });
|
notifications.show({ message: "Invitation sent" });
|
||||||
// TODO: mutate cache
|
// TODO: mutate cache
|
||||||
queryClient.invalidateQueries({
|
queryClient.refetchQueries({
|
||||||
queryKey: ["invitations"],
|
queryKey: ["invitations"],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import { UserRole } from "@/lib/types.ts";
|
|
||||||
|
|
||||||
export function formatMemberCount(memberCount: number): string {
|
export function formatMemberCount(memberCount: number): string {
|
||||||
if (memberCount === 1) {
|
if (memberCount === 1) {
|
||||||
return "1 member";
|
return "1 member";
|
||||||
@ -15,3 +13,15 @@ export function extractPageSlugId(input: string): string {
|
|||||||
const parts = input.split("-");
|
const parts = input.split("-");
|
||||||
return parts.length > 1 ? parts[parts.length - 1] : input;
|
return parts.length > 1 ? parts[parts.length - 1] : input;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const computeSpaceSlug = (name: string) => {
|
||||||
|
const alphanumericName = name.replace(/[^a-zA-Z0-9\s]/g, "");
|
||||||
|
if (alphanumericName.includes(" ")) {
|
||||||
|
return alphanumericName
|
||||||
|
.split(" ")
|
||||||
|
.map((word) => word.charAt(0).toUpperCase())
|
||||||
|
.join("");
|
||||||
|
} else {
|
||||||
|
return alphanumericName.toLowerCase();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@ -1,10 +1,20 @@
|
|||||||
import SettingsTitle from "@/components/settings/settings-title.tsx";
|
import SettingsTitle from "@/components/settings/settings-title.tsx";
|
||||||
import SpaceList from "@/features/space/components/space-list.tsx";
|
import SpaceList from "@/features/space/components/space-list.tsx";
|
||||||
|
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||||
|
import { Group } from "@mantine/core";
|
||||||
|
import CreateSpaceModal from "@/features/space/components/create-space-modal.tsx";
|
||||||
|
|
||||||
export default function Spaces() {
|
export default function Spaces() {
|
||||||
|
const { isAdmin } = useUserRole();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingsTitle title="Spaces" />
|
<SettingsTitle title="Spaces" />
|
||||||
|
|
||||||
|
<Group my="md" justify="flex-end">
|
||||||
|
{isAdmin && <CreateSpaceModal />}
|
||||||
|
</Group>
|
||||||
|
|
||||||
<SpaceList />
|
<SpaceList />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { TextAlign } from '@tiptap/extension-text-align';
|
|||||||
import { TaskList } from '@tiptap/extension-task-list';
|
import { TaskList } from '@tiptap/extension-task-list';
|
||||||
import { TaskItem } from '@tiptap/extension-task-item';
|
import { TaskItem } from '@tiptap/extension-task-item';
|
||||||
import { Underline } from '@tiptap/extension-underline';
|
import { Underline } from '@tiptap/extension-underline';
|
||||||
import { Link } from '@tiptap/extension-link';
|
|
||||||
import { Superscript } from '@tiptap/extension-superscript';
|
import { Superscript } from '@tiptap/extension-superscript';
|
||||||
import SubScript from '@tiptap/extension-subscript';
|
import SubScript from '@tiptap/extension-subscript';
|
||||||
import { Highlight } from '@tiptap/extension-highlight';
|
import { Highlight } from '@tiptap/extension-highlight';
|
||||||
@ -17,6 +16,7 @@ import {
|
|||||||
Details,
|
Details,
|
||||||
DetailsContent,
|
DetailsContent,
|
||||||
DetailsSummary,
|
DetailsSummary,
|
||||||
|
LinkExtension,
|
||||||
MathBlock,
|
MathBlock,
|
||||||
MathInline,
|
MathInline,
|
||||||
Table,
|
Table,
|
||||||
@ -37,7 +37,7 @@ export const tiptapExtensions = [
|
|||||||
TaskList,
|
TaskList,
|
||||||
TaskItem,
|
TaskItem,
|
||||||
Underline,
|
Underline,
|
||||||
Link,
|
LinkExtension,
|
||||||
Superscript,
|
Superscript,
|
||||||
SubScript,
|
SubScript,
|
||||||
Highlight,
|
Highlight,
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { IsOptional, IsString, MaxLength, MinLength } from 'class-validator';
|
|||||||
|
|
||||||
export class CreateSpaceDto {
|
export class CreateSpaceDto {
|
||||||
@MinLength(2)
|
@MinLength(2)
|
||||||
@MaxLength(64)
|
@MaxLength(50)
|
||||||
@IsString()
|
@IsString()
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
@ -11,7 +11,7 @@ export class CreateSpaceDto {
|
|||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
@MinLength(2)
|
@MinLength(2)
|
||||||
@MaxLength(64)
|
@MaxLength(50)
|
||||||
@IsString()
|
@IsString()
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,14 +6,54 @@ import {
|
|||||||
import { CreateSpaceDto } from '../dto/create-space.dto';
|
import { CreateSpaceDto } from '../dto/create-space.dto';
|
||||||
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
||||||
import { SpaceRepo } from '@docmost/db/repos/space/space.repo';
|
import { SpaceRepo } from '@docmost/db/repos/space/space.repo';
|
||||||
import { KyselyTransaction } from '@docmost/db/types/kysely.types';
|
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
|
||||||
import { Space } from '@docmost/db/types/entity.types';
|
import { Space, User } from '@docmost/db/types/entity.types';
|
||||||
import { PaginationResult } from '@docmost/db/pagination/pagination';
|
import { PaginationResult } from '@docmost/db/pagination/pagination';
|
||||||
import { UpdateSpaceDto } from '../dto/update-space.dto';
|
import { UpdateSpaceDto } from '../dto/update-space.dto';
|
||||||
|
import { executeTx } from '@docmost/db/utils';
|
||||||
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
|
import { SpaceMemberService } from './space-member.service';
|
||||||
|
import { SpaceRole } from '../../../common/helpers/types/permission';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SpaceService {
|
export class SpaceService {
|
||||||
constructor(private spaceRepo: SpaceRepo) {}
|
constructor(
|
||||||
|
private spaceRepo: SpaceRepo,
|
||||||
|
private spaceMemberService: SpaceMemberService,
|
||||||
|
@InjectKysely() private readonly db: KyselyDB,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async createSpace(
|
||||||
|
authUser: User,
|
||||||
|
workspaceId: string,
|
||||||
|
createSpaceDto: CreateSpaceDto,
|
||||||
|
trx?: KyselyTransaction,
|
||||||
|
): Promise<Space> {
|
||||||
|
let space = null;
|
||||||
|
|
||||||
|
await executeTx(
|
||||||
|
this.db,
|
||||||
|
async (trx) => {
|
||||||
|
space = await this.create(
|
||||||
|
authUser.id,
|
||||||
|
workspaceId,
|
||||||
|
createSpaceDto,
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.spaceMemberService.addUserToSpace(
|
||||||
|
authUser.id,
|
||||||
|
space.id,
|
||||||
|
SpaceRole.ADMIN,
|
||||||
|
workspaceId,
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { ...space, memberCount: 1 };
|
||||||
|
}
|
||||||
|
|
||||||
async create(
|
async create(
|
||||||
userId: string,
|
userId: string,
|
||||||
@ -28,7 +68,7 @@ export class SpaceService {
|
|||||||
);
|
);
|
||||||
if (slugExists) {
|
if (slugExists) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
'Slug exists. Please use a unique space slug',
|
'Space slug exists. Please use a unique space slug',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -28,6 +28,12 @@ import {
|
|||||||
import { UpdateSpaceDto } from './dto/update-space.dto';
|
import { UpdateSpaceDto } from './dto/update-space.dto';
|
||||||
import { findHighestUserSpaceRole } from '@docmost/db/repos/space/utils';
|
import { findHighestUserSpaceRole } from '@docmost/db/repos/space/utils';
|
||||||
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
|
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
|
||||||
|
import {
|
||||||
|
WorkspaceCaslAction,
|
||||||
|
WorkspaceCaslSubject,
|
||||||
|
} from '../casl/interfaces/workspace-ability.type';
|
||||||
|
import WorkspaceAbilityFactory from '../casl/abilities/workspace-ability.factory';
|
||||||
|
import { CreateSpaceDto } from './dto/create-space.dto';
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Controller('spaces')
|
@Controller('spaces')
|
||||||
@ -37,6 +43,7 @@ export class SpaceController {
|
|||||||
private readonly spaceMemberService: SpaceMemberService,
|
private readonly spaceMemberService: SpaceMemberService,
|
||||||
private readonly spaceMemberRepo: SpaceMemberRepo,
|
private readonly spaceMemberRepo: SpaceMemberRepo,
|
||||||
private readonly spaceAbility: SpaceAbilityFactory,
|
private readonly spaceAbility: SpaceAbilityFactory,
|
||||||
|
private readonly workspaceAbility: WorkspaceAbilityFactory,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ -86,6 +93,22 @@ export class SpaceController {
|
|||||||
return { ...space, membership };
|
return { ...space, membership };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Post('create')
|
||||||
|
createGroup(
|
||||||
|
@Body() createSpaceDto: CreateSpaceDto,
|
||||||
|
@AuthUser() user: User,
|
||||||
|
@AuthWorkspace() workspace: Workspace,
|
||||||
|
) {
|
||||||
|
const ability = this.workspaceAbility.createForUser(user, workspace);
|
||||||
|
if (
|
||||||
|
ability.cannot(WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Space)
|
||||||
|
) {
|
||||||
|
throw new ForbiddenException();
|
||||||
|
}
|
||||||
|
return this.spaceService.createSpace(user, workspace.id, createSpaceDto);
|
||||||
|
}
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('update')
|
@Post('update')
|
||||||
async updateGroup(
|
async updateGroup(
|
||||||
|
|||||||
@ -33,7 +33,7 @@ export class SpaceRepo {
|
|||||||
if (isValidUUID(spaceId)) {
|
if (isValidUUID(spaceId)) {
|
||||||
query = query.where('id', '=', spaceId);
|
query = query.where('id', '=', spaceId);
|
||||||
} else {
|
} else {
|
||||||
query = query.where('slug', '=', spaceId);
|
query = query.where(sql`LOWER(slug)`, '=', sql`LOWER(${spaceId})`);
|
||||||
}
|
}
|
||||||
return query.executeTakeFirst();
|
return query.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user