typesense support

This commit is contained in:
Salihu
2026-08-30 00:12:48 +01:00
parent 435c4d3129
commit 1b73d7668c
7 changed files with 46 additions and 48 deletions
@@ -1,6 +1,6 @@
import { ActionIcon, Dialog, Flex, Text, Tooltip } from "@mantine/core"; import { ActionIcon, Dialog, Flex, Text, Tooltip } from "@mantine/core";
import { IconArrowNarrowDown, IconArrowNarrowUp, IconX } from "@tabler/icons-react"; import { IconArrowNarrowDown, IconArrowNarrowUp, IconX } from "@tabler/icons-react";
import { Editor, useEditor } from "@tiptap/react"; import { useEditor } from "@tiptap/react";
import { isEditorReady } from "@docmost/editor-ext"; import { isEditorReady } from "@docmost/editor-ext";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import classes from "./search-replace.module.css"; import classes from "./search-replace.module.css";
@@ -20,19 +20,11 @@ interface SearchNavigationEvent extends CustomEvent {
function SearchNavigationDialog({ editor }: SearchNavigationDialogProps) { function SearchNavigationDialog({ editor }: SearchNavigationDialogProps) {
const {t} = useTranslation() const {t} = useTranslation()
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [searchTerms, setSearchTerms] = useState<string[]>([]);
const [resultState, setResultState] = useState({ const [resultState, setResultState] = useState({
resultIndex: 0, resultIndex: 0,
resultsLength: 0, resultsLength: 0,
}); });
const handleSetSearchTerms = (editor: Editor, terms: string[]) => {
setSearchTerms(terms);
if (isEditorReady(editor)) {
editor.commands.setSearchTerms(terms);
}
};
const goToSelection = () => { const goToSelection = () => {
if (!isEditorReady(editor)) return; if (!isEditorReady(editor)) return;
@@ -73,7 +65,7 @@ function SearchNavigationDialog({ editor }: SearchNavigationDialogProps) {
if (!terms?.length || !isEditorReady(editor)) return; if (!terms?.length || !isEditorReady(editor)) return;
setOpen(true); setOpen(true);
handleSetSearchTerms(editor, terms); editor.commands.setSearchTerms(terms);
editor.commands.setWholeWord(wholeWord); editor.commands.setWholeWord(wholeWord);
editor.commands.resetIndex(); editor.commands.resetIndex();
@@ -116,7 +108,7 @@ function SearchNavigationDialog({ editor }: SearchNavigationDialogProps) {
}, [editor, open]); }, [editor, open]);
const close = () => { const close = () => {
handleSetSearchTerms(editor, [""]); editor.commands.setSearchTerms([""]);
setOpen(false); setOpen(false);
}; };
@@ -196,7 +196,6 @@ function CollabPageEditor({
const documentState = useDocumentVisibility(); const documentState = useDocumentVisibility();
const { pageSlug } = useParams(); const { pageSlug } = useParams();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const searchQueries = searchParams.getAll("q");
const slugId = extractPageSlugId(pageSlug); const slugId = extractPageSlugId(pageSlug);
const currentPageEditMode = useAtomValue(currentPageEditModeAtom); const currentPageEditMode = useAtomValue(currentPageEditModeAtom);
const canScroll = useCallback( const canScroll = useCallback(
@@ -419,6 +418,8 @@ function CollabPageEditor({
const [showStatic, setShowStatic] = useState(true); const [showStatic, setShowStatic] = useState(true);
const appliedSearchRef = useRef(false); const appliedSearchRef = useRef(false);
const searchQueries = searchParams.getAll("q");
const match = searchParams.get("m");
useEffect(() => { useEffect(() => {
if (!editor || editor.isDestroyed) return; if (!editor || editor.isDestroyed) return;
@@ -430,7 +431,7 @@ function CollabPageEditor({
document.dispatchEvent( document.dispatchEvent(
new CustomEvent("openSearchNavigationDialog", { new CustomEvent("openSearchNavigationDialog", {
detail: { searchTerms: searchQueries, wholeWord: true }, detail: { searchTerms: searchQueries, wholeWord: match === "whole" },
}) })
); );
+26 -29
View File
@@ -25,12 +25,34 @@ const buildPageSlug = (pageSlugId: string, pageTitle?: string): string => {
return `${titleSlug}-${pageSlugId}`; return `${titleSlug}-${pageSlugId}`;
}; };
function appendSearchParams(
url: string,
search?: string[],
wholeWord?: boolean,
): string {
const params = new URLSearchParams();
search
?.map((term) => term.trim())
.filter(Boolean)
.forEach((term) => params.append("q", term));
if (wholeWord) {
params.set("m", "whole");
}
const queryString = params.toString();
return queryString ? `${url}?${queryString}` : url;
}
export const buildPageUrl = ( export const buildPageUrl = (
spaceName: string, spaceName: string,
pageSlugId: string, pageSlugId: string,
pageTitle?: string, pageTitle?: string,
anchorId?: string, anchorId?: string,
search?: string[], search?: string[],
wholeWord?: boolean,
): string => { ): string => {
let url: string; let url: string;
if (spaceName === undefined) { if (spaceName === undefined) {
@@ -39,20 +61,7 @@ export const buildPageUrl = (
url = `/s/${spaceName}/p/${buildPageSlug(pageSlugId, pageTitle)}`; url = `/s/${spaceName}/p/${buildPageSlug(pageSlugId, pageTitle)}`;
} }
if (search?.length > 0) { url = appendSearchParams(url, search, wholeWord);
const params = new URLSearchParams();
search
.map((term) => term.trim())
.filter(Boolean)
.forEach((term) => params.append("q", term));
const queryString = params.toString();
if (queryString) {
url += `?${queryString}`;
}
}
return anchorId ? `${url}#${anchorId}` : url; return anchorId ? `${url}#${anchorId}` : url;
}; };
@@ -63,8 +72,9 @@ export const buildSharedPageUrl = (opts: {
pageTitle?: string; pageTitle?: string;
anchorId?: string; anchorId?: string;
search?: string[]; search?: string[];
wholeWord?: boolean;
}): string => { }): string => {
const { shareId, pageSlugId, pageTitle, anchorId, search } = opts; const { shareId, pageSlugId, pageTitle, anchorId, search, wholeWord } = opts;
let url: string; let url: string;
if (!shareId) { if (!shareId) {
url = `/share/p/${buildPageSlug(pageSlugId, pageTitle)}`; url = `/share/p/${buildPageSlug(pageSlugId, pageTitle)}`;
@@ -72,20 +82,7 @@ export const buildSharedPageUrl = (opts: {
url = `/share/${shareId}/p/${buildPageSlug(pageSlugId, pageTitle)}`; url = `/share/${shareId}/p/${buildPageSlug(pageSlugId, pageTitle)}`;
} }
if (search?.length > 0) { url = appendSearchParams(url, search, wholeWord);
const params = new URLSearchParams();
search
.map((term) => term.trim())
.filter(Boolean)
.forEach((term) => params.append("q", term));
const queryString = params.toString();
if (queryString) {
url += `?${queryString}`;
}
}
return anchorId ? `${url}#${anchorId}` : url; return anchorId ? `${url}#${anchorId}` : url;
}; };
@@ -102,7 +102,8 @@ export function SearchResultItem({
pageResult.slugId, pageResult.slugId,
pageResult.title, pageResult.title,
undefined, undefined,
pageResult.matchedText pageResult.matchedText,
pageResult.wholeWord
)} )}
style={{ userSelect: "none" }} style={{ userSelect: "none" }}
> >
@@ -15,6 +15,7 @@ export interface IPageSearch {
rank: string; rank: string;
highlight: string; highlight: string;
matchedText: string[]; matchedText: string[];
wholeWord: boolean;
space: Partial<ISpace>; space: Partial<ISpace>;
} }
@@ -8,7 +8,8 @@ export class SearchResponseDto {
creatorId: string; creatorId: string;
rank: number; rank: number;
highlight: string; highlight: string;
matchedText?: string[]; matchedText: string[];
wholeWord: boolean;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
space: Partial<Space>; space: Partial<Space>;
@@ -149,11 +149,16 @@ export class SearchService {
.replace(/\r\n|\r|\n/g, ' ') .replace(/\r\n|\r|\n/g, ' ')
.replace(/\s+/g, ' '); .replace(/\s+/g, ' ');
result.matchedText = Array.from( result.matchedText = [
...new Set(
Array.from(
result.highlight.matchAll(/<b>([^<]*)<\/b>/gi), result.highlight.matchAll(/<b>([^<]*)<\/b>/gi),
(match) => match[1], (match) => match[1],
); ),
),
];
result.wholeWord = true
return result; return result;
}); });