mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 06:23:06 +08:00
feat(EE): full-text search in attachments (#1502)
* feat(EE): fulltext search in attachments * feat: global search - search filters - attachments search ui - and more * fix import * fix import * rename migration * add GIN index * fix table name * sanitize
This commit is contained in:
@@ -14,6 +14,14 @@ import SidebarToggle from "@/components/ui/sidebar-toggle-button.tsx";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import useTrial from "@/ee/hooks/use-trial.tsx";
|
import useTrial from "@/ee/hooks/use-trial.tsx";
|
||||||
import { isCloud } from "@/lib/config.ts";
|
import { isCloud } from "@/lib/config.ts";
|
||||||
|
import {
|
||||||
|
SearchControl,
|
||||||
|
SearchMobileControl,
|
||||||
|
} from "@/features/search/components/search-control.tsx";
|
||||||
|
import {
|
||||||
|
searchSpotlight,
|
||||||
|
shareSearchSpotlight,
|
||||||
|
} from "@/features/search/constants.ts";
|
||||||
|
|
||||||
const links = [{ link: APP_ROUTE.HOME, label: "Home" }];
|
const links = [{ link: APP_ROUTE.HOME, label: "Home" }];
|
||||||
|
|
||||||
@@ -79,6 +87,15 @@ export function AppHeader() {
|
|||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Group visibleFrom="sm">
|
||||||
|
<SearchControl onClick={searchSpotlight.open} />
|
||||||
|
</Group>
|
||||||
|
<Group hiddenFrom="sm">
|
||||||
|
<SearchMobileControl onSearch={searchSpotlight.open} />
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Group px={"xl"} wrap="nowrap">
|
<Group px={"xl"} wrap="nowrap">
|
||||||
{isCloud() && isTrial && trialDaysLeft !== 0 && (
|
{isCloud() && isTrial && trialDaysLeft !== 0 && (
|
||||||
<Badge
|
<Badge
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
import { UserProvider } from "@/features/user/user-provider.tsx";
|
import { UserProvider } from "@/features/user/user-provider.tsx";
|
||||||
import { Outlet } from "react-router-dom";
|
import { Outlet, useParams } from "react-router-dom";
|
||||||
import GlobalAppShell from "@/components/layouts/global/global-app-shell.tsx";
|
import GlobalAppShell from "@/components/layouts/global/global-app-shell.tsx";
|
||||||
import { PosthogUser } from "@/ee/components/posthog-user.tsx";
|
import { PosthogUser } from "@/ee/components/posthog-user.tsx";
|
||||||
import { isCloud } from "@/lib/config.ts";
|
import { isCloud } from "@/lib/config.ts";
|
||||||
|
import { SearchSpotlight } from "@/features/search/components/search-spotlight.tsx";
|
||||||
|
import React from "react";
|
||||||
|
import { useGetSpaceBySlugQuery } from "@/features/space/queries/space-query.ts";
|
||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
|
const { spaceSlug } = useParams();
|
||||||
|
const { data: space } = useGetSpaceBySlugQuery(spaceSlug);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserProvider>
|
<UserProvider>
|
||||||
<GlobalAppShell>
|
<GlobalAppShell>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</GlobalAppShell>
|
</GlobalAppShell>
|
||||||
{isCloud() && <PosthogUser />}
|
{isCloud() && <PosthogUser />}
|
||||||
|
<SearchSpotlight spaceId={space?.id} />
|
||||||
</UserProvider>
|
</UserProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import { extractPageSlugId } from "@/lib";
|
|||||||
import { FIVE_MINUTES } from "@/lib/constants.ts";
|
import { FIVE_MINUTES } from "@/lib/constants.ts";
|
||||||
import { PageEditMode } from "@/features/user/types/user.types.ts";
|
import { PageEditMode } from "@/features/user/types/user.types.ts";
|
||||||
import { jwtDecode } from "jwt-decode";
|
import { jwtDecode } from "jwt-decode";
|
||||||
|
import { searchSpotlight } from '@/features/search/constants.ts';
|
||||||
|
|
||||||
interface PageEditorProps {
|
interface PageEditorProps {
|
||||||
pageId: string;
|
pageId: string;
|
||||||
@@ -222,6 +223,10 @@ export default function PageEditor({
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyK') {
|
||||||
|
searchSpotlight.open();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) {
|
if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) {
|
||||||
const slashCommand = document.querySelector("#slash-command");
|
const slashCommand = document.querySelector("#slash-command");
|
||||||
if (slashCommand) {
|
if (slashCommand) {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { UpdateEvent } from "@/features/websocket/types";
|
|||||||
import localEmitter from "@/lib/local-emitter.ts";
|
import localEmitter from "@/lib/local-emitter.ts";
|
||||||
import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
|
import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||||
import { PageEditMode } from "@/features/user/types/user.types.ts";
|
import { PageEditMode } from "@/features/user/types/user.types.ts";
|
||||||
|
import { searchSpotlight } from "@/features/search/constants.ts";
|
||||||
|
|
||||||
export interface TitleEditorProps {
|
export interface TitleEditorProps {
|
||||||
pageId: string;
|
pageId: string;
|
||||||
@@ -86,6 +87,20 @@ export function TitleEditor({
|
|||||||
content: title,
|
content: title,
|
||||||
immediatelyRender: true,
|
immediatelyRender: true,
|
||||||
shouldRerenderOnTransaction: false,
|
shouldRerenderOnTransaction: false,
|
||||||
|
editorProps: {
|
||||||
|
handleDOMEvents: {
|
||||||
|
keydown: (_view, event) => {
|
||||||
|
if ((event.ctrlKey || event.metaKey) && event.code === "KeyS") {
|
||||||
|
event.preventDefault();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ((event.ctrlKey || event.metaKey) && event.code === "KeyK") {
|
||||||
|
searchSpotlight.open();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Group, Center, Text, Badge, ActionIcon } from "@mantine/core";
|
||||||
|
import { Spotlight } from "@mantine/spotlight";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { IconFile, IconDownload } from "@tabler/icons-react";
|
||||||
|
import { buildPageUrl } from "@/features/page/page.utils";
|
||||||
|
import { getPageIcon } from "@/lib";
|
||||||
|
import {
|
||||||
|
IAttachmentSearch,
|
||||||
|
IPageSearch,
|
||||||
|
} from "@/features/search/types/search.types";
|
||||||
|
import DOMPurify from "dompurify";
|
||||||
|
|
||||||
|
interface SearchResultItemProps {
|
||||||
|
result: IPageSearch | IAttachmentSearch;
|
||||||
|
isAttachmentResult: boolean;
|
||||||
|
showSpace?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SearchResultItem({
|
||||||
|
result,
|
||||||
|
isAttachmentResult,
|
||||||
|
showSpace,
|
||||||
|
}: SearchResultItemProps) {
|
||||||
|
if (isAttachmentResult) {
|
||||||
|
const attachmentResult = result as IAttachmentSearch;
|
||||||
|
|
||||||
|
const handleDownload = (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const downloadUrl = `/api/files/${attachmentResult.id}/${attachmentResult.fileName}`;
|
||||||
|
window.open(downloadUrl, "_blank");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Spotlight.Action
|
||||||
|
component={Link}
|
||||||
|
//@ts-ignore
|
||||||
|
to={buildPageUrl(
|
||||||
|
attachmentResult.space.slug,
|
||||||
|
attachmentResult.page.slugId,
|
||||||
|
attachmentResult.page.title,
|
||||||
|
)}
|
||||||
|
style={{ userSelect: "none" }}
|
||||||
|
>
|
||||||
|
<Group wrap="nowrap" w="100%">
|
||||||
|
<Center>
|
||||||
|
<IconFile size={20} />
|
||||||
|
</Center>
|
||||||
|
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<Text>{attachmentResult.fileName}</Text>
|
||||||
|
<Text size="xs" opacity={0.6}>
|
||||||
|
{attachmentResult.space.name} • {attachmentResult.page.title}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{attachmentResult?.highlight && (
|
||||||
|
<Text
|
||||||
|
opacity={0.6}
|
||||||
|
size="xs"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: DOMPurify.sanitize(attachmentResult.highlight, {
|
||||||
|
ALLOWED_TAGS: ["mark", "em", "strong", "b"],
|
||||||
|
ALLOWED_ATTR: []
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
color="gray"
|
||||||
|
onClick={handleDownload}
|
||||||
|
title="Download attachment"
|
||||||
|
>
|
||||||
|
<IconDownload size={18} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Group>
|
||||||
|
</Spotlight.Action>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const pageResult = result as IPageSearch;
|
||||||
|
return (
|
||||||
|
<Spotlight.Action
|
||||||
|
component={Link}
|
||||||
|
//@ts-ignore
|
||||||
|
to={buildPageUrl(
|
||||||
|
pageResult.space.slug,
|
||||||
|
pageResult.slugId,
|
||||||
|
pageResult.title,
|
||||||
|
)}
|
||||||
|
style={{ userSelect: "none" }}
|
||||||
|
>
|
||||||
|
<Group wrap="nowrap" w="100%">
|
||||||
|
<Center>{getPageIcon(pageResult?.icon)}</Center>
|
||||||
|
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<Text>{pageResult.title}</Text>
|
||||||
|
|
||||||
|
{showSpace && pageResult.space && (
|
||||||
|
<Badge variant="light" size="xs" color="gray">
|
||||||
|
{pageResult.space.name}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{pageResult?.highlight && (
|
||||||
|
<Text
|
||||||
|
opacity={0.6}
|
||||||
|
size="xs"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: DOMPurify.sanitize(pageResult.highlight, {
|
||||||
|
ALLOWED_TAGS: ["mark", "em", "strong", "b"],
|
||||||
|
ALLOWED_ATTR: []
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Spotlight.Action>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
.filtersContainer {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 8px 0;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterButton {
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 12px;
|
||||||
|
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-gray-6));
|
||||||
|
&:hover {
|
||||||
|
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-gray-6));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
import React, { useState, useMemo, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Menu,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Divider,
|
||||||
|
Badge,
|
||||||
|
ScrollArea,
|
||||||
|
Avatar,
|
||||||
|
Group,
|
||||||
|
getDefaultZIndex,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import {
|
||||||
|
IconChevronDown,
|
||||||
|
IconBuilding,
|
||||||
|
IconFileDescription,
|
||||||
|
IconSearch,
|
||||||
|
IconCheck,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useDebouncedValue } from "@mantine/hooks";
|
||||||
|
import { useGetSpacesQuery } from "@/features/space/queries/space-query";
|
||||||
|
import { useLicense } from "@/ee/hooks/use-license";
|
||||||
|
import classes from "./search-spotlight-filters.module.css";
|
||||||
|
|
||||||
|
interface SearchSpotlightFiltersProps {
|
||||||
|
onFiltersChange?: (filters: any) => void;
|
||||||
|
spaceId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SearchSpotlightFilters({
|
||||||
|
onFiltersChange,
|
||||||
|
spaceId,
|
||||||
|
}: SearchSpotlightFiltersProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { hasLicenseKey } = useLicense();
|
||||||
|
const [selectedSpaceId, setSelectedSpaceId] = useState<string | null>(
|
||||||
|
spaceId || null,
|
||||||
|
);
|
||||||
|
const [spaceSearchQuery, setSpaceSearchQuery] = useState("");
|
||||||
|
const [debouncedSpaceQuery] = useDebouncedValue(spaceSearchQuery, 300);
|
||||||
|
const [contentType, setContentType] = useState<string | null>("page");
|
||||||
|
|
||||||
|
const { data: spacesData } = useGetSpacesQuery({
|
||||||
|
page: 1,
|
||||||
|
limit: 100,
|
||||||
|
query: debouncedSpaceQuery,
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedSpaceData = useMemo(() => {
|
||||||
|
if (!spacesData?.items || !selectedSpaceId) return null;
|
||||||
|
return spacesData.items.find((space) => space.id === selectedSpaceId);
|
||||||
|
}, [spacesData?.items, selectedSpaceId]);
|
||||||
|
|
||||||
|
const availableSpaces = useMemo(() => {
|
||||||
|
const spaces = spacesData?.items || [];
|
||||||
|
if (!selectedSpaceId) return spaces;
|
||||||
|
|
||||||
|
// Sort to put selected space first
|
||||||
|
return [...spaces].sort((a, b) => {
|
||||||
|
if (a.id === selectedSpaceId) return -1;
|
||||||
|
if (b.id === selectedSpaceId) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}, [spacesData?.items, selectedSpaceId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (onFiltersChange) {
|
||||||
|
onFiltersChange({
|
||||||
|
spaceId: selectedSpaceId,
|
||||||
|
contentType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const contentTypeOptions = [
|
||||||
|
{ value: "page", label: "Pages" },
|
||||||
|
{
|
||||||
|
value: "attachment",
|
||||||
|
label: "Attachments",
|
||||||
|
disabled: !hasLicenseKey,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleSpaceSelect = (spaceId: string | null) => {
|
||||||
|
setSelectedSpaceId(spaceId);
|
||||||
|
|
||||||
|
if (onFiltersChange) {
|
||||||
|
onFiltersChange({
|
||||||
|
spaceId: spaceId,
|
||||||
|
contentType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFilterChange = (filterType: string, value: any) => {
|
||||||
|
let newSelectedSpaceId = selectedSpaceId;
|
||||||
|
let newContentType = contentType;
|
||||||
|
|
||||||
|
switch (filterType) {
|
||||||
|
case "spaceId":
|
||||||
|
newSelectedSpaceId = value;
|
||||||
|
setSelectedSpaceId(value);
|
||||||
|
break;
|
||||||
|
case "contentType":
|
||||||
|
newContentType = value;
|
||||||
|
setContentType(value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onFiltersChange) {
|
||||||
|
onFiltersChange({
|
||||||
|
spaceId: newSelectedSpaceId,
|
||||||
|
contentType: newContentType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.filtersContainer}>
|
||||||
|
<Menu
|
||||||
|
shadow="md"
|
||||||
|
width={250}
|
||||||
|
position="bottom-start"
|
||||||
|
zIndex={getDefaultZIndex("max")}
|
||||||
|
>
|
||||||
|
<Menu.Target>
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
color="gray"
|
||||||
|
size="sm"
|
||||||
|
rightSection={<IconChevronDown size={14} />}
|
||||||
|
leftSection={<IconBuilding size={16} />}
|
||||||
|
className={classes.filterButton}
|
||||||
|
fw={500}
|
||||||
|
>
|
||||||
|
{selectedSpaceId
|
||||||
|
? `Space: ${selectedSpaceData?.name || "Unknown"}`
|
||||||
|
: "Space: All spaces"}
|
||||||
|
</Button>
|
||||||
|
</Menu.Target>
|
||||||
|
<Menu.Dropdown>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Find a space"
|
||||||
|
data-autofocus
|
||||||
|
autoFocus
|
||||||
|
leftSection={<IconSearch size={16} />}
|
||||||
|
value={spaceSearchQuery}
|
||||||
|
onChange={(e) => setSpaceSearchQuery(e.target.value)}
|
||||||
|
size="sm"
|
||||||
|
variant="filled"
|
||||||
|
radius="sm"
|
||||||
|
styles={{ input: { marginBottom: 8 } }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ScrollArea.Autosize mah={280}>
|
||||||
|
<Menu.Item onClick={() => handleSpaceSelect(null)}>
|
||||||
|
<Group flex="1" gap="xs">
|
||||||
|
<Avatar
|
||||||
|
color="initials"
|
||||||
|
variant="filled"
|
||||||
|
name="All spaces"
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
All spaces
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
Search in all your spaces
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
{!selectedSpaceId && <IconCheck size={20} />}
|
||||||
|
</Group>
|
||||||
|
</Menu.Item>
|
||||||
|
|
||||||
|
<Divider my="xs" />
|
||||||
|
|
||||||
|
{availableSpaces.map((space) => (
|
||||||
|
<Menu.Item
|
||||||
|
key={space.id}
|
||||||
|
onClick={() => handleSpaceSelect(space.id)}
|
||||||
|
>
|
||||||
|
<Group flex="1" gap="xs">
|
||||||
|
<Avatar
|
||||||
|
color="initials"
|
||||||
|
variant="filled"
|
||||||
|
name={space.name}
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
<Text size="sm" fw={500} style={{ flex: 1 }} truncate>
|
||||||
|
{space.name}
|
||||||
|
</Text>
|
||||||
|
{selectedSpaceId === space.id && <IconCheck size={20} />}
|
||||||
|
</Group>
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
</ScrollArea.Autosize>
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
|
|
||||||
|
<Menu
|
||||||
|
shadow="md"
|
||||||
|
width={220}
|
||||||
|
position="bottom-start"
|
||||||
|
zIndex={getDefaultZIndex("max")}
|
||||||
|
>
|
||||||
|
<Menu.Target>
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
color="gray"
|
||||||
|
size="sm"
|
||||||
|
rightSection={<IconChevronDown size={14} />}
|
||||||
|
leftSection={<IconFileDescription size={16} />}
|
||||||
|
className={classes.filterButton}
|
||||||
|
fw={500}
|
||||||
|
>
|
||||||
|
{contentType
|
||||||
|
? `Type: ${contentTypeOptions.find((opt) => opt.value === contentType)?.label || contentType}`
|
||||||
|
: "Type"}
|
||||||
|
</Button>
|
||||||
|
</Menu.Target>
|
||||||
|
<Menu.Dropdown>
|
||||||
|
{contentTypeOptions.map((option) => (
|
||||||
|
<Menu.Item
|
||||||
|
key={option.value}
|
||||||
|
onClick={() =>
|
||||||
|
!option.disabled &&
|
||||||
|
contentType !== option.value &&
|
||||||
|
handleFilterChange("contentType", option.value)
|
||||||
|
}
|
||||||
|
disabled={option.disabled}
|
||||||
|
>
|
||||||
|
<Group flex="1" gap="xs">
|
||||||
|
<div>
|
||||||
|
<Text size="sm">{option.label}</Text>
|
||||||
|
{option.disabled && (
|
||||||
|
<Badge size="xs" mt={4}>
|
||||||
|
Enterprise
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{contentType === option.value && <IconCheck size={20} />}
|
||||||
|
</Group>
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { Spotlight } from "@mantine/spotlight";
|
||||||
|
import { IconSearch } from "@tabler/icons-react";
|
||||||
|
import React, { useState, useMemo } from "react";
|
||||||
|
import { useDebouncedValue } from "@mantine/hooks";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { searchSpotlightStore } from "../constants.ts";
|
||||||
|
import { SearchSpotlightFilters } from "./search-spotlight-filters.tsx";
|
||||||
|
import { useUnifiedSearch } from "../hooks/use-unified-search.ts";
|
||||||
|
import { SearchResultItem } from "./search-result-item.tsx";
|
||||||
|
import { useLicense } from "@/ee/hooks/use-license.tsx";
|
||||||
|
|
||||||
|
interface SearchSpotlightProps {
|
||||||
|
spaceId?: string;
|
||||||
|
}
|
||||||
|
export function SearchSpotlight({ spaceId }: SearchSpotlightProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { hasLicenseKey } = useLicense();
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
const [debouncedSearchQuery] = useDebouncedValue(query, 300);
|
||||||
|
const [filters, setFilters] = useState<{
|
||||||
|
spaceId?: string | null;
|
||||||
|
contentType?: string;
|
||||||
|
}>({
|
||||||
|
contentType: "page",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build unified search params
|
||||||
|
const searchParams = useMemo(() => {
|
||||||
|
const params: any = {
|
||||||
|
query: debouncedSearchQuery,
|
||||||
|
contentType: filters.contentType || "page", // Only used for frontend routing
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle space filtering - only pass spaceId if a specific space is selected
|
||||||
|
if (filters.spaceId) {
|
||||||
|
params.spaceId = filters.spaceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}, [debouncedSearchQuery, filters]);
|
||||||
|
|
||||||
|
const { data: searchResults, isLoading } = useUnifiedSearch(searchParams);
|
||||||
|
|
||||||
|
// Determine result type for rendering
|
||||||
|
const isAttachmentSearch =
|
||||||
|
filters.contentType === "attachment" && hasLicenseKey;
|
||||||
|
|
||||||
|
const resultItems = (searchResults || []).map((result) => (
|
||||||
|
<SearchResultItem
|
||||||
|
key={result.id}
|
||||||
|
result={result}
|
||||||
|
isAttachmentResult={isAttachmentSearch}
|
||||||
|
showSpace={!filters.spaceId}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
const handleFiltersChange = (newFilters: any) => {
|
||||||
|
setFilters(newFilters);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Spotlight.Root
|
||||||
|
size="xl"
|
||||||
|
maxHeight={600}
|
||||||
|
store={searchSpotlightStore}
|
||||||
|
query={query}
|
||||||
|
onQueryChange={setQuery}
|
||||||
|
scrollable
|
||||||
|
overlayProps={{
|
||||||
|
backgroundOpacity: 0.55,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Spotlight.Search
|
||||||
|
placeholder={t("Search...")}
|
||||||
|
leftSection={<IconSearch size={20} stroke={1.5} />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: "4px 16px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SearchSpotlightFilters
|
||||||
|
onFiltersChange={handleFiltersChange}
|
||||||
|
spaceId={spaceId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Spotlight.ActionsList>
|
||||||
|
{query.length === 0 && resultItems.length === 0 && (
|
||||||
|
<Spotlight.Empty>{t("Start typing to search...")}</Spotlight.Empty>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{query.length > 0 && !isLoading && resultItems.length === 0 && (
|
||||||
|
<Spotlight.Empty>{t("No results found...")}</Spotlight.Empty>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{resultItems.length > 0 && <>{resultItems}</>}
|
||||||
|
</Spotlight.ActionsList>
|
||||||
|
</Spotlight.Root>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
+7
-1
@@ -9,6 +9,7 @@ import { buildSharedPageUrl } from "@/features/page/page.utils.ts";
|
|||||||
import { getPageIcon } from "@/lib";
|
import { getPageIcon } from "@/lib";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { shareSearchSpotlightStore } from "@/features/search/constants.ts";
|
import { shareSearchSpotlightStore } from "@/features/search/constants.ts";
|
||||||
|
import DOMPurify from "dompurify";
|
||||||
|
|
||||||
interface ShareSearchSpotlightProps {
|
interface ShareSearchSpotlightProps {
|
||||||
shareId?: string;
|
shareId?: string;
|
||||||
@@ -47,7 +48,12 @@ export function ShareSearchSpotlight({ shareId }: ShareSearchSpotlightProps) {
|
|||||||
<Text
|
<Text
|
||||||
opacity={0.6}
|
opacity={0.6}
|
||||||
size="xs"
|
size="xs"
|
||||||
dangerouslySetInnerHTML={{ __html: page.highlight }}
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: DOMPurify.sanitize(page.highlight, {
|
||||||
|
ALLOWED_TAGS: ["mark", "em", "strong", "b"],
|
||||||
|
ALLOWED_ATTR: []
|
||||||
|
}),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { useQuery, UseQueryResult } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
searchPage,
|
||||||
|
searchAttachments,
|
||||||
|
} from "@/features/search/services/search-service";
|
||||||
|
import {
|
||||||
|
IAttachmentSearch,
|
||||||
|
IPageSearch,
|
||||||
|
IPageSearchParams,
|
||||||
|
} from "@/features/search/types/search.types";
|
||||||
|
import { useLicense } from "@/ee/hooks/use-license";
|
||||||
|
|
||||||
|
export type UnifiedSearchResult = IPageSearch | IAttachmentSearch;
|
||||||
|
|
||||||
|
export interface UseUnifiedSearchParams extends IPageSearchParams {
|
||||||
|
contentType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUnifiedSearch(
|
||||||
|
params: UseUnifiedSearchParams,
|
||||||
|
): UseQueryResult<UnifiedSearchResult[], Error> {
|
||||||
|
const { hasLicenseKey } = useLicense();
|
||||||
|
|
||||||
|
const isAttachmentSearch =
|
||||||
|
params.contentType === "attachment" && hasLicenseKey;
|
||||||
|
const searchType = isAttachmentSearch ? "attachment" : "page";
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["unified-search", searchType, params],
|
||||||
|
queryFn: async () => {
|
||||||
|
// Remove contentType from backend params since it's only used for frontend routing
|
||||||
|
const { contentType, ...backendParams } = params;
|
||||||
|
|
||||||
|
if (isAttachmentSearch) {
|
||||||
|
return await searchAttachments(backendParams);
|
||||||
|
} else {
|
||||||
|
return await searchPage(backendParams);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled: !!params.query,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
import { useQuery, UseQueryResult } from "@tanstack/react-query";
|
import { useQuery, UseQueryResult } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
|
searchAttachments,
|
||||||
searchPage,
|
searchPage,
|
||||||
searchShare,
|
searchShare,
|
||||||
searchSuggestions,
|
searchSuggestions,
|
||||||
} from "@/features/search/services/search-service";
|
} from '@/features/search/services/search-service';
|
||||||
import {
|
import {
|
||||||
|
IAttachmentSearch,
|
||||||
IPageSearch,
|
IPageSearch,
|
||||||
IPageSearchParams,
|
IPageSearchParams,
|
||||||
ISuggestionResult,
|
ISuggestionResult,
|
||||||
SearchSuggestionParams,
|
SearchSuggestionParams,
|
||||||
} from "@/features/search/types/search.types";
|
} from '@/features/search/types/search.types';
|
||||||
|
|
||||||
export function usePageSearchQuery(
|
export function usePageSearchQuery(
|
||||||
params: IPageSearchParams,
|
params: IPageSearchParams,
|
||||||
@@ -41,3 +43,13 @@ export function useShareSearchQuery(
|
|||||||
enabled: !!params.query,
|
enabled: !!params.query,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useAttachmentSearchQuery(
|
||||||
|
params: IPageSearchParams,
|
||||||
|
): UseQueryResult<IAttachmentSearch[], Error> {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["attachment-search", params],
|
||||||
|
queryFn: () => searchAttachments(params),
|
||||||
|
enabled: !!params.query,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
import { Group, Center, Text } from "@mantine/core";
|
|
||||||
import { Spotlight } from "@mantine/spotlight";
|
|
||||||
import { IconSearch } from "@tabler/icons-react";
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { useDebouncedValue } from "@mantine/hooks";
|
|
||||||
import { usePageSearchQuery } from "@/features/search/queries/search-query";
|
|
||||||
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
|
||||||
import { getPageIcon } from "@/lib";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { searchSpotlightStore } from "./constants";
|
|
||||||
|
|
||||||
interface SearchSpotlightProps {
|
|
||||||
spaceId?: string;
|
|
||||||
}
|
|
||||||
export function SearchSpotlight({ spaceId }: SearchSpotlightProps) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [query, setQuery] = useState("");
|
|
||||||
const [debouncedSearchQuery] = useDebouncedValue(query, 300);
|
|
||||||
|
|
||||||
const { data: searchResults } = usePageSearchQuery({
|
|
||||||
query: debouncedSearchQuery,
|
|
||||||
spaceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const pages = (
|
|
||||||
searchResults && searchResults.length > 0 ? searchResults : []
|
|
||||||
).map((page) => (
|
|
||||||
<Spotlight.Action
|
|
||||||
key={page.id}
|
|
||||||
component={Link}
|
|
||||||
//@ts-ignore
|
|
||||||
to={buildPageUrl(page.space.slug, page.slugId, page.title)}
|
|
||||||
style={{ userSelect: "none" }}
|
|
||||||
>
|
|
||||||
<Group wrap="nowrap" w="100%">
|
|
||||||
<Center>{getPageIcon(page?.icon)}</Center>
|
|
||||||
|
|
||||||
<div style={{ flex: 1 }}>
|
|
||||||
<Text>{page.title}</Text>
|
|
||||||
|
|
||||||
{page?.highlight && (
|
|
||||||
<Text
|
|
||||||
opacity={0.6}
|
|
||||||
size="xs"
|
|
||||||
dangerouslySetInnerHTML={{ __html: page.highlight }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Group>
|
|
||||||
</Spotlight.Action>
|
|
||||||
));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Spotlight.Root
|
|
||||||
store={searchSpotlightStore}
|
|
||||||
query={query}
|
|
||||||
onQueryChange={setQuery}
|
|
||||||
scrollable
|
|
||||||
overlayProps={{
|
|
||||||
backgroundOpacity: 0.55,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Spotlight.Search
|
|
||||||
placeholder={t("Search...")}
|
|
||||||
leftSection={<IconSearch size={20} stroke={1.5} />}
|
|
||||||
/>
|
|
||||||
<Spotlight.ActionsList>
|
|
||||||
{query.length === 0 && pages.length === 0 && (
|
|
||||||
<Spotlight.Empty>{t("Start typing to search...")}</Spotlight.Empty>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{query.length > 0 && pages.length === 0 && (
|
|
||||||
<Spotlight.Empty>{t("No results found...")}</Spotlight.Empty>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{pages.length > 0 && pages}
|
|
||||||
</Spotlight.ActionsList>
|
|
||||||
</Spotlight.Root>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import api from "@/lib/api-client";
|
import api from "@/lib/api-client";
|
||||||
import {
|
import {
|
||||||
|
IAttachmentSearch,
|
||||||
IPageSearch,
|
IPageSearch,
|
||||||
IPageSearchParams,
|
IPageSearchParams,
|
||||||
ISuggestionResult,
|
ISuggestionResult,
|
||||||
SearchSuggestionParams,
|
SearchSuggestionParams,
|
||||||
} from "@/features/search/types/search.types";
|
} from '@/features/search/types/search.types';
|
||||||
|
|
||||||
export async function searchPage(
|
export async function searchPage(
|
||||||
params: IPageSearchParams,
|
params: IPageSearchParams,
|
||||||
@@ -26,3 +27,10 @@ export async function searchShare(
|
|||||||
const req = await api.post<IPageSearch[]>("/search/share-search", params);
|
const req = await api.post<IPageSearch[]>("/search/share-search", params);
|
||||||
return req.data;
|
return req.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function searchAttachments(
|
||||||
|
params: IPageSearchParams,
|
||||||
|
): Promise<IAttachmentSearch[]> {
|
||||||
|
const req = await api.post<IAttachmentSearch[]>("/search-attachments", params);
|
||||||
|
return req.data;
|
||||||
|
}
|
||||||
|
|||||||
@@ -37,3 +37,25 @@ export interface IPageSearchParams {
|
|||||||
spaceId?: string;
|
spaceId?: string;
|
||||||
shareId?: string;
|
shareId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IAttachmentSearch {
|
||||||
|
id: string;
|
||||||
|
fileName: string;
|
||||||
|
pageId: string;
|
||||||
|
creatorId: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
rank: string;
|
||||||
|
highlight: string;
|
||||||
|
space: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
icon: string;
|
||||||
|
};
|
||||||
|
page: {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
slugId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import {
|
|||||||
SearchControl,
|
SearchControl,
|
||||||
SearchMobileControl,
|
SearchMobileControl,
|
||||||
} from "@/features/search/components/search-control.tsx";
|
} from "@/features/search/components/search-control.tsx";
|
||||||
import { ShareSearchSpotlight } from "@/features/search/share-search-spotlight";
|
import { ShareSearchSpotlight } from "@/features/search/components/share-search-spotlight.tsx";
|
||||||
import { shareSearchSpotlight } from "@/features/search/constants";
|
import { shareSearchSpotlight } from "@/features/search/constants";
|
||||||
import ShareBranding from '@/features/share/components/share-branding.tsx';
|
import ShareBranding from '@/features/share/components/share-branding.tsx';
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
import classes from "./space-sidebar.module.css";
|
import classes from "./space-sidebar.module.css";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { SearchSpotlight } from "@/features/search/search-spotlight.tsx";
|
|
||||||
import { treeApiAtom } from "@/features/page/tree/atoms/tree-api-atom.ts";
|
import { treeApiAtom } from "@/features/page/tree/atoms/tree-api-atom.ts";
|
||||||
import { Link, useLocation, useParams } from "react-router-dom";
|
import { Link, useLocation, useParams } from "react-router-dom";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
@@ -195,8 +194,6 @@ export function SpaceSidebar() {
|
|||||||
onClose={closeSettings}
|
onClose={closeSettings}
|
||||||
spaceId={space?.slug}
|
spaceId={space?.slug}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SearchSpotlight spaceId={space.id} />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const APP_ROUTE = {
|
const APP_ROUTE = {
|
||||||
HOME: "/home",
|
HOME: "/home",
|
||||||
SPACES: "/spaces",
|
SPACES: "/spaces",
|
||||||
|
SEARCH: "/search",
|
||||||
AUTH: {
|
AUTH: {
|
||||||
LOGIN: "/login",
|
LOGIN: "/login",
|
||||||
SIGNUP: "/signup",
|
SIGNUP: "/signup",
|
||||||
|
|||||||
@@ -67,6 +67,7 @@
|
|||||||
"kysely": "^0.28.2",
|
"kysely": "^0.28.2",
|
||||||
"kysely-migration-cli": "^0.4.2",
|
"kysely-migration-cli": "^0.4.2",
|
||||||
"ldapts": "^7.4.0",
|
"ldapts": "^7.4.0",
|
||||||
|
"mammoth": "^1.10.0",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"nanoid": "3.3.11",
|
"nanoid": "3.3.11",
|
||||||
"nestjs-kysely": "^1.2.0",
|
"nestjs-kysely": "^1.2.0",
|
||||||
@@ -76,6 +77,7 @@
|
|||||||
"p-limit": "^6.2.0",
|
"p-limit": "^6.2.0",
|
||||||
"passport-google-oauth20": "^2.0.0",
|
"passport-google-oauth20": "^2.0.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
|
"pdfjs-dist": "^5.4.54",
|
||||||
"pg": "^8.16.0",
|
"pg": "^8.16.0",
|
||||||
"pg-tsquery": "^8.4.2",
|
"pg-tsquery": "^8.4.2",
|
||||||
"postmark": "^4.0.5",
|
"postmark": "^4.0.5",
|
||||||
|
|||||||
@@ -87,3 +87,12 @@ export function extractBearerTokenFromHeader(
|
|||||||
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
||||||
return type === 'Bearer' ? token : undefined;
|
return type === 'Bearer' ? token : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hasLicenseOrEE(opts: {
|
||||||
|
licenseKey: string;
|
||||||
|
plan: string;
|
||||||
|
isCloud: boolean;
|
||||||
|
}): boolean {
|
||||||
|
const { licenseKey, plan, isCloud } = opts;
|
||||||
|
return Boolean(licenseKey) || (isCloud && plan === 'business');
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,12 +3,15 @@ import { OnWorkerEvent, Processor, WorkerHost } from '@nestjs/bullmq';
|
|||||||
import { Job } from 'bullmq';
|
import { Job } from 'bullmq';
|
||||||
import { AttachmentService } from '../services/attachment.service';
|
import { AttachmentService } from '../services/attachment.service';
|
||||||
import { QueueJob, QueueName } from 'src/integrations/queue/constants';
|
import { QueueJob, QueueName } from 'src/integrations/queue/constants';
|
||||||
import { Space } from '@docmost/db/types/entity.types';
|
import { ModuleRef } from '@nestjs/core';
|
||||||
|
|
||||||
@Processor(QueueName.ATTACHMENT_QUEUE)
|
@Processor(QueueName.ATTACHMENT_QUEUE)
|
||||||
export class AttachmentProcessor extends WorkerHost implements OnModuleDestroy {
|
export class AttachmentProcessor extends WorkerHost implements OnModuleDestroy {
|
||||||
private readonly logger = new Logger(AttachmentProcessor.name);
|
private readonly logger = new Logger(AttachmentProcessor.name);
|
||||||
constructor(private readonly attachmentService: AttachmentService) {
|
constructor(
|
||||||
|
private readonly attachmentService: AttachmentService,
|
||||||
|
private moduleRef: ModuleRef,
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,6 +28,33 @@ export class AttachmentProcessor extends WorkerHost implements OnModuleDestroy {
|
|||||||
job.data.pageId,
|
job.data.pageId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
job.name === QueueJob.ATTACHMENT_INDEX_CONTENT ||
|
||||||
|
job.name === QueueJob.ATTACHMENT_INDEXING
|
||||||
|
) {
|
||||||
|
let AttachmentEeModule: any;
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
|
AttachmentEeModule = require('./../../../ee/attachments-ee/attachment-ee.service');
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(
|
||||||
|
'Attachment enterprise module requested but EE module not bundled in this build',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const attachmentEeService = this.moduleRef.get(
|
||||||
|
AttachmentEeModule.AttachmentEeService,
|
||||||
|
{ strict: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (job.name === QueueJob.ATTACHMENT_INDEX_CONTENT) {
|
||||||
|
await attachmentEeService.indexAttachment(job.data.attachmentId);
|
||||||
|
} else if (job.name === QueueJob.ATTACHMENT_INDEXING) {
|
||||||
|
await attachmentEeService.indexAttachments(
|
||||||
|
job.data.workspaceId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ import { executeTx } from '@docmost/db/utils';
|
|||||||
import { UserRepo } from '@docmost/db/repos/user/user.repo';
|
import { UserRepo } from '@docmost/db/repos/user/user.repo';
|
||||||
import { WorkspaceRepo } from '@docmost/db/repos/workspace/workspace.repo';
|
import { WorkspaceRepo } from '@docmost/db/repos/workspace/workspace.repo';
|
||||||
import { SpaceRepo } from '@docmost/db/repos/space/space.repo';
|
import { SpaceRepo } from '@docmost/db/repos/space/space.repo';
|
||||||
|
import { InjectQueue } from '@nestjs/bullmq';
|
||||||
|
import { QueueJob, QueueName } from '../../../integrations/queue/constants';
|
||||||
|
import { Queue } from 'bullmq';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AttachmentService {
|
export class AttachmentService {
|
||||||
@@ -33,6 +36,7 @@ export class AttachmentService {
|
|||||||
private readonly workspaceRepo: WorkspaceRepo,
|
private readonly workspaceRepo: WorkspaceRepo,
|
||||||
private readonly spaceRepo: SpaceRepo,
|
private readonly spaceRepo: SpaceRepo,
|
||||||
@InjectKysely() private readonly db: KyselyDB,
|
@InjectKysely() private readonly db: KyselyDB,
|
||||||
|
@InjectQueue(QueueName.ATTACHMENT_QUEUE) private attachmentQueue: Queue,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async uploadFile(opts: {
|
async uploadFile(opts: {
|
||||||
@@ -99,6 +103,23 @@ export class AttachmentService {
|
|||||||
pageId,
|
pageId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only index PDFs and DOCX files
|
||||||
|
if (['.pdf', '.docx'].includes(attachment.fileExt.toLowerCase())) {
|
||||||
|
await this.attachmentQueue.add(
|
||||||
|
QueueJob.ATTACHMENT_INDEX_CONTENT,
|
||||||
|
{
|
||||||
|
attachmentId: attachmentId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attempts: 2,
|
||||||
|
backoff: {
|
||||||
|
type: 'exponential',
|
||||||
|
delay: 10000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// delete uploaded file on error
|
// delete uploaded file on error
|
||||||
this.logger.error(err);
|
this.logger.error(err);
|
||||||
@@ -367,4 +388,5 @@ export class AttachmentService {
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,13 @@ import {
|
|||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
import { PartialType } from '@nestjs/mapped-types';
|
|
||||||
import { CreateWorkspaceDto } from '../../workspace/dto/create-workspace.dto';
|
|
||||||
|
|
||||||
export class SearchDTO {
|
export class SearchDTO {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsString()
|
@IsString()
|
||||||
query: string;
|
query: string;
|
||||||
|
|
||||||
@IsNotEmpty()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
spaceId: string;
|
spaceId: string;
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { Public } from '../../common/decorators/public.decorator';
|
|||||||
import { ShareRepo } from '@docmost/db/repos/share/share.repo';
|
import { ShareRepo } from '@docmost/db/repos/share/share.repo';
|
||||||
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
||||||
import { EnvironmentService } from '../../integrations/environment/environment.service';
|
import { EnvironmentService } from '../../integrations/environment/environment.service';
|
||||||
|
import { hasLicenseOrEE } from '../../common/helpers';
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Controller('shares')
|
@Controller('shares')
|
||||||
@@ -65,9 +66,11 @@ export class ShareController {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...(await this.shareService.getSharedPage(dto, workspace.id)),
|
...(await this.shareService.getSharedPage(dto, workspace.id)),
|
||||||
hasLicenseKey:
|
hasLicenseKey: hasLicenseOrEE({
|
||||||
Boolean(workspace.licenseKey) ||
|
licenseKey: workspace.licenseKey,
|
||||||
(this.environmentService.isCloud() && workspace.plan === 'business'),
|
isCloud: this.environmentService.isCloud(),
|
||||||
|
plan: workspace.plan,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,9 +178,11 @@ export class ShareController {
|
|||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
...(await this.shareService.getShareTree(dto.shareId, workspace.id)),
|
...(await this.shareService.getShareTree(dto.shareId, workspace.id)),
|
||||||
hasLicenseKey:
|
hasLicenseKey: hasLicenseOrEE({
|
||||||
Boolean(workspace.licenseKey) ||
|
licenseKey: workspace.licenseKey,
|
||||||
(this.environmentService.isCloud() && workspace.plan === 'business'),
|
isCloud: this.environmentService.isCloud(),
|
||||||
|
plan: workspace.plan,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { type Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await db.schema
|
||||||
|
.alterTable('attachments')
|
||||||
|
.addColumn('text_content', 'text', (col) => col)
|
||||||
|
.addColumn('tsv', sql`tsvector`, (col) => col)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createIndex('attachments_tsv_idx')
|
||||||
|
.on('attachments')
|
||||||
|
.using('GIN')
|
||||||
|
.column('tsv')
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await db.schema
|
||||||
|
.alterTable('attachments')
|
||||||
|
.dropIndex('attachments_tsv_idx')
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.alterTable('attachments')
|
||||||
|
.dropColumn('text_content')
|
||||||
|
.dropColumn('tsv')
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
@@ -12,6 +12,23 @@ import {
|
|||||||
export class AttachmentRepo {
|
export class AttachmentRepo {
|
||||||
constructor(@InjectKysely() private readonly db: KyselyDB) {}
|
constructor(@InjectKysely() private readonly db: KyselyDB) {}
|
||||||
|
|
||||||
|
private baseFields: Array<keyof Attachment> = [
|
||||||
|
'id',
|
||||||
|
'fileName',
|
||||||
|
'filePath',
|
||||||
|
'fileSize',
|
||||||
|
'fileExt',
|
||||||
|
'mimeType',
|
||||||
|
'type',
|
||||||
|
'creatorId',
|
||||||
|
'pageId',
|
||||||
|
'spaceId',
|
||||||
|
'workspaceId',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
'deletedAt',
|
||||||
|
];
|
||||||
|
|
||||||
async findById(
|
async findById(
|
||||||
attachmentId: string,
|
attachmentId: string,
|
||||||
opts?: {
|
opts?: {
|
||||||
@@ -22,7 +39,7 @@ export class AttachmentRepo {
|
|||||||
|
|
||||||
return db
|
return db
|
||||||
.selectFrom('attachments')
|
.selectFrom('attachments')
|
||||||
.selectAll()
|
.select(this.baseFields)
|
||||||
.where('id', '=', attachmentId)
|
.where('id', '=', attachmentId)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
@@ -36,7 +53,7 @@ export class AttachmentRepo {
|
|||||||
return db
|
return db
|
||||||
.insertInto('attachments')
|
.insertInto('attachments')
|
||||||
.values(insertableAttachment)
|
.values(insertableAttachment)
|
||||||
.returningAll()
|
.returning(this.baseFields)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +67,7 @@ export class AttachmentRepo {
|
|||||||
|
|
||||||
return db
|
return db
|
||||||
.selectFrom('attachments')
|
.selectFrom('attachments')
|
||||||
.selectAll()
|
.select(this.baseFields)
|
||||||
.where('spaceId', '=', spaceId)
|
.where('spaceId', '=', spaceId)
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
@@ -64,6 +81,7 @@ export class AttachmentRepo {
|
|||||||
.updateTable('attachments')
|
.updateTable('attachments')
|
||||||
.set(updatableAttachment)
|
.set(updatableAttachment)
|
||||||
.where('pageId', 'in', pageIds)
|
.where('pageId', 'in', pageIds)
|
||||||
|
.returning(this.baseFields)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +93,7 @@ export class AttachmentRepo {
|
|||||||
.updateTable('attachments')
|
.updateTable('attachments')
|
||||||
.set(updatableAttachment)
|
.set(updatableAttachment)
|
||||||
.where('id', '=', attachmentId)
|
.where('id', '=', attachmentId)
|
||||||
.returningAll()
|
.returning(this.baseFields)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
@@ -37,6 +37,8 @@ export interface Attachments {
|
|||||||
mimeType: string | null;
|
mimeType: string | null;
|
||||||
pageId: string | null;
|
pageId: string | null;
|
||||||
spaceId: string | null;
|
spaceId: string | null;
|
||||||
|
textContent: string | null;
|
||||||
|
tsv: string | null;
|
||||||
type: string | null;
|
type: string | null;
|
||||||
updatedAt: Generated<Timestamp>;
|
updatedAt: Generated<Timestamp>;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
|
|||||||
+1
-1
Submodule apps/server/src/ee updated: 505081bb85...3775df6013
@@ -9,6 +9,8 @@ export enum QueueName {
|
|||||||
export enum QueueJob {
|
export enum QueueJob {
|
||||||
SEND_EMAIL = 'send-email',
|
SEND_EMAIL = 'send-email',
|
||||||
DELETE_SPACE_ATTACHMENTS = 'delete-space-attachments',
|
DELETE_SPACE_ATTACHMENTS = 'delete-space-attachments',
|
||||||
|
ATTACHMENT_INDEX_CONTENT = 'attachment-index-content',
|
||||||
|
ATTACHMENT_INDEXING = 'attachment-indexing',
|
||||||
DELETE_PAGE_ATTACHMENTS = 'delete-page-attachments',
|
DELETE_PAGE_ATTACHMENTS = 'delete-page-attachments',
|
||||||
PAGE_CONTENT_UPDATE = 'page-content-update',
|
PAGE_CONTENT_UPDATE = 'page-content-update',
|
||||||
|
|
||||||
|
|||||||
Generated
+182
@@ -537,6 +537,9 @@ importers:
|
|||||||
ldapts:
|
ldapts:
|
||||||
specifier: ^7.4.0
|
specifier: ^7.4.0
|
||||||
version: 7.4.0
|
version: 7.4.0
|
||||||
|
mammoth:
|
||||||
|
specifier: ^1.10.0
|
||||||
|
version: 1.10.0
|
||||||
mime-types:
|
mime-types:
|
||||||
specifier: ^2.1.35
|
specifier: ^2.1.35
|
||||||
version: 2.1.35
|
version: 2.1.35
|
||||||
@@ -564,6 +567,9 @@ importers:
|
|||||||
passport-jwt:
|
passport-jwt:
|
||||||
specifier: ^4.0.1
|
specifier: ^4.0.1
|
||||||
version: 4.0.1
|
version: 4.0.1
|
||||||
|
pdfjs-dist:
|
||||||
|
specifier: ^5.4.54
|
||||||
|
version: 5.4.54
|
||||||
pg:
|
pg:
|
||||||
specifier: ^8.16.0
|
specifier: ^8.16.0
|
||||||
version: 8.16.0
|
version: 8.16.0
|
||||||
@@ -2619,6 +2625,70 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-android-arm64@0.1.77':
|
||||||
|
resolution: {integrity: sha512-jC8YX0rbAnu9YrLK1A52KM2HX9EDjrJSCLVuBf9Dsov4IC6GgwMLS2pwL9GFLJnSZBFgdwnA84efBehHT9eshA==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-darwin-arm64@0.1.77':
|
||||||
|
resolution: {integrity: sha512-VFaCaCgAV0+hPwXajDIiHaaGx4fVCuUVYp/CxCGXmTGz699ngIEBx3Sa2oDp0uk3X+6RCRLueb7vD44BKBiPIg==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-darwin-x64@0.1.77':
|
||||||
|
resolution: {integrity: sha512-uD2NSkf6I4S3o0POJDwweK85FE4rfLNA2N714MgiEEMMw5AmupfSJGgpYzcyEXtPzdaca6rBfKcqNvzR1+EyLQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.77':
|
||||||
|
resolution: {integrity: sha512-03GxMMZGhHRQxiA4gyoKT6iQSz8xnA6T9PAfg/WNJnbkVMFZG782DwUJUb39QIZ1uE1euMCPnDgWAJ092MmgJQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-arm64-gnu@0.1.77':
|
||||||
|
resolution: {integrity: sha512-ZO+d2gRU9JU1Bb7SgJcJ1k9wtRMCpSWjJAJ+2phhu0Lw5As8jYXXXmLKmMTGs1bOya2dBMYDLzwp7KS/S/+aCA==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-arm64-musl@0.1.77':
|
||||||
|
resolution: {integrity: sha512-S1KtnP1+nWs2RApzNkdNf8X4trTLrHaY7FivV61ZRaL8NvuGOkSkKa+gWN2iedIGFEDz6gecpl/JAUSewwFXYg==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-riscv64-gnu@0.1.77':
|
||||||
|
resolution: {integrity: sha512-A4YIKFYUwDtrSzCtdCAO5DYmRqlhCVKHdpq0+dBGPnIEhOQDFkPBTfoTAjO3pjlEnorlfKmNMOH21sKQg2esGA==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-x64-gnu@0.1.77':
|
||||||
|
resolution: {integrity: sha512-Lt6Sef5l0+5O1cSZ8ysO0JI+x+rSrqZyXs5f7+kVkCAOVq8X5WTcDVbvWvEs2aRhrWTp5y25Jf2Bn+3IcNHOuQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-x64-musl@0.1.77':
|
||||||
|
resolution: {integrity: sha512-NiNFvC+D+omVeJ3IjYlIbyt/igONSABVe9z0ZZph29epHgZYu4eHwV9osfpRt1BGGOAM8LkFrHk4LBdn2EDymA==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-win32-x64-msvc@0.1.77':
|
||||||
|
resolution: {integrity: sha512-fP6l0hZiWykyjvpZTS3sI46iib8QEflbPakNoUijtwyxRuOPTTBfzAWZUz5z2vKpJJ/8r305wnZeZ8lhsBHY5A==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@napi-rs/canvas@0.1.77':
|
||||||
|
resolution: {integrity: sha512-N9w2DkEKE1AXGp3q55GBOP6BEoFrqChDiFqJtKViTpQCWNOSVuMz7LkoGehbnpxtidppbsC36P0kCZNqJKs29w==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
|
||||||
'@napi-rs/wasm-runtime@0.2.4':
|
'@napi-rs/wasm-runtime@0.2.4':
|
||||||
resolution: {integrity: sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ==}
|
resolution: {integrity: sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ==}
|
||||||
|
|
||||||
@@ -4948,6 +5018,9 @@ packages:
|
|||||||
bl@4.1.0:
|
bl@4.1.0:
|
||||||
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
||||||
|
|
||||||
|
bluebird@3.4.7:
|
||||||
|
resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==}
|
||||||
|
|
||||||
bluebird@3.7.2:
|
bluebird@3.7.2:
|
||||||
resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==}
|
resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==}
|
||||||
|
|
||||||
@@ -5679,6 +5752,9 @@ packages:
|
|||||||
dijkstrajs@1.0.3:
|
dijkstrajs@1.0.3:
|
||||||
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
|
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
|
||||||
|
|
||||||
|
dingbat-to-unicode@1.0.1:
|
||||||
|
resolution: {integrity: sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==}
|
||||||
|
|
||||||
dnd-core@14.0.1:
|
dnd-core@14.0.1:
|
||||||
resolution: {integrity: sha512-+PVS2VPTgKFPYWo3vAFEA8WPbTf7/xo43TifH9G8S1KqnrQu0o77A3unrF5yOugy4mIz7K5wAVFHUcha7wsz6A==}
|
resolution: {integrity: sha512-+PVS2VPTgKFPYWo3vAFEA8WPbTf7/xo43TifH9G8S1KqnrQu0o77A3unrF5yOugy4mIz7K5wAVFHUcha7wsz6A==}
|
||||||
|
|
||||||
@@ -5727,6 +5803,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==}
|
resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
duck@0.1.12:
|
||||||
|
resolution: {integrity: sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==}
|
||||||
|
|
||||||
eastasianwidth@0.2.0:
|
eastasianwidth@0.2.0:
|
||||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||||
|
|
||||||
@@ -7181,6 +7260,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
lop@0.4.2:
|
||||||
|
resolution: {integrity: sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==}
|
||||||
|
|
||||||
lowlight@3.3.0:
|
lowlight@3.3.0:
|
||||||
resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==}
|
resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==}
|
||||||
|
|
||||||
@@ -7227,6 +7309,11 @@ packages:
|
|||||||
makeerror@1.0.12:
|
makeerror@1.0.12:
|
||||||
resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==}
|
resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==}
|
||||||
|
|
||||||
|
mammoth@1.10.0:
|
||||||
|
resolution: {integrity: sha512-9HOmqt8uJ5rz7q8XrECU5gRjNftCq4GNG0YIrA6f9iQPCeLgpvgcmRBHi9NQWJQIpT/MAXeg1oKliAK1xoB3eg==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
mantine-form-zod-resolver@1.3.0:
|
mantine-form-zod-resolver@1.3.0:
|
||||||
resolution: {integrity: sha512-XlXXkJCYuUuOllW0zedYW+m/lbdFQ/bso1Vz+pJOYkxgjhoGvzN2EXWCS2+0iTOT9Q7WnOwWvHmvpTJN3PxSXw==}
|
resolution: {integrity: sha512-XlXXkJCYuUuOllW0zedYW+m/lbdFQ/bso1Vz+pJOYkxgjhoGvzN2EXWCS2+0iTOT9Q7WnOwWvHmvpTJN3PxSXw==}
|
||||||
engines: {node: '>=16.6.0'}
|
engines: {node: '>=16.6.0'}
|
||||||
@@ -7686,6 +7773,9 @@ packages:
|
|||||||
optics-ts@2.4.1:
|
optics-ts@2.4.1:
|
||||||
resolution: {integrity: sha512-HaYzMHvC80r7U/LqAd4hQyopDezC60PO2qF5GuIwALut2cl5rK1VWHsqTp0oqoJJWjiv6uXKqsO+Q2OO0C3MmQ==}
|
resolution: {integrity: sha512-HaYzMHvC80r7U/LqAd4hQyopDezC60PO2qF5GuIwALut2cl5rK1VWHsqTp0oqoJJWjiv6uXKqsO+Q2OO0C3MmQ==}
|
||||||
|
|
||||||
|
option@0.2.4:
|
||||||
|
resolution: {integrity: sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==}
|
||||||
|
|
||||||
optionator@0.9.3:
|
optionator@0.9.3:
|
||||||
resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==}
|
resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -7834,6 +7924,10 @@ packages:
|
|||||||
pause@0.0.1:
|
pause@0.0.1:
|
||||||
resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==}
|
resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==}
|
||||||
|
|
||||||
|
pdfjs-dist@5.4.54:
|
||||||
|
resolution: {integrity: sha512-TBAiTfQw89gU/Z4LW98Vahzd2/LoCFprVGvGbTgFt+QCB1F+woyOPmNNVgLa6djX9Z9GGTnj7qE1UzpOVJiINw==}
|
||||||
|
engines: {node: '>=20.16.0 || >=22.3.0'}
|
||||||
|
|
||||||
peberminta@0.9.0:
|
peberminta@0.9.0:
|
||||||
resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
|
resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
|
||||||
|
|
||||||
@@ -9212,6 +9306,9 @@ packages:
|
|||||||
unbox-primitive@1.0.2:
|
unbox-primitive@1.0.2:
|
||||||
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
|
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
|
||||||
|
|
||||||
|
underscore@1.13.7:
|
||||||
|
resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==}
|
||||||
|
|
||||||
undici-types@6.20.0:
|
undici-types@6.20.0:
|
||||||
resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==}
|
resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==}
|
||||||
|
|
||||||
@@ -9580,6 +9677,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
|
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
|
||||||
engines: {node: '>=4.0.0'}
|
engines: {node: '>=4.0.0'}
|
||||||
|
|
||||||
|
xmlbuilder@10.1.1:
|
||||||
|
resolution: {integrity: sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==}
|
||||||
|
engines: {node: '>=4.0'}
|
||||||
|
|
||||||
xmlbuilder@11.0.1:
|
xmlbuilder@11.0.1:
|
||||||
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
|
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
@@ -12452,6 +12553,50 @@ snapshots:
|
|||||||
'@msgpackr-extract/msgpackr-extract-win32-x64@3.0.2':
|
'@msgpackr-extract/msgpackr-extract-win32-x64@3.0.2':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-android-arm64@0.1.77':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-darwin-arm64@0.1.77':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-darwin-x64@0.1.77':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.77':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-arm64-gnu@0.1.77':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-arm64-musl@0.1.77':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-riscv64-gnu@0.1.77':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-x64-gnu@0.1.77':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-x64-musl@0.1.77':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-win32-x64-msvc@0.1.77':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas@0.1.77':
|
||||||
|
optionalDependencies:
|
||||||
|
'@napi-rs/canvas-android-arm64': 0.1.77
|
||||||
|
'@napi-rs/canvas-darwin-arm64': 0.1.77
|
||||||
|
'@napi-rs/canvas-darwin-x64': 0.1.77
|
||||||
|
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.77
|
||||||
|
'@napi-rs/canvas-linux-arm64-gnu': 0.1.77
|
||||||
|
'@napi-rs/canvas-linux-arm64-musl': 0.1.77
|
||||||
|
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.77
|
||||||
|
'@napi-rs/canvas-linux-x64-gnu': 0.1.77
|
||||||
|
'@napi-rs/canvas-linux-x64-musl': 0.1.77
|
||||||
|
'@napi-rs/canvas-win32-x64-msvc': 0.1.77
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/wasm-runtime@0.2.4':
|
'@napi-rs/wasm-runtime@0.2.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@emnapi/core': 1.2.0
|
'@emnapi/core': 1.2.0
|
||||||
@@ -15049,6 +15194,8 @@ snapshots:
|
|||||||
inherits: 2.0.4
|
inherits: 2.0.4
|
||||||
readable-stream: 3.6.2
|
readable-stream: 3.6.2
|
||||||
|
|
||||||
|
bluebird@3.4.7: {}
|
||||||
|
|
||||||
bluebird@3.7.2: {}
|
bluebird@3.7.2: {}
|
||||||
|
|
||||||
boolbase@1.0.0: {}
|
boolbase@1.0.0: {}
|
||||||
@@ -15805,6 +15952,8 @@ snapshots:
|
|||||||
|
|
||||||
dijkstrajs@1.0.3: {}
|
dijkstrajs@1.0.3: {}
|
||||||
|
|
||||||
|
dingbat-to-unicode@1.0.1: {}
|
||||||
|
|
||||||
dnd-core@14.0.1:
|
dnd-core@14.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@react-dnd/asap': 4.0.1
|
'@react-dnd/asap': 4.0.1
|
||||||
@@ -15862,6 +16011,10 @@ snapshots:
|
|||||||
|
|
||||||
dotenv@16.4.7: {}
|
dotenv@16.4.7: {}
|
||||||
|
|
||||||
|
duck@0.1.12:
|
||||||
|
dependencies:
|
||||||
|
underscore: 1.13.7
|
||||||
|
|
||||||
eastasianwidth@0.2.0: {}
|
eastasianwidth@0.2.0: {}
|
||||||
|
|
||||||
ecdsa-sig-formatter@1.0.11:
|
ecdsa-sig-formatter@1.0.11:
|
||||||
@@ -17707,6 +17860,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
js-tokens: 4.0.0
|
js-tokens: 4.0.0
|
||||||
|
|
||||||
|
lop@0.4.2:
|
||||||
|
dependencies:
|
||||||
|
duck: 0.1.12
|
||||||
|
option: 0.2.4
|
||||||
|
underscore: 1.13.7
|
||||||
|
|
||||||
lowlight@3.3.0:
|
lowlight@3.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/hast': 3.0.4
|
'@types/hast': 3.0.4
|
||||||
@@ -17753,6 +17912,19 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tmpl: 1.0.5
|
tmpl: 1.0.5
|
||||||
|
|
||||||
|
mammoth@1.10.0:
|
||||||
|
dependencies:
|
||||||
|
'@xmldom/xmldom': 0.8.10
|
||||||
|
argparse: 1.0.10
|
||||||
|
base64-js: 1.5.1
|
||||||
|
bluebird: 3.4.7
|
||||||
|
dingbat-to-unicode: 1.0.1
|
||||||
|
jszip: 3.10.1
|
||||||
|
lop: 0.4.2
|
||||||
|
path-is-absolute: 1.0.1
|
||||||
|
underscore: 1.13.7
|
||||||
|
xmlbuilder: 10.1.1
|
||||||
|
|
||||||
mantine-form-zod-resolver@1.3.0(@mantine/form@8.1.3(react@18.3.1))(zod@3.25.56):
|
mantine-form-zod-resolver@1.3.0(@mantine/form@8.1.3(react@18.3.1))(zod@3.25.56):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@mantine/form': 8.1.3(react@18.3.1)
|
'@mantine/form': 8.1.3(react@18.3.1)
|
||||||
@@ -18337,6 +18509,8 @@ snapshots:
|
|||||||
|
|
||||||
optics-ts@2.4.1: {}
|
optics-ts@2.4.1: {}
|
||||||
|
|
||||||
|
option@0.2.4: {}
|
||||||
|
|
||||||
optionator@0.9.3:
|
optionator@0.9.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aashutoshrathi/word-wrap': 1.2.6
|
'@aashutoshrathi/word-wrap': 1.2.6
|
||||||
@@ -18498,6 +18672,10 @@ snapshots:
|
|||||||
|
|
||||||
pause@0.0.1: {}
|
pause@0.0.1: {}
|
||||||
|
|
||||||
|
pdfjs-dist@5.4.54:
|
||||||
|
optionalDependencies:
|
||||||
|
'@napi-rs/canvas': 0.1.77
|
||||||
|
|
||||||
peberminta@0.9.0: {}
|
peberminta@0.9.0: {}
|
||||||
|
|
||||||
peek-readable@7.0.0: {}
|
peek-readable@7.0.0: {}
|
||||||
@@ -20005,6 +20183,8 @@ snapshots:
|
|||||||
has-symbols: 1.0.3
|
has-symbols: 1.0.3
|
||||||
which-boxed-primitive: 1.1.0
|
which-boxed-primitive: 1.1.0
|
||||||
|
|
||||||
|
underscore@1.13.7: {}
|
||||||
|
|
||||||
undici-types@6.20.0: {}
|
undici-types@6.20.0: {}
|
||||||
|
|
||||||
undici@7.10.0: {}
|
undici@7.10.0: {}
|
||||||
@@ -20332,6 +20512,8 @@ snapshots:
|
|||||||
sax: 1.4.1
|
sax: 1.4.1
|
||||||
xmlbuilder: 11.0.1
|
xmlbuilder: 11.0.1
|
||||||
|
|
||||||
|
xmlbuilder@10.1.1: {}
|
||||||
|
|
||||||
xmlbuilder@11.0.1: {}
|
xmlbuilder@11.0.1: {}
|
||||||
|
|
||||||
xmlbuilder@15.1.1: {}
|
xmlbuilder@15.1.1: {}
|
||||||
|
|||||||
Reference in New Issue
Block a user