mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 22:53:08 +08:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7383673636 | |||
| 3e7b2495c5 | |||
| 0fc8edeb52 | |||
| bbf865b2f6 | |||
| 0c622a0dc1 | |||
| f52cd011a4 | |||
| a4d53468c3 | |||
| cc93abfb7e | |||
| 13f26f9c31 | |||
| a4ec2dac6c | |||
| 681d7c789c | |||
| 9f583174a9 | |||
| 05633082c5 | |||
| 491fbad4ac | |||
| e824aeced7 | |||
| 8f056d1071 | |||
| 99cf6dab62 |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "client",
|
"name": "client",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.2.1",
|
"version": "0.2.3",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { Text, Group, UnstyledButton, Badge, Table } from "@mantine/core";
|
import {
|
||||||
|
Text,
|
||||||
|
Group,
|
||||||
|
UnstyledButton,
|
||||||
|
Badge,
|
||||||
|
Table,
|
||||||
|
ScrollArea,
|
||||||
|
} 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 PageListSkeleton from "@/components/ui/page-list-skeleton.tsx";
|
||||||
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
||||||
@@ -22,46 +29,48 @@ export default function RecentChanges({ spaceId }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return pages && pages.items.length > 0 ? (
|
return pages && pages.items.length > 0 ? (
|
||||||
<Table highlightOnHover verticalSpacing="sm">
|
<ScrollArea>
|
||||||
<Table.Tbody>
|
<Table highlightOnHover verticalSpacing="sm">
|
||||||
{pages.items.map((page) => (
|
<Table.Tbody>
|
||||||
<Table.Tr key={page.id}>
|
{pages.items.map((page) => (
|
||||||
<Table.Td>
|
<Table.Tr key={page.id}>
|
||||||
<UnstyledButton
|
|
||||||
component={Link}
|
|
||||||
to={buildPageUrl(page?.space.slug, page.slugId, page.title)}
|
|
||||||
>
|
|
||||||
<Group wrap="nowrap">
|
|
||||||
{page.icon || <IconFileDescription size={18} />}
|
|
||||||
|
|
||||||
<Text fw={500} size="md" lineClamp={1}>
|
|
||||||
{page.title || "Untitled"}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
</UnstyledButton>
|
|
||||||
</Table.Td>
|
|
||||||
{!spaceId && (
|
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Badge
|
<UnstyledButton
|
||||||
color="blue"
|
|
||||||
variant="light"
|
|
||||||
component={Link}
|
component={Link}
|
||||||
to={getSpaceUrl(page?.space.slug)}
|
to={buildPageUrl(page?.space.slug, page.slugId, page.title)}
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
>
|
>
|
||||||
{page?.space.name}
|
<Group wrap="nowrap">
|
||||||
</Badge>
|
{page.icon || <IconFileDescription size={18} />}
|
||||||
|
|
||||||
|
<Text fw={500} size="md" lineClamp={1}>
|
||||||
|
{page.title || "Untitled"}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</UnstyledButton>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
)}
|
{!spaceId && (
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Text c="dimmed" size="xs" fw={500}>
|
<Badge
|
||||||
{formattedDate(page.updatedAt)}
|
color="blue"
|
||||||
</Text>
|
variant="light"
|
||||||
</Table.Td>
|
component={Link}
|
||||||
</Table.Tr>
|
to={getSpaceUrl(page?.space.slug)}
|
||||||
))}
|
style={{ cursor: "pointer" }}
|
||||||
</Table.Tbody>
|
>
|
||||||
</Table>
|
{page?.space.name}
|
||||||
|
</Badge>
|
||||||
|
</Table.Td>
|
||||||
|
)}
|
||||||
|
<Table.Td>
|
||||||
|
<Text c="dimmed" size="xs" fw={500}>
|
||||||
|
{formattedDate(page.updatedAt)}
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</ScrollArea>
|
||||||
) : (
|
) : (
|
||||||
<Text size="md" ta="center">
|
<Text size="md" ta="center">
|
||||||
No pages yet
|
No pages yet
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Avatar, Group, Menu, rem, UnstyledButton, Text } from "@mantine/core";
|
import { Group, Menu, UnstyledButton, Text } from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
|
IconBrush,
|
||||||
IconChevronDown,
|
IconChevronDown,
|
||||||
IconLogout,
|
IconLogout,
|
||||||
IconSettings,
|
IconSettings,
|
||||||
@@ -38,10 +39,7 @@ export default function TopMenu() {
|
|||||||
<Text fw={500} size="sm" lh={1} mr={3}>
|
<Text fw={500} size="sm" lh={1} mr={3}>
|
||||||
{workspace.name}
|
{workspace.name}
|
||||||
</Text>
|
</Text>
|
||||||
<IconChevronDown
|
<IconChevronDown size={16} />
|
||||||
style={{ width: rem(12), height: rem(12) }}
|
|
||||||
stroke={1.5}
|
|
||||||
/>
|
|
||||||
</Group>
|
</Group>
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
@@ -51,12 +49,7 @@ export default function TopMenu() {
|
|||||||
<Menu.Item
|
<Menu.Item
|
||||||
component={Link}
|
component={Link}
|
||||||
to={APP_ROUTE.SETTINGS.WORKSPACE.GENERAL}
|
to={APP_ROUTE.SETTINGS.WORKSPACE.GENERAL}
|
||||||
leftSection={
|
leftSection={<IconSettings size={16} />}
|
||||||
<IconSettings
|
|
||||||
style={{ width: rem(16), height: rem(16) }}
|
|
||||||
stroke={1.5}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Workspace settings
|
Workspace settings
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
@@ -64,12 +57,7 @@ export default function TopMenu() {
|
|||||||
<Menu.Item
|
<Menu.Item
|
||||||
component={Link}
|
component={Link}
|
||||||
to={APP_ROUTE.SETTINGS.WORKSPACE.MEMBERS}
|
to={APP_ROUTE.SETTINGS.WORKSPACE.MEMBERS}
|
||||||
leftSection={
|
leftSection={<IconUsers size={16} />}
|
||||||
<IconUsers
|
|
||||||
style={{ width: rem(16), height: rem(16) }}
|
|
||||||
stroke={1.5}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Manage members
|
Manage members
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
@@ -98,27 +86,22 @@ export default function TopMenu() {
|
|||||||
<Menu.Item
|
<Menu.Item
|
||||||
component={Link}
|
component={Link}
|
||||||
to={APP_ROUTE.SETTINGS.ACCOUNT.PROFILE}
|
to={APP_ROUTE.SETTINGS.ACCOUNT.PROFILE}
|
||||||
leftSection={
|
leftSection={<IconUserCircle size={16} />}
|
||||||
<IconUserCircle
|
|
||||||
style={{ width: rem(16), height: rem(16) }}
|
|
||||||
stroke={1.5}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
My profile
|
My profile
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|
||||||
|
<Menu.Item
|
||||||
|
component={Link}
|
||||||
|
to={APP_ROUTE.SETTINGS.ACCOUNT.PREFERENCES}
|
||||||
|
leftSection={<IconBrush size={16} />}
|
||||||
|
>
|
||||||
|
My preferences
|
||||||
|
</Menu.Item>
|
||||||
|
|
||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
|
|
||||||
<Menu.Item
|
<Menu.Item onClick={logout} leftSection={<IconLogout size={16} />}>
|
||||||
onClick={logout}
|
|
||||||
leftSection={
|
|
||||||
<IconLogout
|
|
||||||
style={{ width: rem(16), height: rem(16) }}
|
|
||||||
stroke={1.5}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Logout
|
Logout
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu.Dropdown>
|
</Menu.Dropdown>
|
||||||
|
|||||||
@@ -2,32 +2,28 @@ import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { Image } from "@mantine/core";
|
import { Image } from "@mantine/core";
|
||||||
import { getFileUrl } from "@/lib/config.ts";
|
import { getFileUrl } from "@/lib/config.ts";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
export default function ImageView(props: NodeViewProps) {
|
export default function ImageView(props: NodeViewProps) {
|
||||||
const { node, selected } = props;
|
const { node, selected } = props;
|
||||||
const { src, width, align, title } = node.attrs;
|
const { src, width, align, title } = node.attrs;
|
||||||
|
|
||||||
const flexJustifyContent = useMemo(() => {
|
const alignClass = useMemo(() => {
|
||||||
if (align === "center") return "center";
|
if (align === "left") return "alignLeft";
|
||||||
if (align === "right") return "flex-end";
|
if (align === "right") return "alignRight";
|
||||||
return "flex-start";
|
if (align === "center") return "alignCenter";
|
||||||
|
return "alignCenter";
|
||||||
}, [align]);
|
}, [align]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeViewWrapper
|
<NodeViewWrapper>
|
||||||
style={{
|
|
||||||
position: "relative",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: flexJustifyContent,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Image
|
<Image
|
||||||
radius="md"
|
radius="md"
|
||||||
fit="contain"
|
fit="contain"
|
||||||
w={width}
|
w={width}
|
||||||
src={getFileUrl(src)}
|
src={getFileUrl(src)}
|
||||||
alt={title}
|
alt={title}
|
||||||
className={selected ? "ProseMirror-selectednode" : ""}
|
className={clsx(selected ? "ProseMirror-selectednode" : "", alignClass)}
|
||||||
/>
|
/>
|
||||||
</NodeViewWrapper>
|
</NodeViewWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -264,10 +264,20 @@ export const getSuggestionItems = ({
|
|||||||
const search = query.toLowerCase();
|
const search = query.toLowerCase();
|
||||||
const filteredGroups: SlashMenuGroupedItemsType = {};
|
const filteredGroups: SlashMenuGroupedItemsType = {};
|
||||||
|
|
||||||
|
const fuzzyMatch = (query, target) => {
|
||||||
|
let queryIndex = 0;
|
||||||
|
target = target.toLowerCase();
|
||||||
|
for (let char of target) {
|
||||||
|
if (query[queryIndex] === char) queryIndex++;
|
||||||
|
if (queryIndex === query.length) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
for (const [group, items] of Object.entries(CommandGroups)) {
|
for (const [group, items] of Object.entries(CommandGroups)) {
|
||||||
const filteredItems = items.filter((item) => {
|
const filteredItems = items.filter((item) => {
|
||||||
return (
|
return (
|
||||||
item.title.toLowerCase().includes(search) ||
|
fuzzyMatch(search, item.title) ||
|
||||||
item.description.toLowerCase().includes(search) ||
|
item.description.toLowerCase().includes(search) ||
|
||||||
(item.searchTerms &&
|
(item.searchTerms &&
|
||||||
item.searchTerms.some((term: string) => term.includes(search)))
|
item.searchTerms.some((term: string) => term.includes(search)))
|
||||||
|
|||||||
@@ -1,31 +1,27 @@
|
|||||||
import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { getFileUrl } from "@/lib/config.ts";
|
import { getFileUrl } from "@/lib/config.ts";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
export default function VideoView(props: NodeViewProps) {
|
export default function VideoView(props: NodeViewProps) {
|
||||||
const { node, selected } = props;
|
const { node, selected } = props;
|
||||||
const { src, width, align } = node.attrs;
|
const { src, width, align } = node.attrs;
|
||||||
|
|
||||||
const flexJustifyContent = useMemo(() => {
|
const alignClass = useMemo(() => {
|
||||||
if (align === "center") return "center";
|
if (align === "left") return "alignLeft";
|
||||||
if (align === "right") return "flex-end";
|
if (align === "right") return "alignRight";
|
||||||
return "flex-start";
|
if (align === "center") return "alignCenter";
|
||||||
|
return "alignCenter";
|
||||||
}, [align]);
|
}, [align]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeViewWrapper
|
<NodeViewWrapper>
|
||||||
style={{
|
|
||||||
position: "relative",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: flexJustifyContent,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<video
|
<video
|
||||||
preload="metadata"
|
preload="metadata"
|
||||||
width={width}
|
width={width}
|
||||||
controls
|
controls
|
||||||
src={getFileUrl(src)}
|
src={getFileUrl(src)}
|
||||||
className={selected ? "ProseMirror-selectednode" : ""}
|
className={clsx(selected ? "ProseMirror-selectednode" : "", alignClass)}
|
||||||
/>
|
/>
|
||||||
</NodeViewWrapper>
|
</NodeViewWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import classes from "@/features/editor/styles/editor.module.css";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { TitleEditor } from "@/features/editor/title-editor";
|
import { TitleEditor } from "@/features/editor/title-editor";
|
||||||
import PageEditor from "@/features/editor/page-editor";
|
import PageEditor from "@/features/editor/page-editor";
|
||||||
|
import { Container } from "@mantine/core";
|
||||||
|
import { useAtom } from "jotai/index";
|
||||||
|
import { userAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||||
|
|
||||||
const MemoizedTitleEditor = React.memo(TitleEditor);
|
const MemoizedTitleEditor = React.memo(TitleEditor);
|
||||||
const MemoizedPageEditor = React.memo(PageEditor);
|
const MemoizedPageEditor = React.memo(PageEditor);
|
||||||
@@ -21,8 +24,16 @@ export function FullEditor({
|
|||||||
spaceSlug,
|
spaceSlug,
|
||||||
editable,
|
editable,
|
||||||
}: FullEditorProps) {
|
}: FullEditorProps) {
|
||||||
|
const [user] = useAtom(userAtom);
|
||||||
|
const fullPageWidth = user.settings?.preferences?.fullPageWidth;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.editor}>
|
<Container
|
||||||
|
fluid={fullPageWidth}
|
||||||
|
{...(fullPageWidth && { mx: 80 })}
|
||||||
|
size={850}
|
||||||
|
className={classes.editor}
|
||||||
|
>
|
||||||
<MemoizedTitleEditor
|
<MemoizedTitleEditor
|
||||||
pageId={pageId}
|
pageId={pageId}
|
||||||
slugId={slugId}
|
slugId={slugId}
|
||||||
@@ -31,6 +42,6 @@ export function FullEditor({
|
|||||||
editable={editable}
|
editable={editable}
|
||||||
/>
|
/>
|
||||||
<MemoizedPageEditor pageId={pageId} editable={editable} />
|
<MemoizedPageEditor pageId={pageId} editable={editable} />
|
||||||
</div>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
);
|
);
|
||||||
font-size: var(--mantine-font-size-md);
|
font-size: var(--mantine-font-size-md);
|
||||||
line-height: var(--mantine-line-height-xl);
|
line-height: var(--mantine-line-height-xl);
|
||||||
font-weight: 415;
|
font-weight: 400;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
> * + * {
|
> * + * {
|
||||||
@@ -125,6 +125,21 @@
|
|||||||
cursor: ew-resize;
|
cursor: ew-resize;
|
||||||
cursor: col-resize;
|
cursor: col-resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alignLeft {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alignRight {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alignCenter {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ProseMirror-icon {
|
.ProseMirror-icon {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
.editor {
|
.editor {
|
||||||
max-width: 800px;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 8px 20px;
|
padding: 8px 20px;
|
||||||
margin: 64px auto;
|
margin: 64px auto;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
iframe {
|
iframe {
|
||||||
display: block;
|
display: block;
|
||||||
outline: 0px solid transparent;
|
outline: 0 solid transparent;
|
||||||
border-radius: var(--mantine-radius-md);
|
border-radius: var(--mantine-radius-md);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ActionIcon, Menu, Tooltip } from "@mantine/core";
|
import { ActionIcon, Group, Menu, Tooltip } from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
|
IconArrowsHorizontal,
|
||||||
IconDots,
|
IconDots,
|
||||||
IconHistory,
|
IconHistory,
|
||||||
IconLink,
|
IconLink,
|
||||||
@@ -19,6 +20,7 @@ import { getAppUrl } from "@/lib/config.ts";
|
|||||||
import { extractPageSlugId } from "@/lib";
|
import { extractPageSlugId } from "@/lib";
|
||||||
import { treeApiAtom } from "@/features/page/tree/atoms/tree-api-atom.ts";
|
import { treeApiAtom } from "@/features/page/tree/atoms/tree-api-atom.ts";
|
||||||
import { useDeletePageModal } from "@/features/page/hooks/use-delete-page-modal.tsx";
|
import { useDeletePageModal } from "@/features/page/hooks/use-delete-page-modal.tsx";
|
||||||
|
import { PageWidthToggle } from "@/features/user/components/page-width-pref.tsx";
|
||||||
|
|
||||||
interface PageHeaderMenuProps {
|
interface PageHeaderMenuProps {
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
@@ -95,6 +97,13 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
|||||||
Copy link
|
Copy link
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
|
|
||||||
|
<Menu.Item leftSection={<IconArrowsHorizontal size={16} stroke={2} />}>
|
||||||
|
<Group wrap="nowrap">
|
||||||
|
<PageWidthToggle label="Full width" />
|
||||||
|
</Group>
|
||||||
|
</Menu.Item>
|
||||||
|
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
leftSection={<IconHistory size={16} stroke={2} />}
|
leftSection={<IconHistory size={16} stroke={2} />}
|
||||||
onClick={openHistoryModal}
|
onClick={openHistoryModal}
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ export default function SpaceTree({ spaceId, readOnly }: SpaceTreeProps) {
|
|||||||
if (pagesData?.pages && !hasNextPage) {
|
if (pagesData?.pages && !hasNextPage) {
|
||||||
const allItems = pagesData.pages.flatMap((page) => page.items);
|
const allItems = pagesData.pages.flatMap((page) => page.items);
|
||||||
const treeData = buildTree(allItems);
|
const treeData = buildTree(allItems);
|
||||||
|
|
||||||
if (data.length < 1 || data?.[0].spaceId !== spaceId) {
|
if (data.length < 1 || data?.[0].spaceId !== spaceId) {
|
||||||
//Thoughts
|
//Thoughts
|
||||||
// don't reset if there is data in state
|
// don't reset if there is data in state
|
||||||
@@ -106,7 +107,7 @@ export default function SpaceTree({ spaceId, readOnly }: SpaceTreeProps) {
|
|||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
if (isDataLoaded.current && currentPage) {
|
if (isDataLoaded.current && currentPage) {
|
||||||
// check if pageId node is present in the tree
|
// check if pageId node is present in the tree
|
||||||
const node = dfs(treeApiRef.current.root, currentPage.id);
|
const node = dfs(treeApiRef.current?.root, currentPage.id);
|
||||||
if (node) {
|
if (node) {
|
||||||
// if node is found, no need to traverse its ancestors
|
// if node is found, no need to traverse its ancestors
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export function useTreeMutation<T>(spaceId: string) {
|
|||||||
slugId: createdPage.slugId,
|
slugId: createdPage.slugId,
|
||||||
name: "",
|
name: "",
|
||||||
position: createdPage.position,
|
position: createdPage.position,
|
||||||
|
spaceId: createdPage.spaceId,
|
||||||
|
parentPageId: createdPage.parentPageId,
|
||||||
children: [],
|
children: [],
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,10 @@
|
|||||||
import { Group, Center, Text } from "@mantine/core";
|
import { Group, Center, Text } from "@mantine/core";
|
||||||
import { Spotlight } from "@mantine/spotlight";
|
import { Spotlight } from "@mantine/spotlight";
|
||||||
import {
|
import { IconFileDescription, IconSearch } from "@tabler/icons-react";
|
||||||
IconFileDescription,
|
|
||||||
IconHome,
|
|
||||||
IconSearch,
|
|
||||||
IconSettings,
|
|
||||||
} from "@tabler/icons-react";
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { useDebouncedValue } from "@mantine/hooks";
|
import { useDebouncedValue } from "@mantine/hooks";
|
||||||
import { usePageSearchQuery } from "@/features/search/queries/search-query";
|
import { usePageSearchQuery } from "@/features/search/queries/search-query";
|
||||||
import { useGetSpaceBySlugQuery } from "@/features/space/queries/space-query.ts";
|
|
||||||
import { useRecentChangesQuery } from "@/features/page/queries/page-query.ts";
|
|
||||||
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
||||||
|
|
||||||
interface SearchSpotlightProps {
|
interface SearchSpotlightProps {
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
import { atomWithStorage } from "jotai/utils";
|
import { atomWithStorage } from "jotai/utils";
|
||||||
|
|
||||||
import { ICurrentUser } from "@/features/user/types/user.types";
|
import { ICurrentUser } from "@/features/user/types/user.types";
|
||||||
|
import { focusAtom } from "jotai-optics";
|
||||||
|
|
||||||
export const currentUserAtom = atomWithStorage<ICurrentUser | null>("currentUser", null);
|
export const currentUserAtom = atomWithStorage<ICurrentUser | null>(
|
||||||
|
"currentUser",
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const userAtom = focusAtom(currentUserAtom, (optic) =>
|
||||||
|
optic.prop("user"),
|
||||||
|
);
|
||||||
|
export const workspaceAtom = focusAtom(currentUserAtom, (optic) =>
|
||||||
|
optic.prop("workspace"),
|
||||||
|
);
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { Group, Text, Switch, MantineSize } from "@mantine/core";
|
||||||
|
import { useAtom } from "jotai/index";
|
||||||
|
import { userAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||||
|
import { updateUser } from "@/features/user/services/user-service.ts";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
export default function PageWidthPref() {
|
||||||
|
return (
|
||||||
|
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||||
|
<div>
|
||||||
|
<Text size="md">Full page width</Text>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
Choose your preferred page width.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PageWidthToggle />
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PageWidthToggleProps {
|
||||||
|
size?: MantineSize;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
export function PageWidthToggle({ size, label }: PageWidthToggleProps) {
|
||||||
|
const [user, setUser] = useAtom(userAtom);
|
||||||
|
const [checked, setChecked] = useState(
|
||||||
|
user.settings?.preferences?.fullPageWidth,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = event.currentTarget.checked;
|
||||||
|
const updatedUser = await updateUser({ fullPageWidth: value });
|
||||||
|
setChecked(value);
|
||||||
|
setUser(updatedUser);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Switch
|
||||||
|
size={size}
|
||||||
|
label={label}
|
||||||
|
labelPosition="left"
|
||||||
|
defaultChecked={checked}
|
||||||
|
onChange={handleChange}
|
||||||
|
aria-label="Toggle full page width"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ export interface IUser {
|
|||||||
emailVerifiedAt: Date;
|
emailVerifiedAt: Date;
|
||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
timezone: string;
|
timezone: string;
|
||||||
settings: any;
|
settings: IUserSettings;
|
||||||
invitedById: string;
|
invitedById: string;
|
||||||
lastLoginAt: string;
|
lastLoginAt: string;
|
||||||
lastActiveAt: Date;
|
lastActiveAt: Date;
|
||||||
@@ -17,9 +17,16 @@ export interface IUser {
|
|||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
deactivatedAt: Date;
|
deactivatedAt: Date;
|
||||||
deletedAt: Date;
|
deletedAt: Date;
|
||||||
|
fullPageWidth: boolean; // used for update
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICurrentUser {
|
export interface ICurrentUser {
|
||||||
user: IUser;
|
user: IUser;
|
||||||
workspace: IWorkspace;
|
workspace: IWorkspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IUserSettings {
|
||||||
|
preferences: {
|
||||||
|
fullPageWidth: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { getBackendUrl } from "@/lib/config.ts";
|
|||||||
|
|
||||||
const api: AxiosInstance = axios.create({
|
const api: AxiosInstance = axios.create({
|
||||||
baseURL: getBackendUrl(),
|
baseURL: getBackendUrl(),
|
||||||
|
withCredentials: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
api.interceptors.request.use(
|
api.interceptors.request.use(
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import SettingsTitle from "@/components/settings/settings-title.tsx";
|
import SettingsTitle from "@/components/settings/settings-title.tsx";
|
||||||
import AccountTheme from "@/features/user/components/account-theme.tsx";
|
import AccountTheme from "@/features/user/components/account-theme.tsx";
|
||||||
|
import PageWidthPref from "@/features/user/components/page-width-pref.tsx";
|
||||||
|
import { Divider } from "@mantine/core";
|
||||||
|
|
||||||
export default function AccountPreferences() {
|
export default function AccountPreferences() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingsTitle title="Preferences" />
|
<SettingsTitle title="Preferences" />
|
||||||
<AccountTheme />
|
<AccountTheme />
|
||||||
|
<Divider my={"md"} />
|
||||||
|
<PageWidthPref />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.2.1",
|
"version": "0.2.3",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
"@aws-sdk/client-s3": "^3.600.0",
|
"@aws-sdk/client-s3": "^3.600.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.600.0",
|
"@aws-sdk/s3-request-presigner": "^3.600.0",
|
||||||
"@casl/ability": "^6.7.1",
|
"@casl/ability": "^6.7.1",
|
||||||
|
"@fastify/cookie": "^9.3.1",
|
||||||
"@fastify/multipart": "^8.3.0",
|
"@fastify/multipart": "^8.3.0",
|
||||||
"@fastify/static": "^7.0.4",
|
"@fastify/static": "^7.0.4",
|
||||||
"@nestjs/bullmq": "^10.1.1",
|
"@nestjs/bullmq": "^10.1.1",
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ import {
|
|||||||
import WorkspaceAbilityFactory from '../casl/abilities/workspace-ability.factory';
|
import WorkspaceAbilityFactory from '../casl/abilities/workspace-ability.factory';
|
||||||
import { PageRepo } from '@docmost/db/repos/page/page.repo';
|
import { PageRepo } from '@docmost/db/repos/page/page.repo';
|
||||||
import { AttachmentRepo } from '@docmost/db/repos/attachment/attachment.repo';
|
import { AttachmentRepo } from '@docmost/db/repos/attachment/attachment.repo';
|
||||||
import { Public } from '../../common/decorators/public.decorator';
|
|
||||||
import { validate as isValidUUID } from 'uuid';
|
import { validate as isValidUUID } from 'uuid';
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
@@ -129,12 +128,11 @@ export class AttachmentController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Public()
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Get('/files/:fileId/:fileName')
|
@Get('/files/:fileId/:fileName')
|
||||||
async getFile(
|
async getFile(
|
||||||
@Res() res: FastifyReply,
|
@Res() res: FastifyReply,
|
||||||
//@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
@AuthWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
@Param('fileId') fileId: string,
|
@Param('fileId') fileId: string,
|
||||||
@Param('fileName') fileName?: string,
|
@Param('fileName') fileName?: string,
|
||||||
@@ -144,18 +142,29 @@ export class AttachmentController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const attachment = await this.attachmentRepo.findById(fileId);
|
const attachment = await this.attachmentRepo.findById(fileId);
|
||||||
if (attachment.workspaceId !== workspace.id) {
|
if (
|
||||||
|
!attachment ||
|
||||||
|
attachment.workspaceId !== workspace.id ||
|
||||||
|
!attachment.pageId ||
|
||||||
|
!attachment.spaceId
|
||||||
|
) {
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!attachment || !attachment.pageId) {
|
const spaceAbility = await this.spaceAbility.createForUser(
|
||||||
throw new NotFoundException('File record not found');
|
user,
|
||||||
|
attachment.spaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (spaceAbility.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) {
|
||||||
|
throw new ForbiddenException();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fileStream = await this.storageService.read(attachment.filePath);
|
const fileStream = await this.storageService.read(attachment.filePath);
|
||||||
res.headers({
|
res.headers({
|
||||||
'Content-Type': getMimeType(attachment.filePath),
|
'Content-Type': getMimeType(attachment.filePath),
|
||||||
|
'Cache-Control': 'public, max-age=3600',
|
||||||
});
|
});
|
||||||
return res.send(fileStream);
|
return res.send(fileStream);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -268,6 +277,7 @@ export class AttachmentController {
|
|||||||
const fileStream = await this.storageService.read(filePath);
|
const fileStream = await this.storageService.read(filePath);
|
||||||
res.headers({
|
res.headers({
|
||||||
'Content-Type': getMimeType(filePath),
|
'Content-Type': getMimeType(filePath),
|
||||||
|
'Cache-Control': 'public, max-age=86400',
|
||||||
});
|
});
|
||||||
return res.send(fileStream);
|
return res.send(fileStream);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import {
|
|||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { PassportStrategy } from '@nestjs/passport';
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
import { Strategy } from 'passport-jwt';
|
||||||
import { EnvironmentService } from '../../../integrations/environment/environment.service';
|
import { EnvironmentService } from '../../../integrations/environment/environment.service';
|
||||||
import { JwtPayload, JwtType } from '../dto/jwt-payload';
|
import { JwtPayload, JwtType } from '../dto/jwt-payload';
|
||||||
import { WorkspaceRepo } from '@docmost/db/repos/workspace/workspace.repo';
|
import { WorkspaceRepo } from '@docmost/db/repos/workspace/workspace.repo';
|
||||||
import { UserRepo } from '@docmost/db/repos/user/user.repo';
|
import { UserRepo } from '@docmost/db/repos/user/user.repo';
|
||||||
|
import { FastifyRequest } from 'fastify';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||||
@@ -18,7 +19,15 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
|||||||
private readonly environmentService: EnvironmentService,
|
private readonly environmentService: EnvironmentService,
|
||||||
) {
|
) {
|
||||||
super({
|
super({
|
||||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
jwtFromRequest: (req: FastifyRequest) => {
|
||||||
|
let accessToken = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
accessToken = JSON.parse(req.cookies?.authTokens)?.accessToken;
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return accessToken || this.extractTokenFromHeader(req);
|
||||||
|
},
|
||||||
ignoreExpiration: false,
|
ignoreExpiration: false,
|
||||||
secretOrKey: environmentService.getAppSecret(),
|
secretOrKey: environmentService.getAppSecret(),
|
||||||
passReqToCallback: true,
|
passReqToCallback: true,
|
||||||
@@ -50,4 +59,9 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
|||||||
|
|
||||||
return { user, workspace };
|
return { user, workspace };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extractTokenFromHeader(request: FastifyRequest): string | undefined {
|
||||||
|
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
||||||
|
return type === 'Bearer' ? token : undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { OmitType, PartialType } from '@nestjs/mapped-types';
|
import { OmitType, PartialType } from '@nestjs/mapped-types';
|
||||||
import { CreateUserDto } from '../../auth/dto/create-user.dto';
|
import { CreateUserDto } from '../../auth/dto/create-user.dto';
|
||||||
import { IsOptional, IsString } from 'class-validator';
|
import { IsBoolean, IsOptional, IsString } from 'class-validator';
|
||||||
|
|
||||||
export class UpdateUserDto extends PartialType(
|
export class UpdateUserDto extends PartialType(
|
||||||
OmitType(CreateUserDto, ['password'] as const),
|
OmitType(CreateUserDto, ['password'] as const),
|
||||||
@@ -8,4 +8,8 @@ export class UpdateUserDto extends PartialType(
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
fullPageWidth: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,10 +20,19 @@ export class UserService {
|
|||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
) {
|
) {
|
||||||
const user = await this.userRepo.findById(userId, workspaceId);
|
const user = await this.userRepo.findById(userId, workspaceId);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new NotFoundException('User not found');
|
throw new NotFoundException('User not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// preference update
|
||||||
|
if (typeof updateUserDto.fullPageWidth !== 'undefined') {
|
||||||
|
return this.updateUserPageWidthPreference(
|
||||||
|
userId,
|
||||||
|
updateUserDto.fullPageWidth,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (updateUserDto.name) {
|
if (updateUserDto.name) {
|
||||||
user.name = updateUserDto.name;
|
user.name = updateUserDto.name;
|
||||||
}
|
}
|
||||||
@@ -42,4 +51,12 @@ export class UserService {
|
|||||||
await this.userRepo.updateUser(updateUserDto, userId, workspaceId);
|
await this.userRepo.updateUser(updateUserDto, userId, workspaceId);
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateUserPageWidthPreference(userId: string, fullPageWidth: boolean) {
|
||||||
|
return this.userRepo.updatePreference(
|
||||||
|
userId,
|
||||||
|
'fullPageWidth',
|
||||||
|
fullPageWidth,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
executeWithPagination,
|
executeWithPagination,
|
||||||
PaginationResult,
|
PaginationResult,
|
||||||
} from '@docmost/db/pagination/pagination';
|
} from '@docmost/db/pagination/pagination';
|
||||||
|
import { sql } from 'kysely';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserRepo {
|
export class UserRepo {
|
||||||
@@ -157,6 +158,24 @@ export class UserRepo {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updatePreference(
|
||||||
|
userId: string,
|
||||||
|
prefKey: string,
|
||||||
|
prefValue: string | boolean,
|
||||||
|
) {
|
||||||
|
return await this.db
|
||||||
|
.updateTable('users')
|
||||||
|
.set({
|
||||||
|
settings: sql`COALESCE(settings, '{}'::jsonb)
|
||||||
|
|| jsonb_build_object('preferences', COALESCE(settings->'preferences', '{}'::jsonb)
|
||||||
|
|| jsonb_build_object('${sql.raw(prefKey)}', ${sql.lit(prefValue)}))`,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where('id', '=', userId)
|
||||||
|
.returning(this.baseFields)
|
||||||
|
.executeTakeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
async getSpaceIds(
|
async getSpaceIds(
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
|
|||||||
+12
-1
@@ -9,6 +9,7 @@ import { TransformHttpResponseInterceptor } from './common/interceptors/http-res
|
|||||||
import fastifyMultipart from '@fastify/multipart';
|
import fastifyMultipart from '@fastify/multipart';
|
||||||
import { WsRedisIoAdapter } from './ws/adapter/ws-redis.adapter';
|
import { WsRedisIoAdapter } from './ws/adapter/ws-redis.adapter';
|
||||||
import { InternalLogFilter } from './common/logger/internal-log-filter';
|
import { InternalLogFilter } from './common/logger/internal-log-filter';
|
||||||
|
import fastifyCookie from '@fastify/cookie';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create<NestFastifyApplication>(
|
const app = await NestFactory.create<NestFastifyApplication>(
|
||||||
@@ -31,6 +32,7 @@ async function bootstrap() {
|
|||||||
app.useWebSocketAdapter(redisIoAdapter);
|
app.useWebSocketAdapter(redisIoAdapter);
|
||||||
|
|
||||||
await app.register(fastifyMultipart as any);
|
await app.register(fastifyMultipart as any);
|
||||||
|
await app.register(fastifyCookie as any);
|
||||||
|
|
||||||
app
|
app
|
||||||
.getHttpAdapter()
|
.getHttpAdapter()
|
||||||
@@ -56,7 +58,16 @@ async function bootstrap() {
|
|||||||
transform: true,
|
transform: true,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
app.enableCors();
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
// make development easy
|
||||||
|
app.enableCors({
|
||||||
|
origin: ['http://localhost:5173'],
|
||||||
|
credentials: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
app.enableCors();
|
||||||
|
}
|
||||||
|
|
||||||
app.useGlobalInterceptors(new TransformHttpResponseInterceptor());
|
app.useGlobalInterceptors(new TransformHttpResponseInterceptor());
|
||||||
app.enableShutdownHooks();
|
app.enableShutdownHooks();
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "docmost",
|
"name": "docmost",
|
||||||
"homepage": "https://docmost.com",
|
"homepage": "https://docmost.com",
|
||||||
"version": "0.2.1",
|
"version": "0.2.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nx run-many -t build",
|
"build": "nx run-many -t build",
|
||||||
|
|||||||
Generated
+17
@@ -331,6 +331,9 @@ importers:
|
|||||||
'@casl/ability':
|
'@casl/ability':
|
||||||
specifier: ^6.7.1
|
specifier: ^6.7.1
|
||||||
version: 6.7.1
|
version: 6.7.1
|
||||||
|
'@fastify/cookie':
|
||||||
|
specifier: ^9.3.1
|
||||||
|
version: 9.3.1
|
||||||
'@fastify/multipart':
|
'@fastify/multipart':
|
||||||
specifier: ^8.3.0
|
specifier: ^8.3.0
|
||||||
version: 8.3.0
|
version: 8.3.0
|
||||||
@@ -1875,6 +1878,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
|
resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
'@fastify/cookie@9.3.1':
|
||||||
|
resolution: {integrity: sha512-h1NAEhB266+ZbZ0e9qUE6NnNR07i7DnNXWG9VbbZ8uC6O/hxHpl+Zoe5sw1yfdZ2U6XhToUGDnzQtWJdCaPwfg==}
|
||||||
|
|
||||||
'@fastify/cors@9.0.1':
|
'@fastify/cors@9.0.1':
|
||||||
resolution: {integrity: sha512-YY9Ho3ovI+QHIL2hW+9X4XqQjXLjJqsU+sMV/xFsxZkE8p3GNnYVFpoOxF7SsP5ZL76gwvbo3V9L+FIekBGU4Q==}
|
resolution: {integrity: sha512-YY9Ho3ovI+QHIL2hW+9X4XqQjXLjJqsU+sMV/xFsxZkE8p3GNnYVFpoOxF7SsP5ZL76gwvbo3V9L+FIekBGU4Q==}
|
||||||
|
|
||||||
@@ -4594,6 +4600,10 @@ packages:
|
|||||||
convert-source-map@2.0.0:
|
convert-source-map@2.0.0:
|
||||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||||
|
|
||||||
|
cookie-signature@1.2.1:
|
||||||
|
resolution: {integrity: sha512-78KWk9T26NhzXtuL26cIJ8/qNHANyJ/ZYrmEXFzUmhZdjpBv+DlWlOANRTGBt48YcyslsLrj0bMLFTmXvLRCOw==}
|
||||||
|
engines: {node: '>=6.6.0'}
|
||||||
|
|
||||||
cookie@0.4.2:
|
cookie@0.4.2:
|
||||||
resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==}
|
resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -9789,6 +9799,11 @@ snapshots:
|
|||||||
|
|
||||||
'@fastify/busboy@2.1.1': {}
|
'@fastify/busboy@2.1.1': {}
|
||||||
|
|
||||||
|
'@fastify/cookie@9.3.1':
|
||||||
|
dependencies:
|
||||||
|
cookie-signature: 1.2.1
|
||||||
|
fastify-plugin: 4.5.1
|
||||||
|
|
||||||
'@fastify/cors@9.0.1':
|
'@fastify/cors@9.0.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
fastify-plugin: 4.5.1
|
fastify-plugin: 4.5.1
|
||||||
@@ -12947,6 +12962,8 @@ snapshots:
|
|||||||
|
|
||||||
convert-source-map@2.0.0: {}
|
convert-source-map@2.0.0: {}
|
||||||
|
|
||||||
|
cookie-signature@1.2.1: {}
|
||||||
|
|
||||||
cookie@0.4.2: {}
|
cookie@0.4.2: {}
|
||||||
|
|
||||||
cookie@0.6.0: {}
|
cookie@0.6.0: {}
|
||||||
|
|||||||
Reference in New Issue
Block a user