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={ +