Merge branch 'main' into i18n

This commit is contained in:
Philip Okugbe
2024-10-15 13:09:33 +01:00
committed by GitHub
61 changed files with 1163 additions and 1019 deletions

View File

@@ -21,6 +21,9 @@ AWS_S3_BUCKET=
AWS_S3_ENDPOINT=
AWS_S3_FORCE_PATH_STYLE=
# default: 50mb
FILE_UPLOAD_SIZE_LIMIT=
# options: smtp | postmark
MAIL_DRIVER=smtp
MAIL_FROM_ADDRESS=hello@example.com
@@ -32,6 +35,7 @@ SMTP_PORT=587
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_SECURE=false
SMTP_IGNORETLS=false
# Postmark driver config
POSTMARK_TOKEN=

View File

@@ -30,6 +30,9 @@ COPY --from=builder /app/packages/editor-ext/package.json /app/packages/editor-e
COPY --from=builder /app/package.json /app/package.json
COPY --from=builder /app/pnpm*.yaml /app/
# Copy patches
COPY --from=builder /app/patches /app/patches
RUN npm install -g pnpm
RUN chown -R node:node /app

View File

@@ -1,7 +1,7 @@
{
"name": "client",
"private": true,
"version": "0.3.1",
"version": "0.4.1",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
@@ -9,6 +9,7 @@
"preview": "vite preview"
},
"dependencies": {
"@docmost/editor-ext": "workspace:*",
"@casl/ability": "^6.7.1",
"@casl/react": "^4.0.0",
"@emoji-mart/data": "^1.2.1",
@@ -70,6 +71,6 @@
"postcss-simple-vars": "^7.0.1",
"prettier": "^3.3.3",
"typescript": "^5.5.4",
"vite": "^5.4.2"
"vite": "^5.4.8"
}
}

View File

