Allow creation of space

* other fixes and cleanups
This commit is contained in:
Philipinho
2024-06-24 20:06:57 +01:00
parent 562abb0413
commit f2a193ac8d
22 changed files with 289 additions and 32 deletions

View File

@ -2,7 +2,8 @@
<html lang="en">
<head>
<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" />
<title>Docmost</title>
</head>

Binary file not shown.

After

Width:  |  Height:  |  Size: 562 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -64,7 +64,7 @@ export default function RecentChanges({ spaceId }: Props) {
</Table>
) : (
<Text size="md" ta="center">
No records to show
No pages yet
</Text>
);
}

View File

@ -29,10 +29,11 @@ export default function TopMenu() {
<Menu.Target>
<UnstyledButton>
<Group gap={7} wrap={"nowrap"}>
<Avatar
src={workspace.logo}
alt={workspace.name}
<UserAvatar
avatarUrl={workspace.logo}
name={workspace.name}
radius="xl"
color="blue"
size={20}
/>
<Text fw={500} size="sm" lh={1} mr={3}>

View File

@ -1,22 +1,17 @@
import * as React from "react";
import * as z from "zod";
import { useForm, zodResolver } from "@mantine/form";
import useAuth from "@/features/auth/hooks/use-auth";
import { ILogin } from "@/features/auth/types/auth.types";
import {
Container,
Title,
Anchor,
TextInput,
Button,
Text,
PasswordInput,
Box,
} from "@mantine/core";
import { Link, useNavigate } from "react-router-dom";
import classes from "./auth.module.css";
import { useEffect, useState } from "react";
import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts";
const formSchema = z.object({

View 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>
</>
);
}

View File

@ -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>
</>
);
}

View File

@ -54,14 +54,24 @@ export function EditSpaceForm({ space, readOnly }: EditSpaceFormProps) {
label="Name"
placeholder="e.g Sales"
variant="filled"
readOnly={readOnly}
{...form.getInputProps("name")}
/>
<TextInput
id="slug"
label="Slug"
variant="filled"
readOnly
value={space.slug}
/>
<Textarea
id="description"
label="Description"
placeholder="e.g Space for sales team to collaborate"
variant="filled"
readOnly={readOnly}
autosize
minRows={1}
maxRows={3}

View File

@ -59,7 +59,6 @@ export function SpaceSidebar() {
className={classes.section}
style={{
border: "none",
paddingTop: "8px",
marginBottom: "0",
}}
>
@ -109,6 +108,25 @@ export function SpaceSidebar() {
<span>Space settings</span>
</div>
</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>

View File

@ -18,6 +18,7 @@ import {
getSpaceMembers,
getSpaces,
removeSpaceMember,
createSpace,
updateSpace,
} from "@/features/space/services/space-service.ts";
import { notifications } from "@mantine/notifications";
@ -35,18 +36,36 @@ export function useGetSpacesQuery(): UseQueryResult<
export function useSpaceQuery(spaceId: string): UseQueryResult<ISpace, Error> {
return useQuery({
queryKey: ["space", spaceId],
queryKey: ["spaces", spaceId],
queryFn: () => getSpaceById(spaceId),
enabled: !!spaceId,
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(
spaceId: string,
): UseQueryResult<ISpace, Error> {
return useQuery({
queryKey: ["space", spaceId],
queryKey: ["spaces", spaceId],
queryFn: () => getSpaceById(spaceId),
enabled: !!spaceId,
staleTime: 5 * 60 * 1000,
@ -103,11 +122,9 @@ export function useAddSpaceMemberMutation() {
queryKey: ["spaceMembers", variables.spaceId],
});
},
onError: () => {
notifications.show({
message: "Failed to add space members",
color: "red",
});
onError: (error) => {
const errorMessage = error["response"]?.data?.message;
notifications.show({ message: errorMessage, color: "red" });
},
});
}

View File

@ -18,6 +18,11 @@ export async function getSpaceById(spaceId: string): Promise<ISpace> {
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> {
const req = await api.post<ISpace>("/spaces/update", data);
return req.data;

View File

@ -32,8 +32,6 @@ export default function AccountAvatar() {
}
};
console.log(currentUser?.user.avatarUrl);
return (
<>
<FileButton onChange={handleFileChange} accept="image/png,image/jpeg">

View File

@ -58,6 +58,7 @@ export default function WorkspaceNameForm() {
label="Name"
placeholder="e.g ACME"
variant="filled"
readOnly={!isAdmin}
{...form.getInputProps("name")}
/>

View File

@ -82,7 +82,7 @@ export function useCreateInvitationMutation() {
onSuccess: (data, variables) => {
notifications.show({ message: "Invitation sent" });
// TODO: mutate cache
queryClient.invalidateQueries({
queryClient.refetchQueries({
queryKey: ["invitations"],
});
},

View File

@ -1,5 +1,3 @@
import { UserRole } from "@/lib/types.ts";
export function formatMemberCount(memberCount: number): string {
if (memberCount === 1) {
return "1 member";
@ -15,3 +13,15 @@ export function extractPageSlugId(input: string): string {
const parts = input.split("-");
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();
}
};

View File

@ -1,10 +1,20 @@
import SettingsTitle from "@/components/settings/settings-title.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() {
const { isAdmin } = useUserRole();
return (
<>
<SettingsTitle title="Spaces" />
<Group my="md" justify="flex-end">
{isAdmin && <CreateSpaceModal />}
</Group>
<SpaceList />
</>
);