diff --git a/apps/client/src/features/editor/components/search-and-replace/search-navigation-dialog.tsx b/apps/client/src/features/editor/components/search-and-replace/search-navigation-dialog.tsx index 63cdc0656..e210cfe43 100644 --- a/apps/client/src/features/editor/components/search-and-replace/search-navigation-dialog.tsx +++ b/apps/client/src/features/editor/components/search-and-replace/search-navigation-dialog.tsx @@ -1,6 +1,6 @@ import { ActionIcon, Dialog, Flex, Text, Tooltip } from "@mantine/core"; 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 React, { useEffect, useState } from "react"; import classes from "./search-replace.module.css"; @@ -20,19 +20,11 @@ interface SearchNavigationEvent extends CustomEvent { function SearchNavigationDialog({ editor }: SearchNavigationDialogProps) { const {t} = useTranslation() const [open, setOpen] = useState(false); - const [searchTerms, setSearchTerms] = useState([]); const [resultState, setResultState] = useState({ resultIndex: 0, resultsLength: 0, }); - const handleSetSearchTerms = (editor: Editor, terms: string[]) => { - setSearchTerms(terms); - if (isEditorReady(editor)) { - editor.commands.setSearchTerms(terms); - } - }; - const goToSelection = () => { if (!isEditorReady(editor)) return; @@ -73,7 +65,7 @@ function SearchNavigationDialog({ editor }: SearchNavigationDialogProps) { if (!terms?.length || !isEditorReady(editor)) return; setOpen(true); - handleSetSearchTerms(editor, terms); + editor.commands.setSearchTerms(terms); editor.commands.setWholeWord(wholeWord); editor.commands.resetIndex(); @@ -116,7 +108,7 @@ function SearchNavigationDialog({ editor }: SearchNavigationDialogProps) { }, [editor, open]); const close = () => { - handleSetSearchTerms(editor, [""]); + editor.commands.setSearchTerms([""]); setOpen(false); }; diff --git a/apps/client/src/features/editor/page-editor.tsx b/apps/client/src/features/editor/page-editor.tsx index 91daf9b0f..69a3e7412 100644 --- a/apps/client/src/features/editor/page-editor.tsx +++ b/apps/client/src/features/editor/page-editor.tsx @@ -196,7 +196,6 @@ function CollabPageEditor({ const documentState = useDocumentVisibility(); const { pageSlug } = useParams(); const [searchParams] = useSearchParams(); - const searchQueries = searchParams.getAll("q"); const slugId = extractPageSlugId(pageSlug); const currentPageEditMode = useAtomValue(currentPageEditModeAtom); const canScroll = useCallback( @@ -419,6 +418,8 @@ function CollabPageEditor({ const [showStatic, setShowStatic] = useState(true); const appliedSearchRef = useRef(false); + const searchQueries = searchParams.getAll("q"); + const match = searchParams.get("m"); useEffect(() => { if (!editor || editor.isDestroyed) return; @@ -430,7 +431,7 @@ function CollabPageEditor({ document.dispatchEvent( new CustomEvent("openSearchNavigationDialog", { - detail: { searchTerms: searchQueries, wholeWord: true }, + detail: { searchTerms: searchQueries, wholeWord: match === "whole" }, }) ); diff --git a/apps/client/src/features/page/page.utils.ts b/apps/client/src/features/page/page.utils.ts index c078ee766..62d5ecbf3 100644 --- a/apps/client/src/features/page/page.utils.ts +++ b/apps/client/src/features/page/page.utils.ts @@ -25,12 +25,34 @@ const buildPageSlug = (pageSlugId: string, pageTitle?: string): string => { 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 = ( spaceName: string, pageSlugId: string, pageTitle?: string, anchorId?: string, search?: string[], + wholeWord?: boolean, ): string => { let url: string; if (spaceName === undefined) { @@ -39,20 +61,7 @@ export const buildPageUrl = ( url = `/s/${spaceName}/p/${buildPageSlug(pageSlugId, pageTitle)}`; } - if (search?.length > 0) { - 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}`; - } - } + url = appendSearchParams(url, search, wholeWord); return anchorId ? `${url}#${anchorId}` : url; }; @@ -63,8 +72,9 @@ export const buildSharedPageUrl = (opts: { pageTitle?: string; anchorId?: string; search?: string[]; + wholeWord?: boolean; }): string => { - const { shareId, pageSlugId, pageTitle, anchorId, search } = opts; + const { shareId, pageSlugId, pageTitle, anchorId, search, wholeWord } = opts; let url: string; if (!shareId) { url = `/share/p/${buildPageSlug(pageSlugId, pageTitle)}`; @@ -72,20 +82,7 @@ export const buildSharedPageUrl = (opts: { url = `/share/${shareId}/p/${buildPageSlug(pageSlugId, pageTitle)}`; } - if (search?.length > 0) { - 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}`; - } - } + url = appendSearchParams(url, search, wholeWord); return anchorId ? `${url}#${anchorId}` : url; }; diff --git a/apps/client/src/features/search/components/search-result-item.tsx b/apps/client/src/features/search/components/search-result-item.tsx index 010b7a746..86e139036 100644 --- a/apps/client/src/features/search/components/search-result-item.tsx +++ b/apps/client/src/features/search/components/search-result-item.tsx @@ -102,7 +102,8 @@ export function SearchResultItem({ pageResult.slugId, pageResult.title, undefined, - pageResult.matchedText + pageResult.matchedText, + pageResult.wholeWord )} style={{ userSelect: "none" }} > diff --git a/apps/client/src/features/search/types/search.types.ts b/apps/client/src/features/search/types/search.types.ts index 9d6f76999..155e9932e 100644 --- a/apps/client/src/features/search/types/search.types.ts +++ b/apps/client/src/features/search/types/search.types.ts @@ -15,6 +15,7 @@ export interface IPageSearch { rank: string; highlight: string; matchedText: string[]; + wholeWord: boolean; space: Partial; } diff --git a/apps/server/src/core/search/dto/search-response.dto.ts b/apps/server/src/core/search/dto/search-response.dto.ts index 5fe63eb51..e16ff8898 100644 --- a/apps/server/src/core/search/dto/search-response.dto.ts +++ b/apps/server/src/core/search/dto/search-response.dto.ts @@ -8,7 +8,8 @@ export class SearchResponseDto { creatorId: string; rank: number; highlight: string; - matchedText?: string[]; + matchedText: string[]; + wholeWord: boolean; createdAt: Date; updatedAt: Date; space: Partial; diff --git a/apps/server/src/core/search/search.service.ts b/apps/server/src/core/search/search.service.ts index 7740e5a37..644241a04 100644 --- a/apps/server/src/core/search/search.service.ts +++ b/apps/server/src/core/search/search.service.ts @@ -149,11 +149,16 @@ export class SearchService { .replace(/\r\n|\r|\n/g, ' ') .replace(/\s+/g, ' '); - result.matchedText = Array.from( - result.highlight.matchAll(/([^<]*)<\/b>/gi), - (match) => match[1], - ); + result.matchedText = [ + ...new Set( + Array.from( + result.highlight.matchAll(/([^<]*)<\/b>/gi), + (match) => match[1], + ), + ), + ]; + result.wholeWord = true return result; });