@@ -4,10 +4,9 @@ import {
UnstyledButton,
Badge,
Table,
ScrollArea,
ActionIcon,
} from '@mantine/core';
import { Link } from 'react-router-dom';
import {Link} from 'react-router-dom';
import PageListSkeleton from '@/components/ui/page-list-skeleton.tsx';
import { buildPageUrl } from '@/features/page/page.utils.ts';
import { formattedDate } from '@/lib/time.ts';
@@ -19,13 +18,13 @@ import { useTranslation } from "react-i18next";
interface Props {
spaceId?: string;
}
export default function RecentChanges({ spaceId }: Props) {
const { t } = useTranslation();
const { data: pages, isLoading, isError } = useRecentChangesQuery(spaceId);
export default function RecentChanges({spaceId}: Props) {
const { t } = useTranslation();
const {data: pages, isLoading, isError} = useRecentChangesQuery(spaceId);
if (isLoading) {
return <PageListSkeleton />;
return <PageListSkeleton/>;
}
if (isError) {
@@ -33,7 +32,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) => (
@@ -46,7 +45,7 @@ export default function RecentChanges({ spaceId }: Props) {
<Group wrap="nowrap">
{page.icon || (
<ActionIcon variant='transparent' color='gray' size={18}>
<IconFileDescription size={18} />
<IconFileDescription size={18}/>
</ActionIcon>
)}
@@ -63,14 +62,14 @@ export default function RecentChanges({ spaceId }: Props) {
variant="light"
component={Link}
to={getSpaceUrl(page?.space.slug)}
style={{ cursor: 'pointer' }}
style={{cursor: 'pointer'}}
>
{page?.space.name}
</Badge>
</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>
@@ -78,7 +77,7 @@ export default function RecentChanges({ spaceId }: Props) {
))}
</Table.Tbody>
</Table>
</ScrollArea>
</Table.ScrollContainer>
) : (
<Text size="md" ta="center">
{t("No pages yet")}

View File

@@ -1,19 +1,19 @@
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";
import { Link } from "react-router-dom";
import {Link} from "react-router-dom";
import APP_ROUTE from "@/lib/app-route.ts";
import { useAtom } from "jotai/index";
import {useAtom} from "jotai/index";
import {
desktopSidebarAtom,
mobileSidebarAtom,
} from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
import { useToggleSidebar } from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts";
import {useToggleSidebar} from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts";
import SidebarToggle from "@/components/ui/sidebar-toggle-button.tsx";
import { useTranslation } from "react-i18next";
const links = [{ link: APP_ROUTE.HOME, label: "Home" }];
const links = [{link: APP_ROUTE.HOME, label: "Home"}];
export function AppHeader() {
const { t } = useTranslation();
@@ -37,28 +37,33 @@ export function AppHeader() {
<Group wrap="nowrap">
{!isHomeRoute && (
<>
<SidebarToggle
aria-label="sidebar toggle"
opened={mobileOpened}
onClick={toggleMobile}
hiddenFrom="sm"
size="sm"
/>
<Tooltip label="Sidebar toggle">
<SidebarToggle
aria-label="sidebar toggle"
opened={desktopOpened}
onClick={toggleDesktop}
visibleFrom="sm"
size="sm"
/>
<SidebarToggle
aria-label="Sidebar toggle"
opened={mobileOpened}
onClick={toggleMobile}
hiddenFrom="sm"
size="sm"
/>
</Tooltip>
<Tooltip label="Sidebar toggle">
<SidebarToggle
aria-label="Sidebar toggle"
opened={desktopOpened}
onClick={toggleDesktop}
visibleFrom="sm"
size="sm"
/>
</Tooltip>
</>
)}
<Text
size="lg"
fw={600}
style={{ cursor: "pointer", userSelect: "none" }}
style={{cursor: "pointer", userSelect: "none"}}
component={Link}
to="/home"
>
@@ -71,7 +76,7 @@ export function AppHeader() {
</Group>
<Group px={"xl"}>
<TopMenu />
<TopMenu/>
</Group>
</Group>
</>

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,18 +11,18 @@ export interface SidebarToggleProps extends BoxProps, ElementProps<"button"> {
opened?: boolean;
}
export default function SidebarToggle({
opened,
size = "sm",
...others
}: SidebarToggleProps) {
return (
<ActionIcon size={size} {...others} variant="subtle" color="gray">
{opened ? (
<IconLayoutSidebarRightExpand />
) : (
<IconLayoutSidebarRightCollapse />
)}
</ActionIcon>
);
}
const SidebarToggle = React.forwardRef<HTMLButtonElement, SidebarToggleProps>(
({ opened, size = "sm", ...others }, ref) => {
return (
<ActionIcon size={size} {...others} variant="subtle" color="gray" ref={ref}>
{opened ? (
<IconLayoutSidebarRightExpand />
) : (
<IconLayoutSidebarRightCollapse />
)}
</ActionIcon>
);
}
);
export default SidebarToggle;

View File

@@ -20,7 +20,7 @@ import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-i
import { useTranslation } from "react-i18next";
const formSchema = z.object({
name: z.string().min(2),
name: z.string().trim().min(1),
password: z.string().min(8),
});

View File

@@ -16,8 +16,8 @@ import classes from "@/features/auth/components/auth.module.css";
import { useTranslation } from "react-i18next";
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

@@ -1,6 +1,8 @@
import { handleAttachmentUpload } from "@docmost/editor-ext";
import { uploadFile } from "@/features/page/services/page-service.ts";
import { notifications } from "@mantine/notifications";
import {getFileUploadSizeLimit} from "@/lib/config.ts";
import {formatBytes} from "@/lib";
export const uploadAttachmentAction = handleAttachmentUpload({
onUpload: async (file: File, pageId: string): Promise<any> => {
@@ -18,10 +20,10 @@ export const uploadAttachmentAction = handleAttachmentUpload({
if (file.type.includes("image/") || file.type.includes("video/")) {
return false;
}
if (file.size / 1024 / 1024 > 50) {
if (file.size > getFileUploadSizeLimit()) {
notifications.show({
color: "red",
message: `File exceeds the 50 MB attachment limit`,
message: `File exceeds the ${formatBytes(getFileUploadSizeLimit())} attachment limit`,
});
return false;
}

View File

@@ -1,6 +1,8 @@
import { handleImageUpload } from "@docmost/editor-ext";
import { uploadFile } from "@/features/page/services/page-service.ts";
import { notifications } from "@mantine/notifications";
import {getFileUploadSizeLimit} from "@/lib/config.ts";
import { formatBytes } from "@/lib";
export const uploadImageAction = handleImageUpload({
onUpload: async (file: File, pageId: string): Promise<any> => {
@@ -18,10 +20,10 @@ export const uploadImageAction = handleImageUpload({
if (!file.type.includes("image/")) {
return false;
}
if (file.size / 1024 / 1024 > 50) {
if (file.size > getFileUploadSizeLimit()) {
notifications.show({
color: "red",
message: `File exceeds the 50 MB attachment limit`,
message: `File exceeds the ${formatBytes(getFileUploadSizeLimit())} attachment limit`,
});
return false;
}

View File

@@ -17,10 +17,6 @@
color: light-dark(var(--mantine-color-red-8), var(--mantine-color-red-7));
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-gray-8));
}
&:not(.error, .empty) * {
font-family: KaTeX_Main, Times New Roman, serif;
}
}
.mathBlock {
@@ -33,7 +29,7 @@
border-radius: 4px;
transition: background-color 0.2s;
margin: 0 0.1rem;
overflow-x: scroll;
overflow-x: auto;
.textInput {
width: 400px;
@@ -52,10 +48,4 @@
color: light-dark(var(--mantine-color-red-8), var(--mantine-color-red-7));
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-gray-8));
}
&:not(.error, .empty) * {
font-family: KaTeX_Main, Times New Roman, serif;
}
}

View File

@@ -1,6 +1,8 @@
import { handleVideoUpload } from "@docmost/editor-ext";
import { uploadFile } from "@/features/page/services/page-service.ts";
import { notifications } from "@mantine/notifications";
import {getFileUploadSizeLimit} from "@/lib/config.ts";
import {formatBytes} from "@/lib";
export const uploadVideoAction = handleVideoUpload({
onUpload: async (file: File, pageId: string): Promise<any> => {
@@ -19,13 +21,14 @@ export const uploadVideoAction = handleVideoUpload({
return false;
}
if (file.size / 1024 / 1024 > 50) {
if (file.size > getFileUploadSizeLimit()) {
notifications.show({
color: "red",
message: `File exceeds the 50 MB attachment limit`,
message: `File exceeds the ${formatBytes(getFileUploadSizeLimit())} attachment limit`,
});
return false;
}
return true;
},
});

View File

@@ -8,7 +8,7 @@ import { MultiUserSelect } from "@/features/group/components/multi-user-select.t
import { useTranslation } from "react-i18next";
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

@@ -81,7 +81,7 @@ export function EditGroupForm({ onClose }: EditGroupFormProps) {
</Stack>
<Group justify="flex-end" mt="md">
<Button type="submit">{t("Edit")}</Button>
<Button type="submit">{t("Save")}</Button>
</Group>
</form>
</Box>

View File

@@ -1,5 +1,5 @@
import { Table, Group, Text, Anchor } from "@mantine/core";
import { useGetGroupsQuery } from "@/features/group/queries/group-query";
import {Table, Group, Text, Anchor} from "@mantine/core";
import {useGetGroupsQuery} from "@/features/group/queries/group-query";
import React from "react";
import { Link } from "react-router-dom";
import { IconGroupCircle } from "@/components/icons/icon-people-circle.tsx";
@@ -13,60 +13,62 @@ export default function GroupList() {
return (
<>
{data && (
<Table highlightOnHover verticalSpacing="sm">
<Table.Thead>
<Table.Tr>
<Table.Th>{t("Group")}</Table.Th>
<Table.Th>{t("Members")}</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{data?.items.map((group, index) => (
<Table.Tr key={index}>
<Table.Td>
<Anchor
size="sm"
underline="never"
style={{
cursor: "pointer",
color: "var(--mantine-color-text)",
}}
component={Link}
to={`/settings/groups/${group.id}`}
>
<Group gap="sm">
<IconGroupCircle />
<div>
<Text fz="sm" fw={500}>
{group.name}
</Text>
<Text fz="xs" c="dimmed">
{group.description}
</Text>
</div>
</Group>
</Anchor>
</Table.Td>
<Table.Td>
<Anchor
size="sm"
underline="never"
style={{
cursor: "pointer",
color: "var(--mantine-color-text)",
}}
component={Link}
to={`/settings/groups/${group.id}`}
>
{formatMemberCount(group.memberCount, t)}
</Anchor>
</Table.Td>
<Table.ScrollContainer minWidth={400}>
<Table highlightOnHover verticalSpacing="sm">
<Table.Thead>
<Table.Tr>
<Table.Th>{t("Group")}</Table.Th>
<Table.Th>{t("Members")}</Table.Th>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Table.Thead>
<Table.Tbody>
{data?.items.map((group, index) => (
<Table.Tr key={index}>
<Table.Td>
<Anchor
size="sm"
underline="never"
style={{
cursor: "pointer",
color: "var(--mantine-color-text)",
}}
component={Link}
to={`/settings/groups/${group.id}`}
>
<Group gap="sm" wrap="nowrap">
<IconGroupCircle/>
<div>
<Text fz="sm" fw={500} lineClamp={1}>
{group.name}
</Text>
<Text fz="xs" c="dimmed" lineClamp={2}>
{group.description}
</Text>
</div>
</Group>
</Anchor>
</Table.Td>
<Table.Td>
<Anchor
size="sm"
underline="never"
style={{
cursor: "pointer",
color: "var(--mantine-color-text)",
whiteSpace: "nowrap"
}}
component={Link}
to={`/settings/groups/${group.id}`}
>
{group.memberCount} members
</Anchor>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Table.ScrollContainer>
)}
</>
);

View File

@@ -1,13 +1,13 @@
import { Group, Table, Text, Badge, Menu, ActionIcon } from "@mantine/core";
import {Group, Table, Text, Badge, Menu, ActionIcon} from "@mantine/core";
import {
useGroupMembersQuery,
useRemoveGroupMemberMutation,
} from "@/features/group/queries/group-query";
import { useParams } from "react-router-dom";
import {useParams} from "react-router-dom";
import React from "react";
import { IconDots } from "@tabler/icons-react";
import { modals } from "@mantine/modals";
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
import {IconDots} from "@tabler/icons-react";
import {modals} from "@mantine/modals";
import {CustomAvatar} from "@/components/ui/custom-avatar.tsx";
import useUserRole from "@/hooks/use-user-role.tsx";
import { useTranslation } from "react-i18next";
@@ -16,7 +16,7 @@ export default function GroupMembersList() {
const { groupId } = useParams();
const { data, isLoading } = useGroupMembersQuery(groupId);
const removeGroupMember = useRemoveGroupMemberMutation();
const { isAdmin } = useUserRole();
const {isAdmin} = useUserRole();
const onRemove = async (userId: string) => {
const memberToRemove = {
@@ -45,64 +45,63 @@ export default function GroupMembersList() {
return (
<>
{data && (
<Table verticalSpacing="sm">
<Table.Thead>
<Table.Tr>
<Table.Th>{t("User")}</Table.Th>
<Table.Th>{t("Status")}</Table.Th>
<Table.Th></Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{data?.items.map((user, index) => (
<Table.Tr key={index}>
<Table.Td>
<Group gap="sm">
<CustomAvatar avatarUrl={user.avatarUrl} name={user.name} />
<div>
<Text fz="sm" fw={500}>
{user.name}
</Text>
<Text fz="xs" c="dimmed">
{user.email}
</Text>
</div>
</Group>
</Table.Td>
<Table.Td>
<Badge variant="light">{t("Active")}</Badge>
</Table.Td>
<Table.Td>
{isAdmin && (
<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(user.id)}>
{t("Remove group member")}
</Menu.Item>
</Menu.Dropdown>
</Menu>
)}
</Table.Td>
<Table.ScrollContainer minWidth={500}>
<Table verticalSpacing="sm">
<Table.Thead>
<Table.Tr>
<Table.Th>{t("User")}</Table.Th>
<Table.Th>{t("Status")}</Table.Th>
<Table.Th></Table.Th>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Table.Thead>
<Table.Tbody>
{data?.items.map((user, index) => (
<Table.Tr key={index}>
<Table.Td>
<Group gap="sm">
<CustomAvatar avatarUrl={user.avatarUrl} name={user.name}/>
<div>
<Text fz="sm" fw={500}>
{user.name}
</Text>
<Text fz="xs" c="dimmed">
{user.email}
</Text>
</div>
</Group>
</Table.Td>
<Table.Td>
<Badge variant="light">{t("Active")}</Badge>
</Table.Td>
<Table.Td>
{isAdmin && (
<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(user.id)}>
{t("Remove group member")}
</Menu.Item>
</Menu.Dropdown>
</Menu>
)}
</Table.Td>
</Table.Tr>
))}
</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

@@ -9,9 +9,10 @@ import { getSpaceUrl } from "@/lib/config.ts";
import { useTranslation } from "react-i18next";
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,10 +1,10 @@
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";
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 {useSpaceQuery} from "@/features/space/queries/space-query.ts";
import {useSpaceAbility} from "@/features/space/permissions/use-space-ability.ts";
import {
SpaceCaslAction,
SpaceCaslSubject,
@@ -39,14 +39,16 @@ export default function SpaceSettingsModal({
xOffset={0}
mah={400}
>
<Modal.Overlay />
<Modal.Content style={{ overflow: "hidden" }}>
<Modal.Overlay/>
<Modal.Content style={{overflow: "hidden"}}>
<Modal.Header py={0}>
<Modal.Title fw={500}>{space?.name}</Modal.Title>
<Modal.CloseButton />
<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">
@@ -57,34 +59,32 @@ export default function SpaceSettingsModal({
</Tabs.Tab>
</Tabs.List>
<ScrollArea h="600" w="100%" scrollbarSize={5}>
<Tabs.Panel value="general">
<SpaceDetails
spaceId={space?.id}
readOnly={spaceAbility.cannot(
SpaceCaslAction.Manage,
SpaceCaslSubject.Settings,
)}
/>
</Tabs.Panel>
<Tabs.Panel value="general">
<SpaceDetails
spaceId={space?.id}
readOnly={spaceAbility.cannot(
SpaceCaslAction.Manage,
SpaceCaslSubject.Settings,
)}
/>
</Tabs.Panel>
<Tabs.Panel value="members">
<Group my="md" justify="flex-end">
{spaceAbility.can(
SpaceCaslAction.Manage,
SpaceCaslSubject.Member,
) && <AddSpaceMembersModal spaceId={space?.id} />}
</Group>
<Tabs.Panel value="members">
<Group my="md" justify="flex-end">
{spaceAbility.can(
SpaceCaslAction.Manage,
SpaceCaslSubject.Member,
) && <AddSpaceMembersModal spaceId={space?.id}/>}
</Group>
<SpaceMembersList
spaceId={space?.id}
readOnly={spaceAbility.cannot(
SpaceCaslAction.Manage,
SpaceCaslSubject.Member,
)}
/>
</Tabs.Panel>
</ScrollArea>
<SpaceMembersList
spaceId={space?.id}
readOnly={spaceAbility.cannot(
SpaceCaslAction.Manage,
SpaceCaslSubject.Member,
)}
/>
</Tabs.Panel>
</Tabs>
</div>
</Modal.Body>

View File

@@ -1,6 +1,6 @@
import { Table, Group, Text, Avatar } from "@mantine/core";
import React, { useState } from "react";
import { useGetSpacesQuery } from "@/features/space/queries/space-query.ts";
import {Table, Group, Text, Avatar} from "@mantine/core";
import React, {useState} from "react";
import {useGetSpacesQuery} from "@/features/space/queries/space-query.ts";
import SpaceSettingsModal from "@/features/space/components/settings-modal.tsx";
import { useDisclosure } from "@mantine/hooks";
import { formatMemberCount } from "@/lib";
@@ -20,44 +20,47 @@ export default function SpaceList() {
return (
<>
{data && (
<Table highlightOnHover verticalSpacing="sm">
<Table.Thead>
<Table.Tr>
<Table.Th>{t("Space")}</Table.Th>
<Table.Th>{t("Members")}</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{data?.items.map((space, index) => (
<Table.Tr
key={index}
style={{ cursor: "pointer" }}
onClick={() => handleClick(space.id)}
>
<Table.Td>
<Group gap="sm">
<Avatar
color="initials"
variant="filled"
name={space.name}
/>
<div>
<Text fz="sm" fw={500}>
{space.name}
</Text>
<Text fz="xs" c="dimmed">
{space.description}
</Text>
</div>
</Group>
</Table.Td>
<Table.Td>{formatMemberCount(space.memberCount, t)}</Table.Td>
<Table.ScrollContainer minWidth={400}>
<Table highlightOnHover verticalSpacing="sm">
<Table.Thead>
<Table.Tr>
<Table.Th>{t("Space")}</Table.Th>
<Table.Th>{t("Members")}</Table.Th>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Table.Thead>
<Table.Tbody>
{data?.items.map((space, index) => (
<Table.Tr
key={index}
style={{cursor: "pointer"}}
onClick={() => handleClick(space.id)}
>
<Table.Td>
<Group gap="sm" wrap="nowrap">
<Avatar
color="initials"
variant="filled"
name={space.name}
/>
<div>
<Text fz="sm" fw={500} lineClamp={1}>
{space.name}
</Text>
<Text fz="xs" c="dimmed" lineClamp={2}>
{space.description}
</Text>
</div>
</Group>
</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

@@ -1,15 +1,15 @@
import { Group, Table, Text, Menu, ActionIcon } from "@mantine/core";
import {Group, Table, Text, Menu, ActionIcon} from "@mantine/core";
import React from "react";
import { IconDots } from "@tabler/icons-react";
import { modals } from "@mantine/modals";
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
import {IconDots} from "@tabler/icons-react";
import {modals} from "@mantine/modals";
import {CustomAvatar} from "@/components/ui/custom-avatar.tsx";
import {
useChangeSpaceMemberRoleMutation,
useRemoveSpaceMemberMutation,
useSpaceMembersQuery,
} from "@/features/space/queries/space-query.ts";
import { IconGroupCircle } from "@/components/icons/icon-people-circle.tsx";
import { IRemoveSpaceMember } from "@/features/space/types/space.types.ts";
import {IconGroupCircle} from "@/components/icons/icon-people-circle.tsx";
import {IRemoveSpaceMember} from "@/features/space/types/space.types.ts";
import RoleSelectMenu from "@/components/ui/role-select-menu.tsx";
import {
getSpaceRoleLabel,
@@ -19,16 +19,19 @@ import { formatMemberCount } from "@/lib";
import { useTranslation } from "react-i18next";
type MemberType = "user" | "group";
interface SpaceMembersProps {
spaceId: string;
readOnly?: boolean;
}
export default function SpaceMembersList({
spaceId,
readOnly,
}: SpaceMembersProps) {
const { t } = useTranslation();
const { data, isLoading } = useSpaceMembersQuery(spaceId);
const removeSpaceMember = useRemoveSpaceMemberMutation();
const changeSpaceMemberRoleMutation = useChangeSpaceMemberRoleMutation();
@@ -96,91 +99,93 @@ export default function SpaceMembersList({
return (
<>
{data && (
<Table verticalSpacing={8}>
<Table.Thead>
<Table.Tr>
<Table.Th>{t("Member")}</Table.Th>
<Table.Th>{t("Role")}</Table.Th>
<Table.Th></Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{data?.items.map((member, index) => (
<Table.Tr key={index}>
<Table.Td>
<Group gap="sm">
{member.type === "user" && (
<CustomAvatar
avatarUrl={member?.avatarUrl}
name={member.name}
/>
)}
{member.type === "group" && <IconGroupCircle />}
<div>
<Text fz="sm" fw={500}>
{member?.name}
</Text>
<Text fz="xs" c="dimmed">
{member.type == "user" && member?.email}
{member.type == "group" &&
`Group - ${formatMemberCount(member?.memberCount, t)}`}
</Text>
</div>
</Group>
</Table.Td>
<Table.Td>
<RoleSelectMenu
roles={spaceRoleData}
roleName={getSpaceRoleLabel(member.role)}
onChange={(newRole) =>
handleRoleChange(
member.id,
member.type,
newRole,
member.role,
)
}
disabled={readOnly}
/>
</Table.Td>
<Table.Td>
{!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)
}
>
{t("Remove space member")}
</Menu.Item>
</Menu.Dropdown>
</Menu>
)}
</Table.Td>
<Table.ScrollContainer minWidth={500}>
<Table verticalSpacing={8}>
<Table.Thead>
<Table.Tr>
<Table.Th>{t("Member")}</Table.Th>
<Table.Th>{t("Role")}</Table.Th>
<Table.Th></Table.Th>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Table.Thead>
<Table.Tbody>
{data?.items.map((member, index) => (
<Table.Tr key={index}>
<Table.Td>
<Group gap="sm">
{member.type === "user" && (
<CustomAvatar
avatarUrl={member?.avatarUrl}
name={member.name}
/>
)}
{member.type === "group" && <IconGroupCircle/>}
<div>
<Text fz="sm" fw={500}>
{member?.name}
</Text>
<Text fz="xs" c="dimmed">
{member.type == "user" && member?.email}
{member.type == "group" &&
`Group - ${formatMemberCount(member?.memberCount)}`}
</Text>
</div>
</Group>
</Table.Td>
<Table.Td>
<RoleSelectMenu
roles={spaceRoleData}
roleName={getSpaceRoleLabel(member.role)}
onChange={(newRole) =>
handleRoleChange(
member.id,
member.type,
newRole,
member.role,
)
}
disabled={readOnly}
/>
</Table.Td>
<Table.Td>
{!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)
}
>
{t("Remove space member")}
</Menu.Item>
</Menu.Dropdown>
</Menu>
)}
</Table.Td>
</Table.Tr>
))}
</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

@@ -1,10 +1,10 @@
import { Group, Table, Avatar, Text, Alert } from "@mantine/core";
import { useWorkspaceInvitationsQuery } from "@/features/workspace/queries/workspace-query.ts";
import {Group, Table, Avatar, Text, Alert} from "@mantine/core";
import {useWorkspaceInvitationsQuery} from "@/features/workspace/queries/workspace-query.ts";
import React from "react";
import { getUserRoleLabel } from "@/features/workspace/types/user-role-data.ts";
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 {IconInfoCircle} from "@tabler/icons-react";
import {formattedDate, timeAgo} from "@/lib/time.ts";
import useUserRole from "@/hooks/use-user-role.tsx";
import { useTranslation } from "react-i18next";
@@ -13,7 +13,7 @@ export default function WorkspaceInvitesTable() {
const { data, isLoading } = useWorkspaceInvitationsQuery({
limit: 100,
});
const { isAdmin } = useUserRole();
const {isAdmin} = useUserRole();
return (
<>
@@ -25,42 +25,44 @@ export default function WorkspaceInvitesTable() {
{data && (
<>
<Table verticalSpacing="sm">
<Table.Thead>
<Table.Tr>
<Table.Th>{t("Email")}</Table.Th>
<Table.Th>{t("Role")}</Table.Th>
<Table.Th>{t("Date")}</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{data?.items.map((invitation, index) => (
<Table.Tr key={index}>
<Table.Td>
<Group gap="sm">
<Avatar name={invitation.email} color="initials" />
<div>
<Text fz="sm" fw={500}>
{invitation.email}
</Text>
</div>
</Group>
</Table.Td>
<Table.Td>{getUserRoleLabel(invitation.role)}</Table.Td>
<Table.Td>{formattedDate(invitation.createdAt)}</Table.Td>
<Table.Td>
{isAdmin && (
<InviteActionMenu invitationId={invitation.id} />
)}
</Table.Td>
<Table.ScrollContainer minWidth={500}>
<Table verticalSpacing="sm">
<Table.Thead>
<Table.Tr>
<Table.Th>{t("Email")}</Table.Th>
<Table.Th>{t("Role")}</Table.Th>
<Table.Th>{t("Date")}</Table.Th>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Table.Thead>
<Table.Tbody>
{data?.items.map((invitation, index) => (
<Table.Tr key={index}>
<Table.Td>
<Group gap="sm">
<Avatar name={invitation.email} color="initials"/>
<div>
<Text fz="sm" fw={500}>
{invitation.email}
</Text>
</div>
</Group>
</Table.Td>
<Table.Td>{getUserRoleLabel(invitation.role)}</Table.Td>
<Table.Td>{timeAgo(invitation.createdAt)}</Table.Td>
<Table.Td>
{isAdmin && (
<InviteActionMenu invitationId={invitation.id}/>
)}
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Table.ScrollContainer>
</>
)}
</>

View File

@@ -1,9 +1,9 @@
import { Group, Table, Text, Badge } from "@mantine/core";
import {Group, Table, Text, Badge} from "@mantine/core";
import {
useChangeMemberRoleMutation,
useWorkspaceMembersQuery,
} from "@/features/workspace/queries/workspace-query.ts";
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
import {CustomAvatar} from "@/components/ui/custom-avatar.tsx";
import React from "react";
import RoleSelectMenu from "@/components/ui/role-select-menu.tsx";
import {
@@ -18,7 +18,7 @@ export default function WorkspaceMembersTable() {
const { t } = useTranslation();
const { data, isLoading } = useWorkspaceMembersQuery({ limit: 100 });
const changeMemberRoleMutation = useChangeMemberRoleMutation();
const { isAdmin, isOwner } = useUserRole();
const {isAdmin, isOwner} = useUserRole();
const assignableUserRoles = isOwner
? userRoleData
@@ -44,50 +44,50 @@ export default function WorkspaceMembersTable() {
return (
<>
{data && (
<Table verticalSpacing="sm">
<Table.Thead>
<Table.Tr>
<Table.Th>{t("User")}</Table.Th>
<Table.Th>{t("Status")}</Table.Th>
<Table.Th>{t("Role")}</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{data?.items.map((user, index) => (
<Table.Tr key={index}>
<Table.Td>
<Group gap="sm">
<CustomAvatar avatarUrl={user.avatarUrl} name={user.name} />
<div>
<Text fz="sm" fw={500}>
{user.name}
</Text>
<Text fz="xs" c="dimmed">
{user.email}
</Text>
</div>
</Group>
</Table.Td>
<Table.Td>
<Badge variant="light">{t("Active")}</Badge>
</Table.Td>
<Table.Td>
<RoleSelectMenu
roles={assignableUserRoles}
roleName={getUserRoleLabel(user.role)}
onChange={(newRole) =>
handleRoleChange(user.id, user.role, newRole)
}
disabled={!isAdmin}
/>
</Table.Td>
<Table.ScrollContainer minWidth={500}>
<Table verticalSpacing="sm">
<Table.Thead>
<Table.Tr>
<Table.Th>{t("User")}</Table.Th>
<Table.Th>{t("Status")}</Table.Th>
<Table.Th>{t("Role")}</Table.Th>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Table.Thead>
<Table.Tbody>
{data?.items.map((user, index) => (
<Table.Tr key={index}>
<Table.Td>
<Group gap="sm">
<CustomAvatar avatarUrl={user.avatarUrl} name={user.name}/>
<div>
<Text fz="sm" fw={500}>
{user.name}
</Text>
<Text fz="xs" c="dimmed">
{user.email}
</Text>
</div>
</Group>
</Table.Td>
<Table.Td>
<Badge variant="light">{t("Active")}</Badge>
</Table.Td>
<Table.Td>
<RoleSelectMenu
roles={assignableUserRoles}
roleName={getUserRoleLabel(user.role)}
onChange={(newRole) =>
handleRoleChange(user.id, user.role, newRole)
}
disabled={!isAdmin}
/>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Table.ScrollContainer>
)}
</>
);

View File

@@ -1,51 +1,62 @@
import bytes from "bytes";
declare global {
interface Window {
CONFIG?: Record<string, string>;
}
interface Window {
CONFIG?: Record<string, string>;
}
}
export function getAppName(): string{
return 'Docmost';
}
export function getAppUrl(): string {
//let appUrl = window.CONFIG?.APP_URL || process.env.APP_URL;
//let appUrl = window.CONFIG?.APP_URL || process.env.APP_URL;
// if (import.meta.env.DEV) {
// return appUrl || "http://localhost:3000";
//}
// if (import.meta.env.DEV) {
// return appUrl || "http://localhost:3000";
//}
return `${window.location.protocol}//${window.location.host}`;
return `${window.location.protocol}//${window.location.host}`;
}
export function getBackendUrl(): string {
return getAppUrl() + '/api';
return getAppUrl() + '/api';
}
export function getCollaborationUrl(): string {
const COLLAB_PATH = '/collab';
const COLLAB_PATH = '/collab';
let url = getAppUrl();
if (import.meta.env.DEV) {
url = process.env.APP_URL;
}
let url = getAppUrl();
if (import.meta.env.DEV) {
url = process.env.APP_URL;
}
const wsProtocol = url.startsWith('https') ? 'wss' : 'ws';
return `${wsProtocol}://${url.split('://')[1]}${COLLAB_PATH}`;
const wsProtocol = url.startsWith('https') ? 'wss' : 'ws';
return `${wsProtocol}://${url.split('://')[1]}${COLLAB_PATH}`;
}
export function getAvatarUrl(avatarUrl: string) {
if (!avatarUrl) {
return null;
}
if (!avatarUrl) {
return null;
}
if (avatarUrl?.startsWith('http')) {
return avatarUrl;
}
if (avatarUrl?.startsWith('http')) {
return avatarUrl;
}
return getBackendUrl() + '/attachments/img/avatar/' + avatarUrl;
return getBackendUrl() + '/attachments/img/avatar/' + avatarUrl;
}
export function getSpaceUrl(spaceSlug: string) {
return '/s/' + spaceSlug;
return '/s/' + spaceSlug;
}
export function getFileUrl(src: string) {
return src?.startsWith('/files/') ? getBackendUrl() + src : src;
return src?.startsWith('/files/') ? getBackendUrl() + src : src;
}
export function getFileUploadSizeLimit() {
const limit = window.CONFIG?.FILE_UPLOAD_SIZE_LIMIT || process?.env.FILE_UPLOAD_SIZE_LIMIT || '50mb';
return bytes(limit);
}

View File

@@ -55,11 +55,21 @@ export async function svgStringToFile(
return new File([blob], fileName, { type: "image/svg+xml" });
}
// Convert a string holding Base64 encoded UTF-8 data into a proper UTF-8 encoded string
// as a replacement for `atob`.
// based on: https://developer.mozilla.org/en-US/docs/Glossary/Base64
function decodeBase64(base64: string): string {
// convert string to bytes
const bytes = Uint8Array.from(atob(base64), (m) => m.codePointAt(0));
// properly decode bytes to UTF-8 encoded string
return new TextDecoder().decode(bytes);
}
export function decodeBase64ToSvgString(base64Data: string): string {
const base64Prefix = 'data:image/svg+xml;base64,';
if (base64Data.startsWith(base64Prefix)) {
base64Data = base64Data.replace(base64Prefix, '');
}
return atob(base64Data);
return decodeBase64(base64Data);
}

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,5 +1,6 @@
import { Helmet } from "react-helmet-async";
import { InviteSignUpForm } from "@/features/auth/components/invite-sign-up-form.tsx";
import {getAppName} from "@/lib/config.ts";
import { useTranslation } from "react-i18next";
export default function InviteSignup() {
@@ -8,7 +9,7 @@ export default function InviteSignup() {
return (
<>
<Helmet>
<title>{t("Invitation signup")} - Docmost</title>
<title>{t("Invitation Signup")} - {getAppName()}</title>
</Helmet>
<InviteSignUpForm />
</>

View File

@@ -1,5 +1,6 @@
import { LoginForm } from "@/features/auth/components/login-form";
import { Helmet } from "react-helmet-async";
import {getAppName} from "@/lib/config.ts";
import { useTranslation } from "react-i18next";
export default function LoginPage() {
@@ -8,7 +9,7 @@ export default function LoginPage() {
return (
<>
<Helmet>
<title>{t("Login")} - Docmost</title>
<title>{t("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";
import { useTranslation } from "react-i18next";
export default function SetupWorkspace() {
@@ -34,7 +35,7 @@ export default function SetupWorkspace() {
return (
<>
<Helmet>
<title>{t("Setup workspace")} - Docmost</title>
<title>{t("Setup Workspace")} - {getAppName()}</title>
</Helmet>
<SetupWorkspaceForm />
</>

View File

@@ -1,15 +1,22 @@
import { Container, Space } from "@mantine/core";
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 (
<Container size={"800"} pt="xl">
<SpaceGrid />
return (
<>
<Helmet>
<title>Home - {getAppName()}</title>
</Helmet>
<Container size={"800"} pt="xl">
<SpaceGrid/>
<Space h="xl" />
<Space h="xl"/>
<HomeTabs />
</Container>
);
<HomeTabs/>
</Container>
</>
);
}

View File

@@ -2,20 +2,24 @@ import SettingsTitle from "@/components/settings/settings-title.tsx";
import AccountLanguage from "@/features/user/components/account-languate";
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 {Divider} from "@mantine/core";
import {getAppName} from "@/lib/config.ts";
import {Helmet} from "react-helmet-async";
import { useTranslation } from "react-i18next";
export default function AccountPreferences() {
const { t } = useTranslation();
return (
<>
<SettingsTitle title={t("Preferences")} />
<AccountTheme />
<Divider my={"md"} />
<AccountLanguage />
<Divider my={"md"} />
<PageWidthPref />
</>
);
export default function AccountPreferences() {
const { t } = useTranslation();
return (
<>
<Helmet>
<title>{t("Preferences")} - {getAppName()}</title>
</Helmet>
<SettingsTitle title={t("Preferences")}/>
<AccountTheme/>
<Divider my={"md"}/>
<PageWidthPref/>
</>
);
}

View File

@@ -4,6 +4,8 @@ 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";
import { useTranslation } from "react-i18next";
export default function AccountSettings() {
@@ -11,6 +13,9 @@ export default function AccountSettings() {
return (
<>
<Helmet>
<title>{t("My Profile")} - {getAppName()}</title>
</Helmet>
<SettingsTitle title={t("My Profile")} />
<AccountAvatar />

View File

@@ -1,16 +1,19 @@
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";
import { useTranslation } from "react-i18next";
export default function GroupInfo() {
const { t } = useTranslation();
return (
<>
<SettingsTitle title={t("Manage Group")} />
<GroupDetails />
<GroupMembersList />
</>
);
return (
<>
<Helmet>
<title>{t("Manage Group")} - {getAppName()}</title>
</Helmet>
<SettingsTitle title={t("Manage Group")} />
<GroupDetails/>
<GroupMembersList/>
</>
);
}

View File

@@ -3,6 +3,8 @@ 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";
import { useTranslation } from "react-i18next";
export default function Groups() {
@@ -11,6 +13,9 @@ export default function Groups() {
return (
<>
<Helmet>
<title>{t("Groups")} - {getAppName()}</title>
</Helmet>
<SettingsTitle title={t("Groups")} />
<Group my="md" justify="flex-end">

View File

@@ -1,23 +1,29 @@
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 {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";
import {getAppName} from "@/lib/config.ts";
import { useTranslation } from "react-i18next";
export default function Spaces() {
const { t } = useTranslation();
const { isAdmin } = useUserRole();
const { t } = useTranslation();
const {isAdmin} = useUserRole();
return (
<>
<SettingsTitle title={t("Spaces")} />
return (
<>
<Helmet>
<title>{t("Spaces")} - {getAppName()}</title>
</Helmet>
<SettingsTitle title={t("Spaces")} />
<Group my="md" justify="flex-end">
{isAdmin && <CreateSpaceModal />}
</Group>
<Group my="md" justify="flex-end">
{isAdmin && <CreateSpaceModal/>}
</Group>
<SpaceList />
</>
);
<SpaceList/>
</>
);
}

View File

@@ -1,64 +1,69 @@
import WorkspaceInviteModal from "@/features/workspace/components/members/components/workspace-invite-modal";
import { Group, SegmentedControl, Space, Text } from "@mantine/core";
import {Group, SegmentedControl, Space, Text} from "@mantine/core";
import WorkspaceMembersTable from "@/features/workspace/components/members/components/workspace-members-table";
import SettingsTitle from "@/components/settings/settings-title.tsx";
import { useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
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";
import { useTranslation } from "react-i18next";
export default function WorkspaceMembers() {
const { t } = useTranslation();
const [segmentValue, setSegmentValue] = useState("members");
const [searchParams] = useSearchParams();
const { isAdmin } = useUserRole();
const navigate = useNavigate();
const [segmentValue, setSegmentValue] = useState("members");
const [searchParams] = useSearchParams();
const {isAdmin} = useUserRole();
const navigate = useNavigate();
const { t } = useTranslation();
useEffect(() => {
const currentTab = searchParams.get("tab");
if (currentTab === "invites") {
setSegmentValue(currentTab);
}
}, [searchParams.get("tab")]);
useEffect(() => {
const currentTab = searchParams.get("tab");
if (currentTab === "invites") {
setSegmentValue(currentTab);
}
}, [searchParams.get("tab")]);
const handleSegmentChange = (value: string) => {
setSegmentValue(value);
if (value === "invites") {
navigate(`?tab=${value}`);
} else {
navigate("");
}
};
const handleSegmentChange = (value: string) => {
setSegmentValue(value);
if (value === "invites") {
navigate(`?tab=${value}`);
} else {
navigate("");
}
};
return (
<>
<SettingsTitle title={t("Members")} />
return (
<>
<Helmet>
<title>{t("Members")} - {getAppName()}</title>
</Helmet>
<SettingsTitle title={t("Members")}/>
{/* <WorkspaceInviteSection /> */}
{/* <Divider my="lg" /> */}
{/* <WorkspaceInviteSection /> */}
{/* <Divider my="lg" /> */}
<Group justify="space-between">
<SegmentedControl
value={segmentValue}
onChange={handleSegmentChange}
data={[
{ label: t("Members"), value: "members" },
{ label: t("Pending"), value: "invites" },
]}
withItemsBorders={false}
/>
<Group justify="space-between">
<SegmentedControl
value={segmentValue}
onChange={handleSegmentChange}
data={[
{ label: t("Members"), value: "members" },
{ label: t("Pending"), value: "invites" },
]}
withItemsBorders={false}
/>
{isAdmin && <WorkspaceInviteModal />}
</Group>
{isAdmin && <WorkspaceInviteModal/>}
</Group>
<Space h="lg" />
<Space h="lg"/>
{segmentValue === "invites" ? (
<WorkspaceInvitesTable />
) : (
<WorkspaceMembersTable />
)}
</>
);
{segmentValue === "invites" ? (
<WorkspaceInvitesTable/>
) : (
<WorkspaceMembersTable/>
)}
</>
);
}

View File

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

View File

@@ -1,15 +1,22 @@
import { Container } from "@mantine/core";
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 {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);
const {spaceSlug} = useParams();
const {data: space} = useGetSpaceBySlugQuery(spaceSlug);
return (
<Container size={"800"} pt="xl">
{space && <SpaceHomeTabs />}
</Container>
);
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

@@ -5,12 +5,13 @@ import * as path from "path";
export const envPath = path.resolve(process.cwd(), "..", "..");
export default defineConfig(({ mode }) => {
const { APP_URL } = loadEnv(mode, envPath, "");
const { APP_URL, FILE_UPLOAD_SIZE_LIMIT } = loadEnv(mode, envPath, "");
return {
define: {
"process.env": {
APP_URL,
FILE_UPLOAD_SIZE_LIMIT
},
'APP_VERSION': JSON.stringify(process.env.npm_package_version),
},

View File

@@ -1,6 +1,6 @@
{
"name": "server",
"version": "0.3.1",
"version": "0.4.1",
"description": "",
"author": "",
"private": true,
@@ -51,7 +51,6 @@
"@socket.io/redis-adapter": "^8.3.0",
"bcrypt": "^5.1.1",
"bullmq": "^5.12.12",
"bytes": "^3.1.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"fix-esm": "^1.0.1",
@@ -81,7 +80,6 @@
"@nestjs/schematics": "^10.1.4",
"@nestjs/testing": "^10.4.1",
"@types/bcrypt": "^5.0.2",
"@types/bytes": "^3.1.4",
"@types/debounce": "^1.2.4",
"@types/fs-extra": "^11.0.4",
"@types/jest": "^29.5.12",

View File

@@ -16,4 +16,3 @@ export const inlineFileExtensions = [
'.mp4',
'.mov',
];
export const MAX_FILE_SIZE = '50MB';

View File

@@ -1,308 +1,310 @@
import {
BadRequestException,
Controller,
ForbiddenException,
Get,
HttpCode,
HttpStatus,
Logger,
NotFoundException,
Param,
Post,
Req,
Res,
UseGuards,
UseInterceptors,
BadRequestException,
Controller,
ForbiddenException,
Get,
HttpCode,
HttpStatus,
Logger,
NotFoundException,
Param,
Post,
Req,
Res,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { AttachmentService } from './services/attachment.service';
import { FastifyReply } from 'fastify';
import { FileInterceptor } from '../../common/interceptors/file.interceptor';
import {AttachmentService} from './services/attachment.service';
import {FastifyReply} from 'fastify';
import {FileInterceptor} from '../../common/interceptors/file.interceptor';
import * as bytes from 'bytes';
import { AuthUser } from '../../common/decorators/auth-user.decorator';
import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { User, Workspace } from '@docmost/db/types/entity.types';
import { StorageService } from '../../integrations/storage/storage.service';
import {AuthUser} from '../../common/decorators/auth-user.decorator';
import {AuthWorkspace} from '../../common/decorators/auth-workspace.decorator';
import {JwtAuthGuard} from '../../common/guards/jwt-auth.guard';
import {User, Workspace} from '@docmost/db/types/entity.types';
import {StorageService} from '../../integrations/storage/storage.service';
import {
getAttachmentFolderPath,
validAttachmentTypes,
getAttachmentFolderPath,
validAttachmentTypes,
} from './attachment.utils';
import { getMimeType } from '../../common/helpers';
import {getMimeType} from '../../common/helpers';
import {
AttachmentType,
inlineFileExtensions,
MAX_AVATAR_SIZE,
MAX_FILE_SIZE,
AttachmentType,
inlineFileExtensions,
MAX_AVATAR_SIZE,
} from './attachment.constants';
import {
SpaceCaslAction,
SpaceCaslSubject,
SpaceCaslAction,
SpaceCaslSubject,
} from '../casl/interfaces/space-ability.type';
import SpaceAbilityFactory from '../casl/abilities/space-ability.factory';
import {
WorkspaceCaslAction,
WorkspaceCaslSubject,
WorkspaceCaslAction,
WorkspaceCaslSubject,
} from '../casl/interfaces/workspace-ability.type';
import WorkspaceAbilityFactory from '../casl/abilities/workspace-ability.factory';
import { PageRepo } from '@docmost/db/repos/page/page.repo';
import { AttachmentRepo } from '@docmost/db/repos/attachment/attachment.repo';
import { validate as isValidUUID } from 'uuid';
import {PageRepo} from '@docmost/db/repos/page/page.repo';
import {AttachmentRepo} from '@docmost/db/repos/attachment/attachment.repo';
import {validate as isValidUUID} from 'uuid';
import {EnvironmentService} from "../../integrations/environment/environment.service";
@Controller()
export class AttachmentController {
private readonly logger = new Logger(AttachmentController.name);
private readonly logger = new Logger(AttachmentController.name);
constructor(
private readonly attachmentService: AttachmentService,
private readonly storageService: StorageService,
private readonly workspaceAbility: WorkspaceAbilityFactory,
private readonly spaceAbility: SpaceAbilityFactory,
private readonly pageRepo: PageRepo,
private readonly attachmentRepo: AttachmentRepo,
) {}
@UseGuards(JwtAuthGuard)
@HttpCode(HttpStatus.OK)
@Post('files/upload')
@UseInterceptors(FileInterceptor)
async uploadFile(
@Req() req: any,
@Res() res: FastifyReply,
@AuthUser() user: User,
@AuthWorkspace() workspace: Workspace,
) {
const maxFileSize = bytes(MAX_FILE_SIZE);
let file = null;
try {
file = await req.file({
limits: { fileSize: maxFileSize, fields: 3, files: 1 },
});
} catch (err: any) {
this.logger.error(err.message);
if (err?.statusCode === 413) {
throw new BadRequestException(
`File too large. Exceeds the ${MAX_FILE_SIZE} limit`,
);
}
}
if (!file) {
throw new BadRequestException('Failed to upload file');
}
const pageId = file.fields?.pageId?.value;
if (!pageId) {
throw new BadRequestException('PageId is required');
}
const page = await this.pageRepo.findById(pageId);
if (!page) {
throw new NotFoundException('Page not found');
}
const spaceAbility = await this.spaceAbility.createForUser(
user,
page.spaceId,
);
if (spaceAbility.cannot(SpaceCaslAction.Manage, SpaceCaslSubject.Page)) {
throw new ForbiddenException();
}
const spaceId = page.spaceId;
const attachmentId = file.fields?.attachmentId?.value;
if (attachmentId && !isValidUUID(attachmentId)) {
throw new BadRequestException('Invalid attachment id');
}
try {
const fileResponse = await this.attachmentService.uploadFile({
filePromise: file,
pageId: pageId,
spaceId: spaceId,
userId: user.id,
workspaceId: workspace.id,
attachmentId: attachmentId,
});
return res.send(fileResponse);
} catch (err: any) {
if (err?.statusCode === 413) {
const errMessage = `File too large. Exceeds the ${MAX_FILE_SIZE} limit`;
this.logger.error(errMessage);
throw new BadRequestException(errMessage);
}
this.logger.error(err);
throw new BadRequestException('Error processing file upload.');
}
}
@UseGuards(JwtAuthGuard)
@Get('/files/:fileId/:fileName')
async getFile(
@Res() res: FastifyReply,
@AuthUser() user: User,
@AuthWorkspace() workspace: Workspace,
@Param('fileId') fileId: string,
@Param('fileName') fileName?: string,
) {
if (!isValidUUID(fileId)) {
throw new NotFoundException('Invalid file id');
}
const attachment = await this.attachmentRepo.findById(fileId);
if (
!attachment ||
attachment.workspaceId !== workspace.id ||
!attachment.pageId ||
!attachment.spaceId
constructor(
private readonly attachmentService: AttachmentService,
private readonly storageService: StorageService,
private readonly workspaceAbility: WorkspaceAbilityFactory,
private readonly spaceAbility: SpaceAbilityFactory,
private readonly pageRepo: PageRepo,
private readonly attachmentRepo: AttachmentRepo,
private readonly environmentService: EnvironmentService,
) {
throw new NotFoundException();
}
const spaceAbility = await this.spaceAbility.createForUser(
user,
attachment.spaceId,
);
if (spaceAbility.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) {
throw new ForbiddenException();
}
try {
const fileStream = await this.storageService.read(attachment.filePath);
res.headers({
'Content-Type': attachment.mimeType,
'Cache-Control': 'public, max-age=3600',
});
if (!inlineFileExtensions.includes(attachment.fileExt)) {
res.header(
'Content-Disposition',
`attachment; filename="${encodeURIComponent(attachment.fileName)}"`,
);
}
return res.send(fileStream);
} catch (err) {
this.logger.error(err);
throw new NotFoundException('File not found');
}
}
@UseGuards(JwtAuthGuard)
@HttpCode(HttpStatus.OK)
@Post('attachments/upload-image')
@UseInterceptors(FileInterceptor)
async uploadAvatarOrLogo(
@Req() req: any,
@Res() res: FastifyReply,
@AuthUser() user: User,
@AuthWorkspace() workspace: Workspace,
) {
const maxFileSize = bytes(MAX_AVATAR_SIZE);
let file = null;
try {
file = await req.file({
limits: { fileSize: maxFileSize, fields: 3, files: 1 },
});
} catch (err: any) {
if (err?.statusCode === 413) {
throw new BadRequestException(
`File too large. Exceeds the ${MAX_AVATAR_SIZE} limit`,
);
}
}
if (!file) {
throw new BadRequestException('Invalid file upload');
}
const attachmentType = file.fields?.type?.value;
const spaceId = file.fields?.spaceId?.value;
if (!attachmentType) {
throw new BadRequestException('attachment type is required');
}
if (
!validAttachmentTypes.includes(attachmentType) ||
attachmentType === AttachmentType.File
@UseGuards(JwtAuthGuard)
@HttpCode(HttpStatus.OK)
@Post('files/upload')
@UseInterceptors(FileInterceptor)
async uploadFile(
@Req() req: any,
@Res() res: FastifyReply,
@AuthUser() user: User,
@AuthWorkspace() workspace: Workspace,
) {
throw new BadRequestException('Invalid image attachment type');
const maxFileSize = bytes(this.environmentService.getFileUploadSizeLimit());
let file = null;
try {
file = await req.file({
limits: {fileSize: maxFileSize, fields: 3, files: 1},
});
} catch (err: any) {
this.logger.error(err.message);
if (err?.statusCode === 413) {
throw new BadRequestException(
`File too large. Exceeds the ${this.environmentService.getFileUploadSizeLimit()} limit`,
);
}
}
if (!file) {
throw new BadRequestException('Failed to upload file');
}
const pageId = file.fields?.pageId?.value;
if (!pageId) {
throw new BadRequestException('PageId is required');
}
const page = await this.pageRepo.findById(pageId);
if (!page) {
throw new NotFoundException('Page not found');
}
const spaceAbility = await this.spaceAbility.createForUser(
user,
page.spaceId,
);
if (spaceAbility.cannot(SpaceCaslAction.Manage, SpaceCaslSubject.Page)) {
throw new ForbiddenException();
}
const spaceId = page.spaceId;
const attachmentId = file.fields?.attachmentId?.value;
if (attachmentId && !isValidUUID(attachmentId)) {
throw new BadRequestException('Invalid attachment id');
}
try {
const fileResponse = await this.attachmentService.uploadFile({
filePromise: file,
pageId: pageId,
spaceId: spaceId,
userId: user.id,
workspaceId: workspace.id,
attachmentId: attachmentId,
});
return res.send(fileResponse);
} catch (err: any) {
if (err?.statusCode === 413) {
const errMessage = `File too large. Exceeds the ${this.environmentService.getFileUploadSizeLimit()} limit`;
this.logger.error(errMessage);
throw new BadRequestException(errMessage);
}
this.logger.error(err);
throw new BadRequestException('Error processing file upload.');
}
}
if (attachmentType === AttachmentType.WorkspaceLogo) {
const ability = this.workspaceAbility.createForUser(user, workspace);
if (
ability.cannot(
WorkspaceCaslAction.Manage,
WorkspaceCaslSubject.Settings,
)
) {
throw new ForbiddenException();
}
}
if (attachmentType === AttachmentType.SpaceLogo) {
if (!spaceId) {
throw new BadRequestException('spaceId is required');
}
const spaceAbility = await this.spaceAbility.createForUser(user, spaceId);
if (
spaceAbility.cannot(SpaceCaslAction.Manage, SpaceCaslSubject.Settings)
) {
throw new ForbiddenException();
}
}
try {
const fileResponse = await this.attachmentService.uploadImage(
file,
attachmentType,
user.id,
workspace.id,
spaceId,
);
return res.send(fileResponse);
} catch (err: any) {
this.logger.error(err);
throw new BadRequestException('Error processing file upload.');
}
}
@Get('attachments/img/:attachmentType/:fileName')
async getLogoOrAvatar(
@Res() res: FastifyReply,
@AuthWorkspace() workspace: Workspace,
@Param('attachmentType') attachmentType: AttachmentType,
@Param('fileName') fileName?: string,
) {
if (
!validAttachmentTypes.includes(attachmentType) ||
attachmentType === AttachmentType.File
@UseGuards(JwtAuthGuard)
@Get('/files/:fileId/:fileName')
async getFile(
@Res() res: FastifyReply,
@AuthUser() user: User,
@AuthWorkspace() workspace: Workspace,
@Param('fileId') fileId: string,
@Param('fileName') fileName?: string,
) {
throw new BadRequestException('Invalid image attachment type');
if (!isValidUUID(fileId)) {
throw new NotFoundException('Invalid file id');
}
const attachment = await this.attachmentRepo.findById(fileId);
if (
!attachment ||
attachment.workspaceId !== workspace.id ||
!attachment.pageId ||
!attachment.spaceId
) {
throw new NotFoundException();
}
const spaceAbility = await this.spaceAbility.createForUser(
user,
attachment.spaceId,
);
if (spaceAbility.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) {
throw new ForbiddenException();
}
try {
const fileStream = await this.storageService.read(attachment.filePath);
res.headers({
'Content-Type': attachment.mimeType,
'Cache-Control': 'public, max-age=3600',
});
if (!inlineFileExtensions.includes(attachment.fileExt)) {
res.header(
'Content-Disposition',
`attachment; filename="${encodeURIComponent(attachment.fileName)}"`,
);
}
return res.send(fileStream);
} catch (err) {
this.logger.error(err);
throw new NotFoundException('File not found');
}
}
const filePath = `${getAttachmentFolderPath(attachmentType, workspace.id)}/${fileName}`;
@UseGuards(JwtAuthGuard)
@HttpCode(HttpStatus.OK)
@Post('attachments/upload-image')
@UseInterceptors(FileInterceptor)
async uploadAvatarOrLogo(
@Req() req: any,
@Res() res: FastifyReply,
@AuthUser() user: User,
@AuthWorkspace() workspace: Workspace,
) {
const maxFileSize = bytes(MAX_AVATAR_SIZE);
try {
const fileStream = await this.storageService.read(filePath);
res.headers({
'Content-Type': getMimeType(filePath),
'Cache-Control': 'public, max-age=86400',
});
return res.send(fileStream);
} catch (err) {
this.logger.error(err);
throw new NotFoundException('File not found');
let file = null;
try {
file = await req.file({
limits: {fileSize: maxFileSize, fields: 3, files: 1},
});
} catch (err: any) {
if (err?.statusCode === 413) {
throw new BadRequestException(
`File too large. Exceeds the ${MAX_AVATAR_SIZE} limit`,
);
}
}
if (!file) {
throw new BadRequestException('Invalid file upload');
}
const attachmentType = file.fields?.type?.value;
const spaceId = file.fields?.spaceId?.value;
if (!attachmentType) {
throw new BadRequestException('attachment type is required');
}
if (
!validAttachmentTypes.includes(attachmentType) ||
attachmentType === AttachmentType.File
) {
throw new BadRequestException('Invalid image attachment type');
}
if (attachmentType === AttachmentType.WorkspaceLogo) {
const ability = this.workspaceAbility.createForUser(user, workspace);
if (
ability.cannot(
WorkspaceCaslAction.Manage,
WorkspaceCaslSubject.Settings,
)
) {
throw new ForbiddenException();
}
}
if (attachmentType === AttachmentType.SpaceLogo) {
if (!spaceId) {
throw new BadRequestException('spaceId is required');
}
const spaceAbility = await this.spaceAbility.createForUser(user, spaceId);
if (
spaceAbility.cannot(SpaceCaslAction.Manage, SpaceCaslSubject.Settings)
) {
throw new ForbiddenException();
}
}
try {
const fileResponse = await this.attachmentService.uploadImage(
file,
attachmentType,
user.id,
workspace.id,
spaceId,
);
return res.send(fileResponse);
} catch (err: any) {
this.logger.error(err);
throw new BadRequestException('Error processing file upload.');
}
}
@Get('attachments/img/:attachmentType/:fileName')
async getLogoOrAvatar(
@Res() res: FastifyReply,
@AuthWorkspace() workspace: Workspace,
@Param('attachmentType') attachmentType: AttachmentType,
@Param('fileName') fileName?: string,
) {
if (
!validAttachmentTypes.includes(attachmentType) ||
attachmentType === AttachmentType.File
) {
throw new BadRequestException('Invalid image attachment type');
}
const filePath = `${getAttachmentFolderPath(attachmentType, workspace.id)}/${fileName}`;
try {
const fileStream = await this.storageService.read(filePath);
res.headers({
'Content-Type': getMimeType(filePath),
'Cache-Control': 'public, max-age=86400',
});
return res.send(fileStream);
} catch (err) {
this.logger.error(err);
throw new NotFoundException('File not found');
}
}
}
}

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

@@ -31,7 +31,7 @@ export class TokenService {
workspaceId,
type: JwtType.REFRESH,
};
const expiresIn = '30d'; // todo: fix
const expiresIn = this.environmentService.getJwtTokenExpiresIn();
return this.jwtService.sign(payload, { expiresIn });
}

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()

View File

@@ -248,7 +248,7 @@ export class WorkspaceInvitationService {
});
await this.mailService.sendToQueue({
to: invitation.email,
to: invitedByUser.email,
subject: `${newUser.name} has accepted your Docmost invite`,
template: emailTemplate,
});

View File

@@ -43,6 +43,11 @@ export class EnvironmentService {
return this.configService.get<string>('STORAGE_DRIVER', 'local');
}
getFileUploadSizeLimit(): string {
return this.configService.get<string>('FILE_UPLOAD_SIZE_LIMIT', '50mb');
}
getAwsS3AccessKeyId(): string {
return this.configService.get<string>('AWS_S3_ACCESS_KEY_ID');
}
@@ -98,6 +103,13 @@ export class EnvironmentService {
return secure === 'true';
}
getSmtpIgnoreTLS(): boolean {
const ignoretls = this.configService
.get<string>('SMTP_IGNORETLS', 'false')
.toLowerCase();
return ignoretls === 'true';
}
getSmtpUsername(): string {
return this.configService.get<string>('SMTP_USERNAME');
}

View File

@@ -22,6 +22,7 @@ export class RedisHealthIndicator extends HealthIndicator {
});
await redis.ping();
redis.disconnect();
return this.getStatus(key, true);
} catch (e) {
this.logger.error(e);

View File

@@ -21,7 +21,6 @@ import {
import { FileInterceptor } from '../../common/interceptors/file.interceptor';
import * as bytes from 'bytes';
import * as path from 'path';
import { MAX_FILE_SIZE } from '../../core/attachment/attachment.constants';
import { ImportService } from './import.service';
import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator';
@@ -45,7 +44,7 @@ export class ImportController {
) {
const validFileExtensions = ['.md', '.html'];
const maxFileSize = bytes(MAX_FILE_SIZE);
const maxFileSize = bytes('100mb');
let file = null;
try {
@@ -56,7 +55,7 @@ export class ImportController {
this.logger.error(err.message);
if (err?.statusCode === 413) {
throw new BadRequestException(
`File too large. Exceeds the ${MAX_FILE_SIZE} limit`,
`File too large. Exceeds the 100mb import limit`,
);
}
}

View File

@@ -44,6 +44,7 @@ export const mailDriverConfigProvider = {
connectionTimeout: 30 * 1000, // 30 seconds
auth,
secure: environmentService.getSmtpSecure(),
ignoreTLS: environmentService.getSmtpIgnoreTLS()
} as SMTPTransport.Options,
};

View File

@@ -33,6 +33,7 @@ export class StaticModule implements OnModuleInit {
ENV: this.environmentService.getNodeEnv(),
APP_URL: this.environmentService.getAppUrl(),
IS_CLOUD: this.environmentService.isCloud(),
FILE_UPLOAD_SIZE_LIMIT: this.environmentService.getFileUploadSizeLimit()
};
const windowScriptContent = `<script>window.CONFIG=${JSON.stringify(configString)};</script>`;

View File

@@ -1,7 +1,7 @@
{
"name": "docmost",
"homepage": "https://docmost.com",
"version": "0.3.1",
"version": "0.4.1",
"private": true,
"scripts": {
"build": "nx run-many -t build",
@@ -59,6 +59,7 @@
"@tiptap/react": "^2.6.6",
"@tiptap/starter-kit": "^2.6.6",
"@tiptap/suggestion": "^2.6.6",
"bytes": "^3.1.2",
"cross-env": "^7.0.3",
"fractional-indexing-jittered": "^0.9.1",
"ioredis": "^5.4.1",
@@ -68,6 +69,7 @@
},
"devDependencies": {
"@nx/js": "19.6.3",
"@types/bytes": "^3.1.4",
"@types/uuid": "^10.0.0",
"concurrently": "^8.2.2",
"nx": "19.6.3",
@@ -79,7 +81,9 @@
"packages/*"
]
},
"prettier": {
"semi": true
"pnpm": {
"patchedDependencies": {
"react-arborist@3.4.0": "patches/react-arborist@3.4.0.patch"
}
}
}

View File

@@ -0,0 +1,33 @@
diff --git a/dist/module/components/default-container.js b/dist/module/components/default-container.js
index 47724f59b482454fe3144dbb98bd16d3df6a9c17..2285e35ea0073a773b7b74e22758056fd3514c1a 100644
--- a/dist/module/components/default-container.js
+++ b/dist/module/components/default-container.js
@@ -34,28 +34,6 @@ export function DefaultContainer() {
return;
}
if (e.key === "Backspace") {
- if (!tree.props.onDelete)
- return;
- const ids = Array.from(tree.selectedIds);
- if (ids.length > 1) {
- let nextFocus = tree.mostRecentNode;
- while (nextFocus && nextFocus.isSelected) {
- nextFocus = nextFocus.nextSibling;
- }
- if (!nextFocus)
- nextFocus = tree.lastNode;
- tree.focus(nextFocus, { scroll: false });
- tree.delete(Array.from(ids));
- }
- else {
- const node = tree.focusedNode;
- if (node) {
- const sib = node.nextSibling;
- const parent = node.parent;
- tree.focus(sib || parent, { scroll: false });
- tree.delete(node);
- }
- }
return;
}
if (e.key === "Tab" && !e.shiftKey) {

198
pnpm-lock.yaml generated
View File

@@ -4,6 +4,11 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
patchedDependencies:
react-arborist@3.4.0:
hash: gjrtleyvvmuvu5j5zdnhxauhsu
path: patches/react-arborist@3.4.0.patch
importers:
.:
@@ -137,6 +142,9 @@ importers:
'@tiptap/suggestion':
specifier: ^2.6.6
version: 2.6.6(@tiptap/core@2.6.6(@tiptap/pm@2.6.6))(@tiptap/pm@2.6.6)
bytes:
specifier: ^3.1.2
version: 3.1.2
cross-env:
specifier: ^7.0.3
version: 7.0.3
@@ -158,7 +166,10 @@ importers:
devDependencies:
'@nx/js':
specifier: 19.6.3
version: 19.6.3(@babel/traverse@7.24.6)(@swc/core@1.5.25)(@types/node@22.5.2)(nx@19.6.3(@swc/core@1.5.25))(typescript@5.5.4)
version: 19.6.3(@babel/traverse@7.24.6)(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(nx@19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5)))(typescript@5.5.4)
'@types/bytes':
specifier: ^3.1.4
version: 3.1.4
'@types/uuid':
specifier: ^10.0.0
version: 10.0.0
@@ -167,7 +178,7 @@ importers:
version: 8.2.2
nx:
specifier: 19.6.3
version: 19.6.3(@swc/core@1.5.25)
version: 19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5))
tsx:
specifier: ^4.19.0
version: 4.19.0
@@ -180,6 +191,9 @@ importers:
'@casl/react':
specifier: ^4.0.0
version: 4.0.0(@casl/ability@6.7.1)(react@18.3.1)
'@docmost/editor-ext':
specifier: workspace:*
version: link:../../packages/editor-ext
'@emoji-mart/data':
specifier: ^1.2.1
version: 1.2.1
@@ -260,7 +274,7 @@ importers:
version: 18.3.1
react-arborist:
specifier: ^3.4.0
version: 3.4.0(@types/node@22.5.2)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
version: 3.4.0(patch_hash=gjrtleyvvmuvu5j5zdnhxauhsu)(@types/node@22.5.2)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-clear-modal:
specifier: ^2.0.9
version: 2.0.9(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -324,7 +338,7 @@ importers:
version: 8.3.0(eslint@9.9.1(jiti@1.21.0))(typescript@5.5.4)
'@vitejs/plugin-react':
specifier: ^4.3.1
version: 4.3.1(vite@5.4.2(@types/node@22.5.2)(less@4.2.0)(sugarss@4.0.1(postcss@8.4.43))(terser@5.29.2))
version: 4.3.1(vite@5.4.8(@types/node@22.5.2)(less@4.2.0)(sugarss@4.0.1(postcss@8.4.43))(terser@5.29.2))
eslint:
specifier: ^9.9.1
version: 9.9.1(jiti@1.21.0)
@@ -353,8 +367,8 @@ importers:
specifier: ^5.5.4
version: 5.5.4
vite:
specifier: ^5.4.2
version: 5.4.2(@types/node@22.5.2)(less@4.2.0)(sugarss@4.0.1(postcss@8.4.43))(terser@5.29.2)
specifier: ^5.4.8
version: 5.4.8(@types/node@22.5.2)(less@4.2.0)(sugarss@4.0.1(postcss@8.4.43))(terser@5.29.2)
apps/server:
dependencies:
@@ -378,7 +392,7 @@ importers:
version: 7.0.4
'@nestjs/bullmq':
specifier: ^10.2.1
version: 10.2.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(bullmq@5.12.12)
version: 10.2.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(bullmq@5.12.12)
'@nestjs/common':
specifier: ^10.4.1
version: 10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
@@ -390,7 +404,7 @@ importers:
version: 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
'@nestjs/event-emitter':
specifier: ^2.0.4
version: 2.0.4(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))
version: 2.0.4(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)
'@nestjs/jwt':
specifier: ^10.2.0
version: 10.2.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))
@@ -402,13 +416,13 @@ importers:
version: 10.0.3(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(passport@0.7.0)
'@nestjs/platform-fastify':
specifier: ^10.4.1
version: 10.4.1(@fastify/static@7.0.4)(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))
version: 10.4.1(@fastify/static@7.0.4)(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)
'@nestjs/platform-socket.io':
specifier: ^10.4.1
version: 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(rxjs@7.8.1)
'@nestjs/terminus':
specifier: ^10.2.3
version: 10.2.3(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1)
version: 10.2.3(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
'@nestjs/websockets':
specifier: ^10.4.1
version: 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(@nestjs/platform-socket.io@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
@@ -427,9 +441,6 @@ importers:
bullmq:
specifier: ^5.12.12
version: 5.12.12
bytes:
specifier: ^3.1.2
version: 3.1.2
class-transformer:
specifier: ^0.5.1
version: 0.5.1
@@ -462,7 +473,7 @@ importers:
version: 5.0.7
nestjs-kysely:
specifier: ^1.0.0
version: 1.0.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(kysely@0.27.4)(reflect-metadata@0.2.2)
version: 1.0.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(kysely@0.27.4)(reflect-metadata@0.2.2)
nodemailer:
specifier: ^6.9.14
version: 6.9.14
@@ -502,19 +513,16 @@ importers:
devDependencies:
'@nestjs/cli':
specifier: ^10.4.5
version: 10.4.5(@swc/core@1.5.25)
version: 10.4.5(@swc/core@1.5.25(@swc/helpers@0.5.5))
'@nestjs/schematics':
specifier: ^10.1.4
version: 10.1.4(chokidar@3.6.0)(typescript@5.5.4)
'@nestjs/testing':
specifier: ^10.4.1
version: 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))
version: 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)
'@types/bcrypt':
specifier: ^5.0.2
version: 5.0.2
'@types/bytes':
specifier: ^3.1.4
version: 3.1.4
'@types/debounce':
specifier: ^1.2.4
version: 1.2.4
@@ -562,7 +570,7 @@ importers:
version: 5.2.1(@types/eslint@8.56.10)(eslint-config-prettier@9.1.0(eslint@9.9.1(jiti@1.21.0)))(eslint@9.9.1(jiti@1.21.0))(prettier@3.3.3)
jest:
specifier: ^29.7.0
version: 29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4))
version: 29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4))
kysely-codegen:
specifier: ^0.16.3
version: 0.16.3(kysely@0.27.4)(pg@8.12.0)
@@ -580,13 +588,13 @@ importers:
version: 7.0.0
ts-jest:
specifier: ^29.2.5
version: 29.2.5(@babel/core@7.24.3)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.3))(jest@29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4)))(typescript@5.5.4)
version: 29.2.5(@babel/core@7.24.3)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.3))(jest@29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4)))(typescript@5.5.4)
ts-loader:
specifier: ^9.5.1
version: 9.5.1(typescript@5.5.4)(webpack@5.94.0(@swc/core@1.5.25))
version: 9.5.1(typescript@5.5.4)(webpack@5.94.0(@swc/core@1.5.25(@swc/helpers@0.5.5)))
ts-node:
specifier: ^10.9.2
version: 10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4)
version: 10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4)
tsconfig-paths:
specifier: ^4.2.0
version: 4.2.0
@@ -4049,6 +4057,7 @@ packages:
are-we-there-yet@2.0.0:
resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==}
engines: {node: '>=10'}
deprecated: This package is no longer supported.
arg@4.1.3:
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
@@ -5244,6 +5253,7 @@ packages:
gauge@3.0.2:
resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==}
engines: {node: '>=10'}
deprecated: This package is no longer supported.
generic-pool@3.9.0:
resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==}
@@ -6346,6 +6356,7 @@ packages:
npmlog@5.0.1:
resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==}
deprecated: This package is no longer supported.
nwsapi@2.2.10:
resolution: {integrity: sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==}
@@ -7791,8 +7802,8 @@ packages:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
vite@5.4.2:
resolution: {integrity: sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==}
vite@5.4.8:
resolution: {integrity: sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies:
@@ -10130,7 +10141,7 @@ snapshots:
jest-util: 29.7.0
slash: 3.0.0
'@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4))':
'@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4))':
dependencies:
'@jest/console': 29.7.0
'@jest/reporters': 29.7.0
@@ -10144,7 +10155,7 @@ snapshots:
exit: 0.1.2
graceful-fs: 4.2.11
jest-changed-files: 29.7.0
jest-config: 29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4))
jest-config: 29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4))
jest-haste-map: 29.7.0
jest-message-util: 29.7.0
jest-regex-util: 29.6.3
@@ -10430,21 +10441,21 @@ snapshots:
'@emnapi/runtime': 1.2.0
'@tybys/wasm-util': 0.9.0
'@nestjs/bull-shared@10.2.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))':
'@nestjs/bull-shared@10.2.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)':
dependencies:
'@nestjs/common': 10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
'@nestjs/core': 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
tslib: 2.6.3
'@nestjs/bullmq@10.2.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(bullmq@5.12.12)':
'@nestjs/bullmq@10.2.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(bullmq@5.12.12)':
dependencies:
'@nestjs/bull-shared': 10.2.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))
'@nestjs/bull-shared': 10.2.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)
'@nestjs/common': 10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
'@nestjs/core': 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
bullmq: 5.12.12
tslib: 2.6.3
'@nestjs/cli@10.4.5(@swc/core@1.5.25)':
'@nestjs/cli@10.4.5(@swc/core@1.5.25(@swc/helpers@0.5.5))':
dependencies:
'@angular-devkit/core': 17.3.8(chokidar@3.6.0)
'@angular-devkit/schematics': 17.3.8(chokidar@3.6.0)
@@ -10454,7 +10465,7 @@ snapshots:
chokidar: 3.6.0
cli-table3: 0.6.5
commander: 4.1.1
fork-ts-checker-webpack-plugin: 9.0.2(typescript@5.3.3)(webpack@5.94.0(@swc/core@1.5.25))
fork-ts-checker-webpack-plugin: 9.0.2(typescript@5.3.3)(webpack@5.94.0(@swc/core@1.5.25(@swc/helpers@0.5.5)))
glob: 10.4.2
inquirer: 8.2.6
node-emoji: 1.11.0
@@ -10463,10 +10474,10 @@ snapshots:
tsconfig-paths: 4.2.0
tsconfig-paths-webpack-plugin: 4.1.0
typescript: 5.3.3
webpack: 5.94.0(@swc/core@1.5.25)
webpack: 5.94.0(@swc/core@1.5.25(@swc/helpers@0.5.5))
webpack-node-externals: 3.0.0
optionalDependencies:
'@swc/core': 1.5.25
'@swc/core': 1.5.25(@swc/helpers@0.5.5)
transitivePeerDependencies:
- esbuild
- uglify-js
@@ -10507,7 +10518,7 @@ snapshots:
transitivePeerDependencies:
- encoding
'@nestjs/event-emitter@2.0.4(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))':
'@nestjs/event-emitter@2.0.4(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)':
dependencies:
'@nestjs/common': 10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
'@nestjs/core': 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
@@ -10532,7 +10543,7 @@ snapshots:
'@nestjs/common': 10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
passport: 0.7.0
'@nestjs/platform-fastify@10.4.1(@fastify/static@7.0.4)(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))':
'@nestjs/platform-fastify@10.4.1(@fastify/static@7.0.4)(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)':
dependencies:
'@fastify/cors': 9.0.1
'@fastify/formbody': 7.4.0
@@ -10582,7 +10593,7 @@ snapshots:
transitivePeerDependencies:
- chokidar
'@nestjs/terminus@10.2.3(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1)':
'@nestjs/terminus@10.2.3(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)':
dependencies:
'@nestjs/common': 10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
'@nestjs/core': 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
@@ -10591,7 +10602,7 @@ snapshots:
reflect-metadata: 0.2.2
rxjs: 7.8.1
'@nestjs/testing@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))':
'@nestjs/testing@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)':
dependencies:
'@nestjs/common': 10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
'@nestjs/core': 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
@@ -10650,15 +10661,15 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.17.1
'@nrwl/devkit@19.6.3(nx@19.6.3(@swc/core@1.5.25))':
'@nrwl/devkit@19.6.3(nx@19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5)))':
dependencies:
'@nx/devkit': 19.6.3(nx@19.6.3(@swc/core@1.5.25))
'@nx/devkit': 19.6.3(nx@19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5)))
transitivePeerDependencies:
- nx
'@nrwl/js@19.6.3(@babel/traverse@7.24.6)(@swc/core@1.5.25)(@types/node@22.5.2)(nx@19.6.3(@swc/core@1.5.25))(typescript@5.5.4)':
'@nrwl/js@19.6.3(@babel/traverse@7.24.6)(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(nx@19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5)))(typescript@5.5.4)':
dependencies:
'@nx/js': 19.6.3(@babel/traverse@7.24.6)(@swc/core@1.5.25)(@types/node@22.5.2)(nx@19.6.3(@swc/core@1.5.25))(typescript@5.5.4)
'@nx/js': 19.6.3(@babel/traverse@7.24.6)(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(nx@19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5)))(typescript@5.5.4)
transitivePeerDependencies:
- '@babel/traverse'
- '@swc-node/register'
@@ -10671,18 +10682,18 @@ snapshots:
- typescript
- verdaccio
'@nrwl/tao@19.6.3(@swc/core@1.5.25)':
'@nrwl/tao@19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5))':
dependencies:
nx: 19.6.3(@swc/core@1.5.25)
nx: 19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5))
tslib: 2.6.2
transitivePeerDependencies:
- '@swc-node/register'
- '@swc/core'
- debug
'@nrwl/workspace@19.6.3(@swc/core@1.5.25)':
'@nrwl/workspace@19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5))':
dependencies:
'@nx/workspace': 19.6.3(@swc/core@1.5.25)
'@nx/workspace': 19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5))
transitivePeerDependencies:
- '@swc-node/register'
- '@swc/core'
@@ -10696,20 +10707,20 @@ snapshots:
transitivePeerDependencies:
- encoding
'@nx/devkit@19.6.3(nx@19.6.3(@swc/core@1.5.25))':
'@nx/devkit@19.6.3(nx@19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5)))':
dependencies:
'@nrwl/devkit': 19.6.3(nx@19.6.3(@swc/core@1.5.25))
'@nrwl/devkit': 19.6.3(nx@19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5)))
ejs: 3.1.9
enquirer: 2.3.6
ignore: 5.3.1
minimatch: 9.0.3
nx: 19.6.3(@swc/core@1.5.25)
nx: 19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5))
semver: 7.6.2
tmp: 0.2.1
tslib: 2.6.2
yargs-parser: 21.1.1
'@nx/js@19.6.3(@babel/traverse@7.24.6)(@swc/core@1.5.25)(@types/node@22.5.2)(nx@19.6.3(@swc/core@1.5.25))(typescript@5.5.4)':
'@nx/js@19.6.3(@babel/traverse@7.24.6)(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(nx@19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5)))(typescript@5.5.4)':
dependencies:
'@babel/core': 7.24.6
'@babel/plugin-proposal-decorators': 7.23.7(@babel/core@7.24.6)
@@ -10718,9 +10729,9 @@ snapshots:
'@babel/preset-env': 7.23.8(@babel/core@7.24.6)
'@babel/preset-typescript': 7.23.3(@babel/core@7.24.6)
'@babel/runtime': 7.23.7
'@nrwl/js': 19.6.3(@babel/traverse@7.24.6)(@swc/core@1.5.25)(@types/node@22.5.2)(nx@19.6.3(@swc/core@1.5.25))(typescript@5.5.4)
'@nx/devkit': 19.6.3(nx@19.6.3(@swc/core@1.5.25))
'@nx/workspace': 19.6.3(@swc/core@1.5.25)
'@nrwl/js': 19.6.3(@babel/traverse@7.24.6)(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(nx@19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5)))(typescript@5.5.4)
'@nx/devkit': 19.6.3(nx@19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5)))
'@nx/workspace': 19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5))
babel-plugin-const-enum: 1.2.0(@babel/core@7.24.6)
babel-plugin-macros: 2.8.0
babel-plugin-transform-typescript-metadata: 0.3.2(@babel/core@7.24.6)(@babel/traverse@7.24.6)
@@ -10738,7 +10749,7 @@ snapshots:
ora: 5.3.0
semver: 7.6.2
source-map-support: 0.5.19
ts-node: 10.9.1(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4)
ts-node: 10.9.1(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4)
tsconfig-paths: 4.2.0
tslib: 2.6.2
transitivePeerDependencies:
@@ -10782,13 +10793,13 @@ snapshots:
'@nx/nx-win32-x64-msvc@19.6.3':
optional: true
'@nx/workspace@19.6.3(@swc/core@1.5.25)':
'@nx/workspace@19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5))':
dependencies:
'@nrwl/workspace': 19.6.3(@swc/core@1.5.25)
'@nx/devkit': 19.6.3(nx@19.6.3(@swc/core@1.5.25))
'@nrwl/workspace': 19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5))
'@nx/devkit': 19.6.3(nx@19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5)))
chalk: 4.1.2
enquirer: 2.3.6
nx: 19.6.3(@swc/core@1.5.25)
nx: 19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5))
tslib: 2.6.2
yargs-parser: 21.1.1
transitivePeerDependencies:
@@ -11397,7 +11408,7 @@ snapshots:
'@swc/core-win32-x64-msvc@1.5.25':
optional: true
'@swc/core@1.5.25':
'@swc/core@1.5.25(@swc/helpers@0.5.5)':
dependencies:
'@swc/counter': 0.1.3
'@swc/types': 0.1.7
@@ -11412,6 +11423,7 @@ snapshots:
'@swc/core-win32-arm64-msvc': 1.5.25
'@swc/core-win32-ia32-msvc': 1.5.25
'@swc/core-win32-x64-msvc': 1.5.25
'@swc/helpers': 0.5.5
optional: true
'@swc/counter@0.1.3': {}
@@ -12036,14 +12048,14 @@ snapshots:
dependencies:
'@ucast/core': 1.10.2
'@vitejs/plugin-react@4.3.1(vite@5.4.2(@types/node@22.5.2)(less@4.2.0)(sugarss@4.0.1(postcss@8.4.43))(terser@5.29.2))':
'@vitejs/plugin-react@4.3.1(vite@5.4.8(@types/node@22.5.2)(less@4.2.0)(sugarss@4.0.1(postcss@8.4.43))(terser@5.29.2))':
dependencies:
'@babel/core': 7.24.6
'@babel/plugin-transform-react-jsx-self': 7.24.6(@babel/core@7.24.6)
'@babel/plugin-transform-react-jsx-source': 7.24.6(@babel/core@7.24.6)
'@types/babel__core': 7.20.5
react-refresh: 0.14.2
vite: 5.4.2(@types/node@22.5.2)(less@4.2.0)(sugarss@4.0.1(postcss@8.4.43))(terser@5.29.2)
vite: 5.4.8(@types/node@22.5.2)(less@4.2.0)(sugarss@4.0.1(postcss@8.4.43))(terser@5.29.2)
transitivePeerDependencies:
- supports-color
@@ -12757,13 +12769,13 @@ snapshots:
optionalDependencies:
typescript: 5.3.3
create-jest@29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4)):
create-jest@29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4)):
dependencies:
'@jest/types': 29.6.3
chalk: 4.1.2
exit: 0.1.2
graceful-fs: 4.2.11
jest-config: 29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4))
jest-config: 29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4))
jest-util: 29.7.0
prompts: 2.4.2
transitivePeerDependencies:
@@ -13581,7 +13593,7 @@ snapshots:
cross-spawn: 7.0.3
signal-exit: 4.1.0
fork-ts-checker-webpack-plugin@9.0.2(typescript@5.3.3)(webpack@5.94.0(@swc/core@1.5.25)):
fork-ts-checker-webpack-plugin@9.0.2(typescript@5.3.3)(webpack@5.94.0(@swc/core@1.5.25(@swc/helpers@0.5.5))):
dependencies:
'@babel/code-frame': 7.24.6
chalk: 4.1.2
@@ -13596,7 +13608,7 @@ snapshots:
semver: 7.6.2
tapable: 2.2.1
typescript: 5.3.3
webpack: 5.94.0(@swc/core@1.5.25)
webpack: 5.94.0(@swc/core@1.5.25(@swc/helpers@0.5.5))
form-data@4.0.0:
dependencies:
@@ -14110,16 +14122,16 @@ snapshots:
- babel-plugin-macros
- supports-color
jest-cli@29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4)):
jest-cli@29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4)):
dependencies:
'@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4))
'@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4))
'@jest/test-result': 29.7.0
'@jest/types': 29.6.3
chalk: 4.1.2
create-jest: 29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4))
create-jest: 29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4))
exit: 0.1.2
import-local: 3.1.0
jest-config: 29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4))
jest-config: 29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4))
jest-util: 29.7.0
jest-validate: 29.7.0
yargs: 17.7.2
@@ -14129,7 +14141,7 @@ snapshots:
- supports-color
- ts-node
jest-config@29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4)):
jest-config@29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4)):
dependencies:
'@babel/core': 7.24.6
'@jest/test-sequencer': 29.7.0
@@ -14155,7 +14167,7 @@ snapshots:
strip-json-comments: 3.1.1
optionalDependencies:
'@types/node': 22.5.2
ts-node: 10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4)
ts-node: 10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4)
transitivePeerDependencies:
- babel-plugin-macros
- supports-color
@@ -14381,12 +14393,12 @@ snapshots:
merge-stream: 2.0.0
supports-color: 8.1.1
jest@29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4)):
jest@29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4)):
dependencies:
'@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4))
'@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4))
'@jest/types': 29.6.3
import-local: 3.1.0
jest-cli: 29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4))
jest-cli: 29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4))
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
@@ -14864,7 +14876,7 @@ snapshots:
neo-async@2.6.2: {}
nestjs-kysely@1.0.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(kysely@0.27.4)(reflect-metadata@0.2.2):
nestjs-kysely@1.0.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(kysely@0.27.4)(reflect-metadata@0.2.2):
dependencies:
'@nestjs/common': 10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
'@nestjs/core': 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
@@ -14951,10 +14963,10 @@ snapshots:
nwsapi@2.2.10: {}
nx@19.6.3(@swc/core@1.5.25):
nx@19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5)):
dependencies:
'@napi-rs/wasm-runtime': 0.2.4
'@nrwl/tao': 19.6.3(@swc/core@1.5.25)
'@nrwl/tao': 19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5))
'@yarnpkg/lockfile': 1.1.0
'@yarnpkg/parsers': 3.0.0-rc.46
'@zkochan/js-yaml': 0.0.7
@@ -14999,7 +15011,7 @@ snapshots:
'@nx/nx-linux-x64-musl': 19.6.3
'@nx/nx-win32-arm64-msvc': 19.6.3
'@nx/nx-win32-x64-msvc': 19.6.3
'@swc/core': 1.5.25
'@swc/core': 1.5.25(@swc/helpers@0.5.5)
transitivePeerDependencies:
- debug
@@ -15493,7 +15505,7 @@ snapshots:
dependencies:
safe-buffer: 5.2.1
react-arborist@3.4.0(@types/node@22.5.2)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
react-arborist@3.4.0(patch_hash=gjrtleyvvmuvu5j5zdnhxauhsu)(@types/node@22.5.2)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
react: 18.3.1
react-dnd: 14.0.5(@types/node@22.5.2)(@types/react@18.3.5)(react@18.3.1)
@@ -16152,16 +16164,16 @@ snapshots:
mkdirp: 1.0.4
yallist: 4.0.0
terser-webpack-plugin@5.3.10(@swc/core@1.5.25)(webpack@5.94.0(@swc/core@1.5.25)):
terser-webpack-plugin@5.3.10(@swc/core@1.5.25(@swc/helpers@0.5.5))(webpack@5.94.0(@swc/core@1.5.25(@swc/helpers@0.5.5))):
dependencies:
'@jridgewell/trace-mapping': 0.3.25
jest-worker: 27.5.1
schema-utils: 3.3.0
serialize-javascript: 6.0.2
terser: 5.29.2
webpack: 5.94.0(@swc/core@1.5.25)
webpack: 5.94.0(@swc/core@1.5.25(@swc/helpers@0.5.5))
optionalDependencies:
'@swc/core': 1.5.25
'@swc/core': 1.5.25(@swc/helpers@0.5.5)
terser@5.29.2:
dependencies:
@@ -16235,12 +16247,12 @@ snapshots:
ts-dedent@2.2.0: {}
ts-jest@29.2.5(@babel/core@7.24.3)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.3))(jest@29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4)))(typescript@5.5.4):
ts-jest@29.2.5(@babel/core@7.24.3)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.3))(jest@29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4)))(typescript@5.5.4):
dependencies:
bs-logger: 0.2.6
ejs: 3.1.10
fast-json-stable-stringify: 2.1.0
jest: 29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4))
jest: 29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4))
jest-util: 29.7.0
json5: 2.2.3
lodash.memoize: 4.1.2
@@ -16254,7 +16266,7 @@ snapshots:
'@jest/types': 29.6.3
babel-jest: 29.7.0(@babel/core@7.24.3)
ts-loader@9.5.1(typescript@5.5.4)(webpack@5.94.0(@swc/core@1.5.25)):
ts-loader@9.5.1(typescript@5.5.4)(webpack@5.94.0(@swc/core@1.5.25(@swc/helpers@0.5.5))):
dependencies:
chalk: 4.1.2
enhanced-resolve: 5.16.0
@@ -16262,9 +16274,9 @@ snapshots:
semver: 7.6.0
source-map: 0.7.4
typescript: 5.5.4
webpack: 5.94.0(@swc/core@1.5.25)
webpack: 5.94.0(@swc/core@1.5.25(@swc/helpers@0.5.5))
ts-node@10.9.1(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4):
ts-node@10.9.1(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4):
dependencies:
'@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.9
@@ -16282,9 +16294,9 @@ snapshots:
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
optionalDependencies:
'@swc/core': 1.5.25
'@swc/core': 1.5.25(@swc/helpers@0.5.5)
ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4):
ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4):
dependencies:
'@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.9
@@ -16302,7 +16314,7 @@ snapshots:
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
optionalDependencies:
'@swc/core': 1.5.25
'@swc/core': 1.5.25(@swc/helpers@0.5.5)
tsconfig-paths-webpack-plugin@4.1.0:
dependencies:
@@ -16449,7 +16461,7 @@ snapshots:
vary@1.1.2: {}
vite@5.4.2(@types/node@22.5.2)(less@4.2.0)(sugarss@4.0.1(postcss@8.4.43))(terser@5.29.2):
vite@5.4.8(@types/node@22.5.2)(less@4.2.0)(sugarss@4.0.1(postcss@8.4.43))(terser@5.29.2):
dependencies:
esbuild: 0.21.5
postcss: 8.4.43
@@ -16507,7 +16519,7 @@ snapshots:
webpack-sources@3.2.3: {}
webpack@5.94.0(@swc/core@1.5.25):
webpack@5.94.0(@swc/core@1.5.25(@swc/helpers@0.5.5)):
dependencies:
'@types/estree': 1.0.5
'@webassemblyjs/ast': 1.12.1
@@ -16529,7 +16541,7 @@ snapshots:
neo-async: 2.6.2
schema-utils: 3.3.0
tapable: 2.2.1
terser-webpack-plugin: 5.3.10(@swc/core@1.5.25)(webpack@5.94.0(@swc/core@1.5.25))
terser-webpack-plugin: 5.3.10(@swc/core@1.5.25(@swc/helpers@0.5.5))(webpack@5.94.0(@swc/core@1.5.25(@swc/helpers@0.5.5)))
watchpack: 2.4.1
webpack-sources: 3.2.3
transitivePeerDependencies: