♻️(frontend) extract the mention search from the interlinking

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 <anthony.le-courric@mail.numerique.gouv.fr>
This commit is contained in:
Anthony LC
2026-10-09 14:03:00 +02:00
parent dbf8fece4e
commit bbd7a11500
27 changed files with 461 additions and 180 deletions
@@ -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');
@@ -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(),
},
}),
@@ -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 (
<DisableInvalidInterlink
docId={docId}
onUpdateInlineContent={() => {
props.updateInlineContent({
type: 'interlinkingLinkInline',
props: {
disabled: true,
},
});
}}
/>
);
}
return (
<LinkSelected
docId={docId}
blockId={blockId}
isEditable={props.editor.isEditable}
/>
);
}
return <SearchPage {...props} />;
},
render: (props) => (
<InterlinkingLink
docId={props.inlineContent.props.docId}
blockId={props.inlineContent.props.blockId}
disabled={props.inlineContent.props.disabled}
isEditable={props.editor.isEditable}
onDisable={() => {
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 <DisableInvalidInterlink docId={docId} onDisable={onDisable} />;
}
return (
<LinkSelected docId={docId} blockId={blockId} isEditable={isEditable} />
);
};
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;
};
@@ -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<Doc>();
return (
<>
{withSeparator && (
<Box
role="separator"
$margin={{ vertical: 'xxs' }}
$css={css`
border-top: 1px solid
var(--c--contextuals--border--surface--primary);
`}
/>
)}
<DocSearchContent
groupName={t('Link a doc')}
hideGroupNameWhenEmpty
onResults={onResults}
onLoadingChange={onLoadingChange}
search={search}
parentDocId={treeContext?.root?.id}
isSearchNotMandatory
onSelect={(doc) => {
onPick({
type: 'interlinkingLinkInline',
props: { docId: doc.id },
});
}}
renderSearchElement={(doc) => {
const { emoji, titleWithoutEmoji } = getEmojiAndTitle(
doc.title || untitledDocument,
);
return (
<QuickSearchItemContent
left={
<Box
$direction="row"
$gap="0.2rem"
$align="center"
$padding={{
vertical: '0.5rem',
horizontal: '0.2rem',
}}
$width="100%"
>
<Box
$css={css`
width: 24px;
flex-shrink: 0;
`}
>
{emoji ? (
<Text $size="18px">{emoji}</Text>
) : (
<DocIcon
aria-hidden="true"
width="24px"
height="24px"
color="var(--c--contextuals--content--semantic--neutral--primary)"
/>
)}
</Box>
<Text
$size="sm"
$color="var(--c--contextuals--content--semantic--neutral--primary)"
spellCheck="false"
$weight="500"
>
{titleWithoutEmoji}
</Text>
</Box>
}
right={
<ArrowIcon
aria-hidden="true"
width="24px"
height="24px"
color="var(--c--contextuals--content--semantic--neutral--tertiary)"
/>
}
/>
);
}}
/>
</>
);
};
@@ -0,0 +1,60 @@
import { render, screen, waitFor } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { InterlinkingLink } from '../InterlinkingLinkInlineContent';
vi.mock('../LinkSelected', () => ({
LinkSelected: ({ docId, blockId }: { docId: string; blockId?: string }) => (
<span data-testid="link-selected">{`${docId}#${blockId ?? ''}`}</span>
),
}));
const DOC_ID = '6f1e4c9a-2f6b-4a3e-9d5c-1b2a3c4d5e6f';
/**
* The documents saved before the search of the mentions got its own inline
* content still hold `interlinkingLinkInline` in its former search states.
*/
describe('InterlinkingLink', () => {
it('renders the linked doc', () => {
render(
<InterlinkingLink
docId={DOC_ID}
blockId="block-1"
isEditable
onDisable={vi.fn()}
/>,
);
expect(screen.getByTestId('link-selected')).toHaveTextContent(
`${DOC_ID}#block-1`,
);
});
it('renders nothing for the former search state, without docId', () => {
const { container } = render(
<InterlinkingLink docId="" isEditable onDisable={vi.fn()} />,
);
expect(container).toBeEmptyDOMElement();
});
it('renders nothing for the former closed search state, disabled', () => {
const { container } = render(
<InterlinkingLink docId="" disabled isEditable onDisable={vi.fn()} />,
);
expect(container).toBeEmptyDOMElement();
});
it('disables a link to an invalid doc id', async () => {
const onDisable = vi.fn();
const { container } = render(
<InterlinkingLink docId="not-a-uuid" isEditable onDisable={onDisable} />,
);
expect(container).toBeEmptyDOMElement();
await waitFor(() => expect(onDisable).toHaveBeenCalledTimes(1));
});
});
@@ -26,7 +26,7 @@ const t = ((key: string) => key) as unknown as Parameters<
>[1];
describe('getInterlinkinghMenuItems', () => {
it('inserts an interlink inline content when the "link-doc" item is clicked', () => {
it('inserts the mention search inline content when the "link-doc" item is clicked', () => {
const insertInlineContent = vi.fn();
const editor = { insertInlineContent } as unknown as DocsBlockNoteEditor;
const createPage = vi.fn();
@@ -38,7 +38,7 @@ describe('getInterlinkinghMenuItems', () => {
expect(insertInlineContent).toHaveBeenCalledWith([
{
type: 'interlinkingLinkInline',
type: 'mentionSearchInline',
props: { trigger: '/' },
},
]);
@@ -1,2 +1,3 @@
export * from './InterlinkingLinkInlineContent';
export * from './InterlinkingSearchGroup';
export * from './pasteInterlink';
@@ -0,0 +1,56 @@
import { StyleSchema } from '@blocknote/core';
import { createReactInlineContentSpec } from '@blocknote/react';
import { Search } from './Search';
export type MentionSearchInlineContentType = {
type: 'mentionSearchInline';
propSchema: {
disabled?: {
default: false;
values: [true, false];
};
trigger?: {
default: '/';
values: readonly ['/', '@'];
};
};
content: 'none';
};
/**
* Transient inline content hosting the search of a mention. It replaces
* itself by the inline content of the kind of mention the user picked.
*
* Info: it is the parent of every kind of mention (Interlinking, ...), they
* do not know about it. Keep the type of the mentions themselves unchanged,
* they are persisted in the documents.
*/
export const MentionSearchInlineContent = createReactInlineContentSpec<
MentionSearchInlineContentType,
StyleSchema
>(
{
type: 'mentionSearchInline',
propSchema: {
disabled: {
default: false,
values: [true, false],
},
trigger: {
default: '/',
values: ['/', '@'],
},
},
content: 'none',
},
{
render: (props) => {
if (props.inlineContent.props.disabled) {
return null;
}
return <Search {...props} />;
},
},
);
@@ -1,28 +1,19 @@
import { StyleSchema } from '@blocknote/core';
import { ReactCustomInlineContentRenderProps } from '@blocknote/react';
import { useTreeContext } from '@gouvfr-lasuite/ui-components';
import { Popover } from '@mantine/core';
import type { KeyboardEvent } from 'react';
import { useEffect, useId, useRef, useState } from 'react';
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,
Card,
QuickSearch,
QuickSearchItemContent,
Text,
} from '@/components';
import { Box, Card, QuickSearch } from '@/components';
import { DocsBlockNoteEditor } from '@/docs/doc-editor/types';
import { Doc, getEmojiAndTitle, useTrans } from '@/docs/doc-management';
import { DocSearchContent } from '@/docs/doc-search';
import { useDocSearchFilterStore } from '@/docs/doc-search/stores/useDocSearchFilterStore';
import { useResponsiveStore } from '@/stores';
import { InterlinkingLinkInlineContentType } from './InterlinkingLinkInlineContent';
import { InterlinkingSearchGroup } from './Interlinking';
import { MentionSearchInlineContentType } from './MentionSearchInlineContent';
import { MentionedInlineContent } from './types';
const inputStyle = css`
background-color: transparent;
@@ -34,32 +25,37 @@ const inputStyle = css`
font-family: 'Inter';
`;
type ReactInterlinkingSearch = ReactCustomInlineContentRenderProps<
InterlinkingLinkInlineContentType,
type ReactMentionSearch = ReactCustomInlineContentRenderProps<
MentionSearchInlineContentType,
StyleSchema
>;
export const SearchPage = ({
/**
* The search replaces itself by the content the user picked. At runtime
* `updateInlineContent` accepts any inline content, but it is typed with the
* one of the search.
*/
type ReplaceByMentioned = (inlineContent: MentionedInlineContent) => void;
export const Search = ({
contentRef,
updateInlineContent,
editor,
inlineContent,
}: ReactInterlinkingSearch) => {
}: ReactMentionSearch) => {
const trigger = inlineContent.props.trigger;
const { t } = useTranslation();
const inputRef = useRef<HTMLInputElement>(null);
const [search, setSearch] = useState('');
const { isDesktop } = useResponsiveStore();
const { untitledDocument } = useTrans();
const isEditable = editor.isEditable;
const treeContext = useTreeContext<Doc>();
const modalRef = useRef<HTMLDivElement>(null);
const dropdownId = useId();
const [popoverOpened, setPopoverOpened] = useState(false);
const { setFilter } = useDocSearchFilterStore();
/**
* When the search page is opened, we set the search
* When the search is opened, we set the search
* target to 'current' to limit the search to the current
* document and its sub-documents.
*/
@@ -91,7 +87,7 @@ export const SearchPage = ({
}
updateInlineContent({
type: 'interlinkingLinkInline',
type: 'mentionSearchInline',
props: {
disabled: true,
},
@@ -106,6 +102,17 @@ export const SearchPage = ({
}
};
const pick = (mentioned: MentionedInlineContent) => {
if (!isEditable) {
return;
}
(updateInlineContent as unknown as ReplaceByMentioned)(mentioned);
contentRef(null);
editor.focus();
};
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Escape') {
e.preventDefault();
@@ -260,84 +267,7 @@ export const SearchPage = ({
$margin="sm"
$padding="none"
>
<DocSearchContent
groupName={t('Link a doc')}
search={search}
parentDocId={treeContext?.root?.id}
isSearchNotMandatory
onSelect={(doc) => {
if (!isEditable) {
return;
}
updateInlineContent({
type: 'interlinkingLinkInline',
props: {
docId: doc.id,
},
});
contentRef(null);
editor.focus();
}}
renderSearchElement={(doc) => {
const { emoji, titleWithoutEmoji } = getEmojiAndTitle(
doc.title || untitledDocument,
);
return (
<QuickSearchItemContent
left={
<Box
$direction="row"
$gap="0.2rem"
$align="center"
$padding={{
vertical: '0.5rem',
horizontal: '0.2rem',
}}
$width="100%"
>
<Box
$css={css`
width: 24px;
flex-shrink: 0;
`}
>
{emoji ? (
<Text $size="18px">{emoji}</Text>
) : (
<DocIcon
aria-hidden="true"
width="24px"
height="24px"
color="var(--c--contextuals--content--semantic--neutral--primary)"
/>
)}
</Box>
<Text
$size="sm"
$color="var(--c--contextuals--content--semantic--neutral--primary)"
spellCheck="false"
$weight="500"
>
{titleWithoutEmoji}
</Text>
</Box>
}
right={
<ArrowIcon
aria-hidden="true"
width="24px"
height="24px"
color="var(--c--contextuals--content--semantic--neutral--tertiary)"
/>
}
/>
);
}}
/>
<InterlinkingSearchGroup search={search} onPick={pick} />
</Card>
</QuickSearch>
</Box>
@@ -41,7 +41,7 @@ vi.mock('@/docs/doc-search', async () => {
};
});
import { SearchPage } from '../SearchPage';
import { Search } from '../Search';
// cmdk and Mantine rely on browser APIs jsdom doesn't implement.
beforeEach(() => {
@@ -75,7 +75,7 @@ beforeEach(() => {
);
});
const renderSearchPage = async ({ isEditable = true, trigger = '/' } = {}) => {
const renderSearch = async ({ isEditable = true, trigger = '/' } = {}) => {
const updateInlineContent = vi.fn();
const contentRef = vi.fn();
const insertInlineContent = vi.fn();
@@ -88,12 +88,12 @@ const renderSearchPage = async ({ isEditable = true, trigger = '/' } = {}) => {
} as unknown as DocsBlockNoteEditor;
render(
<SearchPage
<Search
editor={editor as any}
inlineContent={
{
type: 'interlinkingLinkInline',
props: { trigger, disabled: false, docId: '' },
type: 'mentionSearchInline',
props: { trigger, disabled: false },
} as any
}
updateInlineContent={updateInlineContent}
@@ -105,13 +105,13 @@ const renderSearchPage = async ({ isEditable = true, trigger = '/' } = {}) => {
{ wrapper: Wrapper },
);
// SearchPage focuses the input and opens the popover after a 100ms timeout.
// Search focuses the input and opens the popover after a 100ms timeout.
await waitFor(() => expect(screen.getByRole('combobox')).toHaveFocus());
return { updateInlineContent, contentRef, insertInlineContent, focus };
};
describe('SearchPage', () => {
describe('Search', () => {
beforeEach(() => {
capturedProps.length = 0;
useDocSearchFilterStore.setState({ filter: 'all' });
@@ -122,20 +122,20 @@ describe('SearchPage', () => {
});
it('limits the search to the current doc subtree on mount', async () => {
await renderSearchPage();
await renderSearch();
expect(useDocSearchFilterStore.getState().filter).toBe('current');
});
it('renders the trigger character and focuses the search input', async () => {
await renderSearchPage({ trigger: '@' });
await renderSearch({ trigger: '@' });
expect(screen.getByText('@')).toBeInTheDocument();
expect(screen.getByRole('combobox')).toHaveFocus();
});
it('forwards the typed text to the search results', async () => {
await renderSearchPage();
await renderSearch();
fireEvent.input(screen.getByRole('combobox'), {
target: { value: 'my query' },
@@ -150,7 +150,7 @@ describe('SearchPage', () => {
});
it('selects a result and inserts the interlink', async () => {
const { updateInlineContent, contentRef, focus } = await renderSearchPage();
const { updateInlineContent, contentRef, focus } = await renderSearch();
fireEvent.click(await screen.findByText('First result'));
@@ -163,7 +163,7 @@ describe('SearchPage', () => {
});
it('ignores a selection when the editor is not editable', async () => {
const { updateInlineContent } = await renderSearchPage({
const { updateInlineContent } = await renderSearch({
isEditable: false,
});
@@ -174,7 +174,7 @@ describe('SearchPage', () => {
it('closes and re-inserts the trigger and typed text on Escape', async () => {
const { updateInlineContent, insertInlineContent, focus } =
await renderSearchPage({ trigger: '/' });
await renderSearch({ trigger: '/' });
fireEvent.input(screen.getByRole('combobox'), {
target: { value: 'abc' },
@@ -182,7 +182,7 @@ describe('SearchPage', () => {
fireEvent.keyDown(screen.getByRole('combobox'), { key: 'Escape' });
expect(updateInlineContent).toHaveBeenCalledWith({
type: 'interlinkingLinkInline',
type: 'mentionSearchInline',
props: { disabled: true },
});
expect(focus).toHaveBeenCalled();
@@ -190,20 +190,19 @@ describe('SearchPage', () => {
});
it('closes without inserting anything on Backspace when the search is empty', async () => {
const { updateInlineContent, insertInlineContent } =
await renderSearchPage();
const { updateInlineContent, insertInlineContent } = await renderSearch();
fireEvent.keyDown(screen.getByRole('combobox'), { key: 'Backspace' });
expect(updateInlineContent).toHaveBeenCalledWith({
type: 'interlinkingLinkInline',
type: 'mentionSearchInline',
props: { disabled: true },
});
expect(insertInlineContent).not.toHaveBeenCalled();
});
it('lets a Backspace with existing text fall through to normal editing', async () => {
const { updateInlineContent } = await renderSearchPage();
const { updateInlineContent } = await renderSearch();
fireEvent.input(screen.getByRole('combobox'), {
target: { value: 'abc' },
@@ -214,7 +213,7 @@ describe('SearchPage', () => {
});
it('moves the highlighted result with ArrowDown/ArrowUp and selects it on Enter', async () => {
const { updateInlineContent } = await renderSearchPage();
const { updateInlineContent } = await renderSearch();
await screen.findByText('First result');
const input = screen.getByRole('combobox');
@@ -0,0 +1,3 @@
export * from './Interlinking';
export * from './MentionSearchInlineContent';
export * from './types';
@@ -0,0 +1,8 @@
/**
* What the mention search can be replaced by once the user picked an item.
* Each kind of mention (Interlinking, ...) adds its own inline content here.
*/
export type MentionedInlineContent = {
type: 'interlinkingLinkInline';
props: { docId: string };
};
@@ -1 +1 @@
export * from './Interlinking';
export * from './Mention';
@@ -102,7 +102,7 @@ export const useShortcuts = (
event.preventDefault();
editor.insertInlineContent([
{
type: 'interlinkingLinkInline',
type: 'mentionSearchInline',
props: {
trigger: '@',
},
@@ -0,0 +1,43 @@
import { TextRun } from 'docx';
import { describe, expect, it } from 'vitest';
import {
inlineContentMappingMentionSearchDocx,
inlineContentMappingMentionSearchODT,
inlineContentMappingMentionSearchPDF,
} from '../inline-content-mapping';
const search = {
type: 'mentionSearchInline' as const,
props: { trigger: '@', disabled: false },
};
// The exporter is only needed by the mappings that render nested content.
const exporter = {} as never;
describe('mention search export mappings', () => {
it('exports nothing in ODT', () => {
expect(
inlineContentMappingMentionSearchODT(search as never, exporter),
).toBeNull();
});
it('exports an empty text run in Docx', () => {
const run = inlineContentMappingMentionSearchDocx(
search as never,
exporter,
);
expect(run).toBeInstanceOf(TextRun);
expect(JSON.stringify(run)).not.toContain('@');
});
it('exports nothing in PDF', () => {
const element = inlineContentMappingMentionSearchPDF(
search as never,
exporter,
) as unknown as React.ReactElement<{ children?: unknown }>;
expect(element.props.children).toBeUndefined();
});
});
@@ -1,3 +1,6 @@
export * from './interlinkingLinkPDF';
export * from './interlinkingLinkDocx';
export * from './interlinkingLinkODT';
export * from './mentionSearchPDF';
export * from './mentionSearchDocx';
export * from './mentionSearchODT';
@@ -0,0 +1,9 @@
import { TextRun } from 'docx';
import { DocsExporterDocx } from '../types';
/**
* The search of a mention is transient, there is nothing to export.
*/
export const inlineContentMappingMentionSearchDocx: DocsExporterDocx['mappings']['inlineContentMapping']['mentionSearchInline'] =
() => new TextRun('');
@@ -0,0 +1,7 @@
import { DocsExporterODT } from '../types';
/**
* The search of a mention is transient, there is nothing to export.
*/
export const inlineContentMappingMentionSearchODT: DocsExporterODT['mappings']['inlineContentMapping']['mentionSearchInline'] =
() => null;
@@ -0,0 +1,7 @@
import { DocsExporterPDF } from '../types';
/**
* The search of a mention is transient, there is nothing to export.
*/
export const inlineContentMappingMentionSearchPDF: DocsExporterPDF['mappings']['inlineContentMapping']['mentionSearchInline'] =
() => <></>;
@@ -11,7 +11,10 @@ import {
blockMappingQuoteDocx,
blockMappingUploadLoaderDocx,
} from './blocks-mapping';
import { createInlineContentMappingInterlinkingLinkDocx } from './inline-content-mapping';
import {
createInlineContentMappingInterlinkingLinkDocx,
inlineContentMappingMentionSearchDocx,
} from './inline-content-mapping';
import { DocsExporterDocx } from './types';
export const getDocxDocsSchemaMappings = (
@@ -60,6 +63,7 @@ export const getDocxDocsSchemaMappings = (
...docxDefaultSchemaMappings.inlineContentMapping,
interlinkingLinkInline:
createInlineContentMappingInterlinkingLinkDocx(interlinkTitles),
mentionSearchInline: inlineContentMappingMentionSearchDocx,
// Renders inline math as a native (editable) Word equation.
math: inlineMathMapping,
},
@@ -10,7 +10,10 @@ import {
blockMappingImageODT,
blockMappingUploadLoaderODT,
} from './blocks-mapping';
import { createInlineContentMappingInterlinkingLinkODT } from './inline-content-mapping';
import {
createInlineContentMappingInterlinkingLinkODT,
inlineContentMappingMentionSearchODT,
} from './inline-content-mapping';
import { DocsExporterODT } from './types';
// Align default inline mappings to our editor inline schema without using `any`
@@ -40,6 +43,7 @@ export const getOdtDocsSchemaMappings = (
...baseInlineMappings,
interlinkingLinkInline:
createInlineContentMappingInterlinkingLinkODT(interlinkTitles),
mentionSearchInline: inlineContentMappingMentionSearchODT,
// Renders inline math as a native (editable) ODF formula object.
math: inlineMathMapping,
},
@@ -14,7 +14,10 @@ import {
blockMappingTablePDF,
blockMappingUploadLoaderPDF,
} from './blocks-mapping';
import { createInlineContentMappingInterlinkingLinkPDF } from './inline-content-mapping';
import {
createInlineContentMappingInterlinkingLinkPDF,
inlineContentMappingMentionSearchPDF,
} from './inline-content-mapping';
import { DocsExporterPDF } from './types';
export const getPdfDocsSchemaMappings = (
@@ -43,6 +46,7 @@ export const getPdfDocsSchemaMappings = (
...pdfDefaultSchemaMappings.inlineContentMapping,
interlinkingLinkInline:
createInlineContentMappingInterlinkingLinkPDF(interlinkTitles),
mentionSearchInline: inlineContentMappingMentionSearchPDF,
// Inline math is rasterized to an image that flows with the text.
math: inlineMathMapping,
},
@@ -13,6 +13,7 @@ import { DocSearchItem } from './DocSearchItem';
type DocSearchContentProps = {
groupName?: string;
hideGroupNameWhenEmpty?: boolean;
search: string;
filterResults?: (doc: DocSearch) => boolean;
isSearchNotMandatory?: boolean;
@@ -25,6 +26,7 @@ type DocSearchContentProps = {
export const DocSearchContent = ({
groupName,
hideGroupNameWhenEmpty,
search,
filterResults,
onResults,
@@ -82,7 +84,8 @@ export const DocSearchContent = ({
onResults?.(elements);
setDocsData({
groupName: groupName,
groupName:
hideGroupNameWhenEmpty && elements.length === 0 ? '' : groupName,
groupKey: 'docs',
elements,
endActions: hasNextPage
@@ -111,6 +114,7 @@ export const DocSearchContent = ({
data?.pages,
filterResults,
groupName,
hideGroupNameWhenEmpty,
isSearchNotMandatory,
loading,
hasNextPage,