From bbd7a115009bdf102da55a70ff713a00930fbda1 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Thu, 8 Oct 2026 15:40:55 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F(frontend)=20extract=20the=20?= =?UTF-8?q?mention=20search=20from=20the=20interlinking?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The search opened by the slash menu and by typing @ was a state of the interlinking inline content, so it could only ever propose docs. Give it its own transient mentionSearchInline inline content, hosted in a new Mention folder, parent of every kind of mention. The interlinking becomes one of its kinds and only keeps the doc search group. The persisted interlinkingLinkInline stays unchanged: documents already saved keep their links, and the former search states render nothing. Signed-off-by: Anthony LC --- .../__tests__/app-impress/doc-editor.spec.ts | 2 +- .../doc-editor/components/BlockNoteEditor.tsx | 2 + .../InterlinkingLinkInlineContent.tsx | 119 ++++++++-------- .../Interlinking/InterlinkingSearchGroup.tsx | 124 +++++++++++++++++ .../Interlinking/LinkSelected.tsx | 0 .../__tests__/InterlinkingLink.test.tsx | 60 ++++++++ .../InterlinkingLinkInlineContent.test.tsx | 4 +- .../__tests__/LinkSelected.test.tsx | 0 .../__tests__/pasteInterlink.test.ts | 0 .../{ => Mention}/Interlinking/index.ts | 1 + .../Interlinking/pasteInterlink.ts | 0 .../Mention/MentionSearchInlineContent.tsx | 56 ++++++++ .../SearchPage.tsx => Mention/Search.tsx} | 128 ++++-------------- .../__tests__/Search.test.tsx} | 37 +++-- .../custom-inline-content/Mention/index.ts | 3 + .../custom-inline-content/Mention/types.ts | 8 ++ .../components/custom-inline-content/index.ts | 2 +- .../docs/doc-editor/hook/useShortcuts.tsx | 2 +- .../__tests__/mentionSearchMapping.test.tsx | 43 ++++++ .../inline-content-mapping/index.ts | 3 + .../mentionSearchDocx.tsx | 9 ++ .../mentionSearchODT.tsx | 7 + .../mentionSearchPDF.tsx | 7 + .../features/docs/doc-export/mappingDocx.tsx | 6 +- .../features/docs/doc-export/mappingODT.ts | 6 +- .../features/docs/doc-export/mappingPDF.tsx | 6 +- .../components/DocSearchContent.tsx | 6 +- 27 files changed, 461 insertions(+), 180 deletions(-) rename src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/{ => Mention}/Interlinking/InterlinkingLinkInlineContent.tsx (62%) create mode 100644 src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Mention/Interlinking/InterlinkingSearchGroup.tsx rename src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/{ => Mention}/Interlinking/LinkSelected.tsx (100%) create mode 100644 src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Mention/Interlinking/__tests__/InterlinkingLink.test.tsx rename src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/{ => Mention}/Interlinking/__tests__/InterlinkingLinkInlineContent.test.tsx (95%) rename src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/{ => Mention}/Interlinking/__tests__/LinkSelected.test.tsx (100%) rename src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/{ => Mention}/Interlinking/__tests__/pasteInterlink.test.ts (100%) rename src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/{ => Mention}/Interlinking/index.ts (65%) rename src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/{ => Mention}/Interlinking/pasteInterlink.ts (100%) create mode 100644 src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Mention/MentionSearchInlineContent.tsx rename src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/{Interlinking/SearchPage.tsx => Mention/Search.tsx} (67%) rename src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/{Interlinking/__tests__/SearchPage.test.tsx => Mention/__tests__/Search.test.tsx} (87%) create mode 100644 src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Mention/index.ts create mode 100644 src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Mention/types.ts create mode 100644 src/frontend/apps/impress/src/features/docs/doc-export/__tests__/mentionSearchMapping.test.tsx create mode 100644 src/frontend/apps/impress/src/features/docs/doc-export/inline-content-mapping/mentionSearchDocx.tsx create mode 100644 src/frontend/apps/impress/src/features/docs/doc-export/inline-content-mapping/mentionSearchODT.tsx create mode 100644 src/frontend/apps/impress/src/features/docs/doc-export/inline-content-mapping/mentionSearchPDF.tsx diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts index 7aabc689c..4baf345b1 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts @@ -448,7 +448,7 @@ test.describe('Doc Editor', () => { await page.getByText('Link a doc').first().click(); const input = page.locator( - "span[data-inline-content-type='interlinkingLinkInline'] input", + "span[data-inline-content-type='mentionSearchInline'] input", ); const searchContainer = page.locator('.quick-search-container'); diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx index 879473f3d..4c2e774a0 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx @@ -72,6 +72,7 @@ const localesBNAI = BlockNoteAI?.localesAI || {}; import { createSafeCodeBlockSpec } from './custom-blocks/CodeBlock'; import { InterlinkingLinkInlineContent, + MentionSearchInlineContent, getPastedDocInterlink, } from './custom-inline-content'; import XLMultiColumn from './xl-multi-column'; @@ -93,6 +94,7 @@ const baseBlockNoteSchema = withPageBreak( inlineContentSpecs: { ...defaultInlineContentSpecs, interlinkingLinkInline: InterlinkingLinkInlineContent, + mentionSearchInline: MentionSearchInlineContent, math: createReactInlineMathSpec(), }, }), diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/InterlinkingLinkInlineContent.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Mention/Interlinking/InterlinkingLinkInlineContent.tsx similarity index 62% rename from src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/InterlinkingLinkInlineContent.tsx rename to src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Mention/Interlinking/InterlinkingLinkInlineContent.tsx index 9d96099a9..47133fb79 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/InterlinkingLinkInlineContent.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Mention/Interlinking/InterlinkingLinkInlineContent.tsx @@ -11,11 +11,14 @@ import { DocsBlockNoteEditor } from '@/docs/doc-editor/types'; import { useCreateChildDocTree, useDocStore } from '@/docs/doc-management'; import { LinkSelected } from './LinkSelected'; -import { SearchPage } from './SearchPage'; export type InterlinkingLinkInlineContentType = { type: 'interlinkingLinkInline'; propSchema: { + /** + * @deprecated Only set by the former search state of this inline content, + * now `mentionSearchInline`. Kept to read the documents saved before. + */ disabled?: { default: false; values: [true, false]; @@ -26,6 +29,9 @@ export type InterlinkingLinkInlineContentType = { blockId?: { default: ''; }; + /** + * @deprecated See `disabled`. + */ trigger?: { default: '/'; values: readonly ['/', '@']; @@ -59,56 +65,63 @@ export const InterlinkingLinkInlineContent = createReactInlineContentSpec< content: 'none', }, { - /** - * Can have 3 render states: - * 1. Disabled state: when the inline content is disabled, it renders nothing - * 2. Search state: when the inline content has no docId, it renders the search page - * 3. Linked state: when the inline content has a docId, it renders the linked doc. - * - * Info: We keep everything in the same inline content to easily preserve - * the element position when switching between states - */ - render: (props) => { - const { disabled, docId, blockId } = props.inlineContent.props; - - if (disabled) { - return null; - } - - if (docId) { - /** - * Should not happen - */ - if (!uuidValidate(docId)) { - return ( - { - props.updateInlineContent({ - type: 'interlinkingLinkInline', - props: { - disabled: true, - }, - }); - }} - /> - ); - } - - return ( - - ); - } - - return ; - }, + render: (props) => ( + { + props.updateInlineContent({ + type: 'interlinkingLinkInline', + props: { + disabled: true, + }, + }); + }} + /> + ), }, ); +interface InterlinkingLinkProps { + docId?: string; + blockId?: string; + disabled?: boolean; + isEditable: boolean; + onDisable: () => void; +} + +/** + * Can have 2 render states: + * 1. Linked state: when the inline content has a docId, it renders the linked doc. + * 2. Empty state: otherwise, it renders nothing. It is what remains in the + * documents saved while this inline content was also the search of the + * mentions, now `mentionSearchInline`. + */ +export const InterlinkingLink = ({ + docId, + blockId, + disabled, + isEditable, + onDisable, +}: InterlinkingLinkProps) => { + if (disabled || !docId) { + return null; + } + + /** + * Should not happen + */ + if (!uuidValidate(docId)) { + return ; + } + + return ( + + ); +}; + export const getInterlinkinghMenuItems = ( editor: DocsBlockNoteEditor, t: TFunction<'translation', undefined>, @@ -121,7 +134,7 @@ export const getInterlinkinghMenuItems = ( onItemClick: () => { editor.insertInlineContent([ { - type: 'interlinkingLinkInline', + type: 'mentionSearchInline', props: { trigger: '/', }, @@ -156,18 +169,18 @@ export const useGetInterlinkingMenuItems = () => { const DisableInvalidInterlink = ({ docId, - onUpdateInlineContent, + onDisable, }: { docId: string; - onUpdateInlineContent: () => void; + onDisable: () => void; }) => { useEffect(() => { Sentry.captureException(new Error(`Invalid docId: ${docId}`), { extra: { info: 'InterlinkingInlineContent' }, }); - onUpdateInlineContent(); - }, [docId, onUpdateInlineContent]); + onDisable(); + }, [docId, onDisable]); return null; }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Mention/Interlinking/InterlinkingSearchGroup.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Mention/Interlinking/InterlinkingSearchGroup.tsx new file mode 100644 index 000000000..5c3e09b33 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Mention/Interlinking/InterlinkingSearchGroup.tsx @@ -0,0 +1,124 @@ +import { useTreeContext } from '@gouvfr-lasuite/ui-components'; +import { useTranslation } from 'react-i18next'; +import { css } from 'styled-components'; + +import DocIcon from '@/assets/icons/ui-kit/doc.svg'; +import ArrowIcon from '@/assets/icons/ui-kit/keyboard_return.svg'; +import { Box } from '@/components/Box'; +import { Text } from '@/components/Text'; +import { QuickSearchItemContent } from '@/components/quick-search/QuickSearchItemContent'; +import { DocSearchContent } from '@/docs/doc-search'; +import { type DocSearch } from '@/docs/doc-search/api/useSearchDocs'; +import { useTrans } from '@/features/docs/doc-management/hooks/useTrans'; +import { type Doc } from '@/features/docs/doc-management/types'; +import { getEmojiAndTitle } from '@/features/docs/doc-management/utils'; + +import { type MentionedInlineContent } from '../types'; + +interface InterlinkingSearchGroupProps { + search: string; + onPick: (inlineContent: MentionedInlineContent) => void; + /** Separate the docs from the section above. */ + withSeparator?: boolean; + onResults?: (results: DocSearch[]) => void; + onLoadingChange?: (loading: boolean) => void; +} + +export const InterlinkingSearchGroup = ({ + search, + onPick, + withSeparator, + onResults, + onLoadingChange, +}: InterlinkingSearchGroupProps) => { + const { t } = useTranslation(); + const { untitledDocument } = useTrans(); + const treeContext = useTreeContext(); + + return ( + <> + {withSeparator && ( + + )} + { + onPick({ + type: 'interlinkingLinkInline', + props: { docId: doc.id }, + }); + }} + renderSearchElement={(doc) => { + const { emoji, titleWithoutEmoji } = getEmojiAndTitle( + doc.title || untitledDocument, + ); + + return ( + + + {emoji ? ( + {emoji} + ) : ( + + + + {titleWithoutEmoji} + + + } + right={ +