fix: bug fixes (#397)

* Add more html page titles

* Make tables responsive

* fix react query keys

* Add tooltip to sidebar toggle

* fix: trim inputs

* fix inputs
This commit is contained in:
Philip Okugbe
2024-10-13 17:09:45 +01:00
committed by GitHub
parent fa3c8a03e1
commit 36e720920b
38 changed files with 681 additions and 612 deletions

View File

@ -4,7 +4,6 @@ import {
UnstyledButton,
Badge,
Table,
ScrollArea,
ActionIcon,
} from '@mantine/core';
import {Link} from 'react-router-dom';
@ -18,6 +17,7 @@ import { getSpaceUrl } from '@/lib/config.ts';
interface Props {
spaceId?: string;
}
export default function RecentChanges({spaceId}: Props) {
const {data: pages, isLoading, isError} = useRecentChangesQuery(spaceId);
@ -30,7 +30,7 @@ export default function RecentChanges({ spaceId }: Props) {
}
return pages && pages.items.length > 0 ? (
<ScrollArea>
<Table.ScrollContainer minWidth={500}>
<Table highlightOnHover verticalSpacing="sm">
<Table.Tbody>
{pages.items.map((page) => (
@ -67,7 +67,7 @@ export default function RecentChanges({ spaceId }: Props) {
</Table.Td>
)}
<Table.Td>
<Text c="dimmed" size="xs" fw={500}>
<Text c="dimmed" style={{whiteSpace: 'nowrap'}} size="xs" fw={500}>
{formattedDate(page.updatedAt)}
</Text>
</Table.Td>
@ -75,7 +75,7 @@ export default function RecentChanges({ spaceId }: Props) {
))}
</Table.Tbody>
</Table>
</ScrollArea>
</Table.ScrollContainer>
) : (
<Text size="md" ta="center">
No pages yet

View File

@ -1,4 +1,4 @@
import { Group, Text } from "@mantine/core";
import {Group, Text, Tooltip} from "@mantine/core";
import classes from "./app-header.module.css";
import React from "react";
import TopMenu from "@/components/layouts/global/top-menu.tsx";
@ -35,21 +35,26 @@ export function AppHeader() {
<Group wrap="nowrap">
{!isHomeRoute && (
<>
<Tooltip label="Sidebar toggle">
<SidebarToggle
aria-label="sidebar toggle"
aria-label="Sidebar toggle"
opened={mobileOpened}
onClick={toggleMobile}
hiddenFrom="sm"
size="sm"
/>
</Tooltip>
<Tooltip label="Sidebar toggle">
<SidebarToggle
aria-label="sidebar toggle"
aria-label="Sidebar toggle"
opened={desktopOpened}
onClick={toggleDesktop}
visibleFrom="sm"
size="sm"
/>
</Tooltip>
</>
)}

View File

@ -1,15 +1,9 @@
import React from "react";
import {
IconLayoutSidebarRightCollapse,
IconLayoutSidebarRightExpand,
IconLayoutSidebarRightExpand
} from "@tabler/icons-react";
import {
ActionIcon,
BoxProps,
ElementProps,
MantineColor,
MantineSize,
} from "@mantine/core";
import React from "react";
import { ActionIcon, BoxProps, ElementProps, MantineColor, MantineSize } from "@mantine/core";
export interface SidebarToggleProps extends BoxProps, ElementProps<"button"> {
size?: MantineSize | `compact-${MantineSize}` | (string & {});
@ -17,13 +11,10 @@ export interface SidebarToggleProps extends BoxProps, ElementProps<"button"> {
opened?: boolean;
}
export default function SidebarToggle({
opened,
size = "sm",
...others
}: SidebarToggleProps) {
const SidebarToggle = React.forwardRef<HTMLButtonElement, SidebarToggleProps>(
({ opened, size = "sm", ...others }, ref) => {
return (
<ActionIcon size={size} {...others} variant="subtle" color="gray">
<ActionIcon size={size} {...others} variant="subtle" color="gray" ref={ref}>
{opened ? (
<IconLayoutSidebarRightExpand />
) : (
@ -32,3 +23,6 @@ export default function SidebarToggle({
</ActionIcon>
);
}
);
export default SidebarToggle;

View File

@ -19,7 +19,7 @@ import { useGetInvitationQuery } from "@/features/workspace/queries/workspace-qu
import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts";
const formSchema = z.object({
name: z.string().min(2),
name: z.string().trim().min(1),
password: z.string().min(8),
});

View File

@ -15,8 +15,8 @@ import useAuth from "@/features/auth/hooks/use-auth";
import classes from "@/features/auth/components/auth.module.css";
const formSchema = z.object({
workspaceName: z.string().min(2).max(60),
name: z.string().min(2).max(60),
workspaceName: z.string().trim().min(3).max(50),
name: z.string().min(1).max(50),
email: z
.string()
.min(1, { message: "email is required" })

View File

@ -7,7 +7,7 @@ import { useNavigate } from "react-router-dom";
import { MultiUserSelect } from "@/features/group/components/multi-user-select.tsx";
const formSchema = z.object({
name: z.string().min(2).max(50),
name: z.string().trim().min(2).max(50),
description: z.string().max(500),
});

View File

@ -10,6 +10,7 @@ export default function GroupList() {
return (
<>
{data && (
<Table.ScrollContainer minWidth={400}>
<Table highlightOnHover verticalSpacing="sm">
<Table.Thead>
<Table.Tr>
@ -32,13 +33,13 @@ export default function GroupList() {
component={Link}
to={`/settings/groups/${group.id}`}
>
<Group gap="sm">
<Group gap="sm" wrap="nowrap">
<IconGroupCircle/>
<div>
<Text fz="sm" fw={500}>
<Text fz="sm" fw={500} lineClamp={1}>
{group.name}
</Text>
<Text fz="xs" c="dimmed">
<Text fz="xs" c="dimmed" lineClamp={2}>
{group.description}
</Text>
</div>
@ -53,6 +54,7 @@ export default function GroupList() {
style={{
cursor: "pointer",
color: "var(--mantine-color-text)",
whiteSpace: "nowrap"
}}
component={Link}
to={`/settings/groups/${group.id}`}
@ -64,6 +66,7 @@ export default function GroupList() {
))}
</Table.Tbody>
</Table>
</Table.ScrollContainer>
)}
</>
);

View File

@ -42,6 +42,7 @@ export default function GroupMembersList() {
return (
<>
{data && (
<Table.ScrollContainer minWidth={500}>
<Table verticalSpacing="sm">
<Table.Thead>
<Table.Tr>
@ -100,6 +101,7 @@ export default function GroupMembersList() {
))}
</Table.Tbody>
</Table>
</Table.ScrollContainer>
)}
</>
);

View File

@ -29,24 +29,22 @@ export function useGetGroupsQuery(
export function useGroupQuery(groupId: string): UseQueryResult<IGroup, Error> {
return useQuery({
queryKey: ['groups', groupId],
queryKey: ['group', groupId],
queryFn: () => getGroupById(groupId),
enabled: !!groupId,
});
}
export function useGroupMembersQuery(groupId: string) {
return useQuery({
queryKey: ['groupMembers', groupId],
queryFn: () => getGroupMembers(groupId),
enabled: !!groupId,
});
}
export function useCreateGroupMutation() {
const queryClient = useQueryClient();
return useMutation<IGroup, Error, Partial<IGroup>>({
mutationFn: (data) => createGroup(data),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['groups'],
});
notifications.show({ message: 'Group created successfully' });
},
onError: () => {
@ -96,6 +94,14 @@ export function useDeleteGroupMutation() {
});
}
export function useGroupMembersQuery(groupId: string) {
return useQuery({
queryKey: ['groupMembers', groupId],
queryFn: () => getGroupMembers(groupId),
enabled: !!groupId,
});
}
export function useAddGroupMemberMutation() {
const queryClient = useQueryClient();

View File

@ -8,9 +8,10 @@ import { computeSpaceSlug } from "@/lib";
import { getSpaceUrl } from "@/lib/config.ts";
const formSchema = z.object({
name: z.string().min(2).max(50),
name: z.string().trim().min(2).max(50),
slug: z
.string()
.trim()
.min(2)
.max(50)
.regex(

View File

@ -1,4 +1,4 @@
import { Modal, Tabs, rem, Group, ScrollArea } from "@mantine/core";
import {Modal, Tabs, rem, Group, ScrollArea, Text} 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, {useMemo} from "react";
@ -40,11 +40,13 @@ export default function SpaceSettingsModal({
<Modal.Overlay/>
<Modal.Content style={{overflow: "hidden"}}>
<Modal.Header py={0}>
<Modal.Title fw={500}>{space?.name}</Modal.Title>
<Modal.Title>
<Text fw={500} lineClamp={1}>{space?.name}</Text>
</Modal.Title>
<Modal.CloseButton/>
</Modal.Header>
<Modal.Body>
<div style={{ height: rem("600px") }}>
<div style={{height: rem(600)}}>
<Tabs defaultValue="members">
<Tabs.List>
<Tabs.Tab fw={500} value="general">
@ -55,7 +57,6 @@ export default function SpaceSettingsModal({
</Tabs.Tab>
</Tabs.List>
<ScrollArea h="600" w="100%" scrollbarSize={5}>
<Tabs.Panel value="general">
<SpaceDetails
spaceId={space?.id}
@ -82,7 +83,6 @@ export default function SpaceSettingsModal({
)}
/>
</Tabs.Panel>
</ScrollArea>
</Tabs>
</div>
</Modal.Body>

View File

@ -18,6 +18,7 @@ export default function SpaceList() {
return (
<>
{data && (
<Table.ScrollContainer minWidth={400}>
<Table highlightOnHover verticalSpacing="sm">
<Table.Thead>
<Table.Tr>
@ -34,28 +35,31 @@ export default function SpaceList() {
onClick={() => handleClick(space.id)}
>
<Table.Td>
<Group gap="sm">
<Group gap="sm" wrap="nowrap">
<Avatar
color="initials"
variant="filled"
name={space.name}
/>
<div>
<Text fz="sm" fw={500}>
<Text fz="sm" fw={500} lineClamp={1}>
{space.name}
</Text>
<Text fz="xs" c="dimmed">
<Text fz="xs" c="dimmed" lineClamp={2}>
{space.description}
</Text>
</div>
</Group>
</Table.Td>
<Table.Td>{formatMemberCount(space.memberCount)}</Table.Td>
<Table.Td>
<Text size="sm" style={{whiteSpace: 'nowrap'}}>{formatMemberCount(space.memberCount)}</Text>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Table.ScrollContainer>
)}
{selectedSpaceId && (

View File

@ -18,10 +18,12 @@ import {
import {formatMemberCount} from "@/lib";
type MemberType = "user" | "group";
interface SpaceMembersProps {
spaceId: string;
readOnly?: boolean;
}
export default function SpaceMembersList({
spaceId,
readOnly,
@ -93,6 +95,7 @@ export default function SpaceMembersList({
return (
<>
{data && (
<Table.ScrollContainer minWidth={500}>
<Table verticalSpacing={8}>
<Table.Thead>
<Table.Tr>
@ -178,6 +181,7 @@ export default function SpaceMembersList({
))}
</Table.Tbody>
</Table>
</Table.ScrollContainer>
)}
</>
);

View File

@ -36,7 +36,7 @@ export function useGetSpacesQuery(
export function useSpaceQuery(spaceId: string): UseQueryResult<ISpace, Error> {
return useQuery({
queryKey: ['spaces', spaceId],
queryKey: ['space', spaceId],
queryFn: () => getSpaceById(spaceId),
enabled: !!spaceId,
staleTime: 5 * 60 * 1000,
@ -65,7 +65,7 @@ export function useGetSpaceBySlugQuery(
spaceId: string
): UseQueryResult<ISpace, Error> {
return useQuery({
queryKey: ['spaces', spaceId],
queryKey: ['space', spaceId],
queryFn: () => getSpaceById(spaceId),
enabled: !!spaceId,
staleTime: 5 * 60 * 1000,
@ -111,7 +111,7 @@ export function useDeleteSpaceMutation() {
if (variables.slug) {
queryClient.removeQueries({
queryKey: ['spaces', variables.slug],
queryKey: ['space', variables.slug],
exact: true,
});
}

View File

@ -4,7 +4,7 @@ import React from "react";
import {getUserRoleLabel} from "@/features/workspace/types/user-role-data.ts";
import InviteActionMenu from "@/features/workspace/components/members/components/invite-action-menu.tsx";
import {IconInfoCircle} from "@tabler/icons-react";
import { formattedDate } from "@/lib/time.ts";
import {formattedDate, timeAgo} from "@/lib/time.ts";
import useUserRole from "@/hooks/use-user-role.tsx";
export default function WorkspaceInvitesTable() {
@ -21,6 +21,7 @@ export default function WorkspaceInvitesTable() {
{data && (
<>
<Table.ScrollContainer minWidth={500}>
<Table verticalSpacing="sm">
<Table.Thead>
<Table.Tr>
@ -46,7 +47,7 @@ export default function WorkspaceInvitesTable() {
<Table.Td>{getUserRoleLabel(invitation.role)}</Table.Td>
<Table.Td>{formattedDate(invitation.createdAt)}</Table.Td>
<Table.Td>{timeAgo(invitation.createdAt)}</Table.Td>
<Table.Td>
{isAdmin && (
@ -57,6 +58,7 @@ export default function WorkspaceInvitesTable() {
))}
</Table.Tbody>
</Table>
</Table.ScrollContainer>
</>
)}
</>

View File

@ -40,6 +40,7 @@ export default function WorkspaceMembersTable() {
return (
<>
{data && (
<Table.ScrollContainer minWidth={500}>
<Table verticalSpacing="sm">
<Table.Thead>
<Table.Tr>
@ -84,6 +85,7 @@ export default function WorkspaceMembersTable() {
))}
</Table.Tbody>
</Table>
</Table.ScrollContainer>
)}
</>
);

View File

@ -6,6 +6,10 @@ declare global {
}
}
export function getAppName(): string{
return 'Docmost';
}
export function getAppUrl(): string {
//let appUrl = window.CONFIG?.APP_URL || process.env.APP_URL;

View File

@ -1,11 +1,12 @@
import { ForgotPasswordForm } from "@/features/auth/components/forgot-password-form";
import { getAppName } from "@/lib/config";
import { Helmet } from "react-helmet-async";
export default function ForgotPassword() {
return (
<>
<Helmet>
<title>Forgot Password - Docmost</title>
<title>Forgot Password - {getAppName()}</title>
</Helmet>
<ForgotPasswordForm />
</>

View File

@ -1,11 +1,12 @@
import { Helmet } from "react-helmet-async";
import { InviteSignUpForm } from "@/features/auth/components/invite-sign-up-form.tsx";
import {getAppName} from "@/lib/config.ts";
export default function InviteSignup() {
return (
<>
<Helmet>
<title>Invitation Signup - Docmost</title>
<title>Invitation Signuo - {getAppName()}</title>
</Helmet>
<InviteSignUpForm />
</>

View File

@ -1,11 +1,12 @@
import { LoginForm } from "@/features/auth/components/login-form";
import { Helmet } from "react-helmet-async";
import {getAppName} from "@/lib/config.ts";
export default function LoginPage() {
return (
<>
<Helmet>
<title>Login - Docmost</title>
<title>Login - {getAppName()}</title>
</Helmet>
<LoginForm />
</>

View File

@ -4,6 +4,7 @@ import { Link, useSearchParams } from "react-router-dom";
import { useVerifyUserTokenQuery } from "@/features/auth/queries/auth-query";
import { Button, Container, Group, Text } from "@mantine/core";
import APP_ROUTE from "@/lib/app-route";
import {getAppName} from "@/lib/config.ts";
export default function PasswordReset() {
const [searchParams] = useSearchParams();
@ -21,7 +22,7 @@ export default function PasswordReset() {
return (
<>
<Helmet>
<title>Password Reset - Docmost</title>
<title>Password Reset - {getAppName()}</title>
</Helmet>
<Container my={40}>
<Text size="lg" ta="center">
@ -45,7 +46,7 @@ export default function PasswordReset() {
return (
<>
<Helmet>
<title>Password Reset - Docmost</title>
<title>Password Reset - {getAppName()}</title>
</Helmet>
<PasswordResetForm resetToken={resetToken} />
</>

View File

@ -3,6 +3,7 @@ import { SetupWorkspaceForm } from "@/features/auth/components/setup-workspace-f
import { Helmet } from "react-helmet-async";
import React, { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import {getAppName} from "@/lib/config.ts";
export default function SetupWorkspace() {
const {
@ -32,7 +33,7 @@ export default function SetupWorkspace() {
return (
<>
<Helmet>
<title>Setup Workspace - Docmost</title>
<title>Setup Workspace - {getAppName()}</title>
</Helmet>
<SetupWorkspaceForm />
</>

View File

@ -1,9 +1,15 @@
import {Container, Space} from "@mantine/core";
import HomeTabs from "@/features/home/components/home-tabs";
import SpaceGrid from "@/features/space/components/space-grid.tsx";
import {getAppName} from "@/lib/config.ts";
import {Helmet} from "react-helmet-async";
export default function Home() {
return (
<>
<Helmet>
<title>Home - {getAppName()}</title>
</Helmet>
<Container size={"800"} pt="xl">
<SpaceGrid/>
@ -11,5 +17,6 @@ export default function Home() {
<HomeTabs/>
</Container>
</>
);
}

View File

@ -2,10 +2,15 @@ import SettingsTitle from "@/components/settings/settings-title.tsx";
import AccountTheme from "@/features/user/components/account-theme.tsx";
import PageWidthPref from "@/features/user/components/page-width-pref.tsx";
import {Divider} from "@mantine/core";
import {getAppName} from "@/lib/config.ts";
import {Helmet} from "react-helmet-async";
export default function AccountPreferences() {
return (
<>
<Helmet>
<title>Preferences - {getAppName()}</title>
</Helmet>
<SettingsTitle title="Preferences"/>
<AccountTheme/>
<Divider my={"md"}/>

View File

@ -4,10 +4,15 @@ import ChangePassword from "@/features/user/components/change-password";
import { Divider } from "@mantine/core";
import AccountAvatar from "@/features/user/components/account-avatar";
import SettingsTitle from "@/components/settings/settings-title.tsx";
import {getAppName} from "@/lib/config.ts";
import {Helmet} from "react-helmet-async";
export default function AccountSettings() {
return (
<>
<Helmet>
<title>My Profile - {getAppName()}</title>
</Helmet>
<SettingsTitle title="My Profile" />
<AccountAvatar />

View File

@ -1,10 +1,15 @@
import SettingsTitle from "@/components/settings/settings-title.tsx";
import GroupMembersList from "@/features/group/components/group-members";
import GroupDetails from "@/features/group/components/group-details";
import {getAppName} from "@/lib/config.ts";
import {Helmet} from "react-helmet-async";
export default function GroupInfo() {
return (
<>
<Helmet>
<title>Manage Group - {getAppName()}</title>
</Helmet>
<SettingsTitle title="Manage Group"/>
<GroupDetails/>
<GroupMembersList/>

View File

@ -3,12 +3,17 @@ import SettingsTitle from "@/components/settings/settings-title.tsx";
import { Group } from "@mantine/core";
import CreateGroupModal from "@/features/group/components/create-group-modal";
import useUserRole from "@/hooks/use-user-role.tsx";
import {getAppName} from "@/lib/config.ts";
import {Helmet} from "react-helmet-async";
export default function Groups() {
const { isAdmin } = useUserRole();
return (
<>
<Helmet>
<title>Groups - {getAppName()}</title>
</Helmet>
<SettingsTitle title="Groups" />
<Group my="md" justify="flex-end">

View File

@ -3,12 +3,17 @@ 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";
import {getAppName} from "@/lib/config.ts";
import {Helmet} from "react-helmet-async";
export default function Spaces() {
const {isAdmin} = useUserRole();
return (
<>
<Helmet>
<title>Spaces - {getAppName()}</title>
</Helmet>
<SettingsTitle title="Spaces"/>
<Group my="md" justify="flex-end">

View File

@ -6,6 +6,8 @@ import { useEffect, useState } from "react";
import {useNavigate, useSearchParams} from "react-router-dom";
import WorkspaceInvitesTable from "@/features/workspace/components/members/components/workspace-invites-table.tsx";
import useUserRole from "@/hooks/use-user-role.tsx";
import {getAppName} from "@/lib/config.ts";
import {Helmet} from "react-helmet-async";
export default function WorkspaceMembers() {
const [segmentValue, setSegmentValue] = useState("members");
@ -31,6 +33,9 @@ export default function WorkspaceMembers() {
return (
<>
<Helmet>
<title>Members - {getAppName()}</title>
</Helmet>
<SettingsTitle title="Members"/>
{/* <WorkspaceInviteSection /> */}

View File

@ -1,9 +1,14 @@
import SettingsTitle from "@/components/settings/settings-title.tsx";
import WorkspaceNameForm from "@/features/workspace/components/settings/components/workspace-name-form";
import {getAppName} from "@/lib/config.ts";
import {Helmet} from "react-helmet-async";
export default function WorkspaceSettings() {
return (
<>
<Helmet>
<title>Workspace Settings - {getAppName()}</title>
</Helmet>
<SettingsTitle title="General"/>
<WorkspaceNameForm/>
</>

View File

@ -2,14 +2,21 @@ import { Container } from "@mantine/core";
import SpaceHomeTabs from "@/features/space/components/space-home-tabs.tsx";
import {useParams} from "react-router-dom";
import {useGetSpaceBySlugQuery} from "@/features/space/queries/space-query.ts";
import {getAppName} from "@/lib/config.ts";
import {Helmet} from "react-helmet-async";
export default function SpaceHome() {
const {spaceSlug} = useParams();
const {data: space} = useGetSpaceBySlugQuery(spaceSlug);
return (
<>
<Helmet>
<title>{space?.name || 'Overview'} - {getAppName()}</title>
</Helmet>
<Container size={"800"} pt="xl">
{space && <SpaceHomeTabs/>}
</Container>
</>
);
}

View File

@ -1,23 +0,0 @@
import { Title, Text, Stack } from '@mantine/core';
import { ThemeToggle } from '@/components/theme-toggle';
export function Welcome() {
return (
<Stack>
<Title ta="center" mt={100}>
<Text
inherit
variant="gradient"
component="span"
gradient={{ from: 'pink', to: 'yellow' }}
>
Welcome
</Text>
</Title>
<Text ta="center" size="lg" maw={580} mx="auto" mt="xl">
Welcome to something new and interesting.
</Text>
<ThemeToggle />
</Stack>
);
}

View File

@ -1,15 +1,18 @@
import { IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator';
import { CreateUserDto } from './create-user.dto';
import {Transform, TransformFnParams} from "class-transformer";
export class CreateAdminUserDto extends CreateUserDto {
@IsNotEmpty()
@MinLength(3)
@MaxLength(35)
@MinLength(1)
@MaxLength(50)
@Transform(({ value }: TransformFnParams) => value?.trim())
name: string;
@IsNotEmpty()
@MinLength(4)
@MaxLength(35)
@MinLength(3)
@MaxLength(50)
@IsString()
@Transform(({ value }: TransformFnParams) => value?.trim())
workspaceName: string;
}

View File

@ -6,12 +6,14 @@ import {
MaxLength,
MinLength,
} from 'class-validator';
import {Transform, TransformFnParams} from "class-transformer";
export class CreateUserDto {
@IsOptional()
@MinLength(2)
@MaxLength(60)
@MinLength(1)
@MaxLength(50)
@IsString()
@Transform(({ value }: TransformFnParams) => value?.trim())
name: string;
@IsNotEmpty()

View File

@ -1,4 +1,4 @@
import { IsString, MinLength } from 'class-validator';
import { IsString } from 'class-validator';
export class VerifyUserTokenDto {
@IsString()

View File

@ -7,11 +7,13 @@ import {
MaxLength,
MinLength,
} from 'class-validator';
import {Transform, TransformFnParams} from "class-transformer";
export class CreateGroupDto {
@MinLength(2)
@MaxLength(50)
@IsString()
@Transform(({ value }: TransformFnParams) => value?.trim())
name: string;
@IsOptional()

View File

@ -5,11 +5,13 @@ import {
MaxLength,
MinLength,
} from 'class-validator';
import {Transform, TransformFnParams} from "class-transformer";
export class CreateSpaceDto {
@MinLength(2)
@MaxLength(50)
@IsString()
@Transform(({ value }: TransformFnParams) => value?.trim())
name: string;
@IsOptional()

View File

@ -1,15 +1,17 @@
import { IsOptional, IsString, MaxLength, MinLength } from 'class-validator';
import {IsAlphanumeric, IsOptional, IsString, MaxLength, MinLength} from 'class-validator';
import {Transform, TransformFnParams} from "class-transformer";
export class CreateWorkspaceDto {
@MinLength(4)
@MaxLength(64)
@IsString()
@Transform(({ value }: TransformFnParams) => value?.trim())
name: string;
@IsOptional()
@MinLength(4)
@MaxLength(30)
@IsString()
@IsAlphanumeric()
hostname?: string;
@IsOptional()