diff --git a/packages/frontend/component/src/ui/property/property.css.ts b/packages/frontend/component/src/ui/property/property.css.ts
index 428197c253124..f81c917991719 100644
--- a/packages/frontend/component/src/ui/property/property.css.ts
+++ b/packages/frontend/component/src/ui/property/property.css.ts
@@ -221,6 +221,7 @@ export const sectionContent = style({
display: 'flex',
flexDirection: 'column',
gap: 4,
+ marginTop: 4,
selectors: {
'&[hidden]': {
display: 'none',
diff --git a/packages/frontend/core/src/blocksuite/presets/_common/components/text-renderer.ts b/packages/frontend/core/src/blocksuite/presets/_common/components/text-renderer.ts
index 14117f02fcd38..e3204dff57912 100644
--- a/packages/frontend/core/src/blocksuite/presets/_common/components/text-renderer.ts
+++ b/packages/frontend/core/src/blocksuite/presets/_common/components/text-renderer.ts
@@ -1,4 +1,9 @@
-import { BlockStdScope, type EditorHost } from '@blocksuite/affine/block-std';
+import {
+ BlockStdScope,
+ type EditorHost,
+ type ExtensionType,
+ ShadowlessElement,
+} from '@blocksuite/affine/block-std';
import type {
AffineAIPanelState,
AffineAIPanelWidgetConfig,
@@ -10,8 +15,14 @@ import {
ParagraphBlockComponent,
} from '@blocksuite/affine/blocks';
import { WithDisposable } from '@blocksuite/affine/global/utils';
-import { BlockViewType, type Doc, type Query } from '@blocksuite/affine/store';
-import { css, html, LitElement, nothing, type PropertyValues } from 'lit';
+import {
+ BlockViewType,
+ type Doc,
+ type JobMiddleware,
+ type Query,
+ type Schema,
+} from '@blocksuite/affine/store';
+import { css, html, nothing, type PropertyValues } from 'lit';
import { property, query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { keyed } from 'lit/directives/keyed.js';
@@ -70,9 +81,12 @@ const customHeadingStyles = css`
export type TextRendererOptions = {
maxHeight?: number;
customHeading?: boolean;
+ extensions?: ExtensionType[];
+ additionalMiddlewares?: JobMiddleware[];
};
-export class TextRenderer extends WithDisposable(LitElement) {
+// todo: refactor it for more general purpose usage instead of AI only?
+export class TextRenderer extends WithDisposable(ShadowlessElement) {
static override styles = css`
.ai-answer-text-editor.affine-page-viewport {
background: transparent;
@@ -177,8 +191,9 @@ export class TextRenderer extends WithDisposable(LitElement) {
if (this._answers.length > 0) {
const latestAnswer = this._answers.pop();
this._answers = [];
- if (latestAnswer) {
- markDownToDoc(this.host, latestAnswer)
+ const schema = this.schema ?? this.host?.std.doc.collection.schema;
+ if (latestAnswer && schema) {
+ markDownToDoc(schema, latestAnswer, this.options.additionalMiddlewares)
.then(doc => {
this._doc = doc.blockCollection.getDoc({
query: this._query,
@@ -245,7 +260,7 @@ export class TextRenderer extends WithDisposable(LitElement) {
html`
${new BlockStdScope({
doc: this._doc,
- extensions: CustomPageEditorBlockSpecs,
+ extensions: this.options.extensions ?? CustomPageEditorBlockSpecs,
}).render()}
`
)}
@@ -277,7 +292,10 @@ export class TextRenderer extends WithDisposable(LitElement) {
accessor answer!: string;
@property({ attribute: false })
- accessor host!: EditorHost;
+ accessor host: EditorHost | null = null;
+
+ @property({ attribute: false })
+ accessor schema: Schema | null = null;
@property({ attribute: false })
accessor options!: TextRendererOptions;
diff --git a/packages/frontend/core/src/blocksuite/presets/_common/utils/markdown-utils.ts b/packages/frontend/core/src/blocksuite/presets/_common/utils/markdown-utils.ts
index dffd42abf7fdf..1eba15ce163d7 100644
--- a/packages/frontend/core/src/blocksuite/presets/_common/utils/markdown-utils.ts
+++ b/packages/frontend/core/src/blocksuite/presets/_common/utils/markdown-utils.ts
@@ -12,6 +12,7 @@ import {
PlainTextAdapter,
titleMiddleware,
} from '@blocksuite/affine/blocks';
+import type { JobMiddleware, Schema } from '@blocksuite/affine/store';
import { DocCollection, Job } from '@blocksuite/affine/store';
import { assertExists } from '@blocksuite/global/utils';
import type {
@@ -184,16 +185,23 @@ export async function replaceFromMarkdown(
await job.snapshotToSlice(snapshot, host.doc, parent, index);
}
-export async function markDownToDoc(host: EditorHost, answer: string) {
- const schema = host.std.doc.collection.schema;
+export async function markDownToDoc(
+ schema: Schema,
+ answer: string,
+ additionalMiddlewares?: JobMiddleware[]
+) {
// Should not create a new doc in the original collection
const collection = new DocCollection({
schema,
});
collection.meta.initialize();
+ const middlewares = [defaultImageProxyMiddleware];
+ if (additionalMiddlewares) {
+ middlewares.push(...additionalMiddlewares);
+ }
const job = new Job({
collection,
- middlewares: [defaultImageProxyMiddleware],
+ middlewares,
});
const mdAdapter = new MarkdownAdapter(job);
const doc = await mdAdapter.toDoc({
diff --git a/packages/frontend/core/src/blocksuite/presets/ai/utils/editor-actions.ts b/packages/frontend/core/src/blocksuite/presets/ai/utils/editor-actions.ts
index 6e7405e3345fb..e994a6e49ba1f 100644
--- a/packages/frontend/core/src/blocksuite/presets/ai/utils/editor-actions.ts
+++ b/packages/frontend/core/src/blocksuite/presets/ai/utils/editor-actions.ts
@@ -142,7 +142,7 @@ export const copyTextAnswer = async (panel: AffineAIPanelWidget) => {
};
export const copyText = async (host: EditorHost, text: string) => {
- const previewDoc = await markDownToDoc(host, text);
+ const previewDoc = await markDownToDoc(host.std.doc.schema, text);
const models = previewDoc
.getBlocksByFlavour('affine:note')
.map(b => b.model)
diff --git a/packages/frontend/core/src/blocksuite/presets/index.ts b/packages/frontend/core/src/blocksuite/presets/index.ts
new file mode 100644
index 0000000000000..1ebd6df41011e
--- /dev/null
+++ b/packages/frontend/core/src/blocksuite/presets/index.ts
@@ -0,0 +1 @@
+export * from './_common/components/text-renderer';
diff --git a/packages/frontend/core/src/components/affine/reference-link/index.tsx b/packages/frontend/core/src/components/affine/reference-link/index.tsx
index d74dd9c36629d..c936e89dc2bbb 100644
--- a/packages/frontend/core/src/components/affine/reference-link/index.tsx
+++ b/packages/frontend/core/src/components/affine/reference-link/index.tsx
@@ -7,7 +7,12 @@ import { useI18n } from '@affine/i18n';
import { track } from '@affine/track';
import type { DocMode } from '@blocksuite/affine/blocks';
import type { DocCollection } from '@blocksuite/affine/store';
-import { LiveData, useLiveData, useService } from '@toeverything/infra';
+import {
+ DocsService,
+ LiveData,
+ useLiveData,
+ useService,
+} from '@toeverything/infra';
import clsx from 'clsx';
import { nanoid } from 'nanoid';
import {
@@ -36,6 +41,7 @@ function AffinePageReferenceInner({
Icon: UserIcon,
}: AffinePageReferenceProps) {
const docDisplayMetaService = useService(DocDisplayMetaService);
+ const docsService = useService(DocsService);
const i18n = useI18n();
let linkWithMode: DocMode | null = null;
@@ -62,15 +68,19 @@ function AffinePageReferenceInner({
);
})
);
- const title = useLiveData(
+ const notFound = !useLiveData(docsService.list.doc$(pageId));
+
+ let title = useLiveData(
docDisplayMetaService.title$(pageId, { reference: true })
);
+ title = notFound ? i18n.t('com.affine.notFoundPage.title') : title;
+
return (
- <>
+
{i18n.t(title)}
- >
+
);
}
diff --git a/packages/frontend/core/src/components/affine/reference-link/styles.css.ts b/packages/frontend/core/src/components/affine/reference-link/styles.css.ts
index dfe5dd6df75dd..3bb23f49c4182 100644
--- a/packages/frontend/core/src/components/affine/reference-link/styles.css.ts
+++ b/packages/frontend/core/src/components/affine/reference-link/styles.css.ts
@@ -1,9 +1,11 @@
-import { style } from '@vanilla-extract/css';
+import { cssVarV2 } from '@toeverything/theme/v2';
+import { globalStyle, style } from '@vanilla-extract/css';
export const pageReferenceIcon = style({
verticalAlign: 'middle',
fontSize: '1.1em',
transform: 'translate(2px, -1px)',
+ color: cssVarV2('icon/primary'),
});
export const pageReferenceLink = style({
@@ -12,3 +14,13 @@ export const pageReferenceLink = style({
wordBreak: 'break-word',
hyphens: 'auto',
});
+
+export const notFound = style({
+ color: cssVarV2('text/secondary'),
+ textDecoration: 'line-through',
+});
+
+globalStyle('affine-reference .affine-reference', {
+ color: 'inherit !important',
+ textDecoration: 'none !important',
+});
diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/bi-directional-link-panel.css.ts b/packages/frontend/core/src/components/blocksuite/block-suite-editor/bi-directional-link-panel.css.ts
index 1c4244c329050..6ee9a37150c36 100644
--- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/bi-directional-link-panel.css.ts
+++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/bi-directional-link-panel.css.ts
@@ -1,5 +1,6 @@
import { cssVar } from '@toeverything/theme';
-import { style } from '@vanilla-extract/css';
+import { cssVarV2 } from '@toeverything/theme/v2';
+import { globalStyle, style } from '@vanilla-extract/css';
export const container = style({
width: '100%',
@@ -49,7 +50,6 @@ export const title = style({
});
export const showButton = style({
- width: '56px',
height: '28px',
borderRadius: '8px',
border: '1px solid ' + cssVar('--affine-border-color'),
@@ -74,9 +74,45 @@ export const linksTitles = style({
export const link = style({
width: '100%',
- height: '32px',
+ height: '30px',
display: 'flex',
alignItems: 'center',
gap: '4px',
whiteSpace: 'nowrap',
});
+
+globalStyle(`${link} .affine-reference-title`, {
+ borderBottom: 'none',
+});
+
+export const linkPreviewContainer = style({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '8px',
+});
+
+export const linkPreview = style({
+ border: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
+ borderRadius: '8px',
+ padding: '8px',
+ color: cssVarV2('text/primary'),
+ ':hover': {
+ backgroundColor: cssVarV2('layer/background/hoverOverlay'),
+ },
+});
+
+export const linkPreviewRenderer = style({
+ cursor: 'pointer',
+});
+
+export const collapsedIcon = style({
+ transition: 'all 0.2s ease-in-out',
+ color: cssVarV2('icon/primary'),
+ fontSize: 20,
+ selectors: {
+ '&[data-collapsed="true"]': {
+ transform: 'rotate(90deg)',
+ color: cssVarV2('icon/secondary'),
+ },
+ },
+});
diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/bi-directional-link-panel.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-editor/bi-directional-link-panel.tsx
index 3bc66ae915eea..689be5b91f656 100644
--- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/bi-directional-link-panel.tsx
+++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/bi-directional-link-panel.tsx
@@ -1,32 +1,179 @@
+import {
+ Button,
+ createReactComponentFromLit,
+ useLitPortalFactory,
+} from '@affine/component';
+import { TextRenderer } from '@affine/core/blocksuite/presets';
import {
type Backlink,
DocLinksService,
type Link,
} from '@affine/core/modules/doc-link';
+import { toURLSearchParams } from '@affine/core/modules/navigation';
+import { WorkbenchLink } from '@affine/core/modules/workbench';
import { useI18n } from '@affine/i18n';
-import { LiveData, useLiveData, useServices } from '@toeverything/infra';
-import { Fragment, useCallback, useState } from 'react';
+import type { JobMiddleware } from '@blocksuite/affine/store';
+import { ToggleExpandIcon } from '@blocksuite/icons/rc';
+import * as Collapsible from '@radix-ui/react-collapsible';
+import {
+ getAFFiNEWorkspaceSchema,
+ LiveData,
+ useFramework,
+ useLiveData,
+ useServices,
+ WorkspaceService,
+} from '@toeverything/infra';
+import React, {
+ Fragment,
+ type ReactNode,
+ useCallback,
+ useMemo,
+ useState,
+} from 'react';
-import { AffinePageReference } from '../../affine/reference-link';
+import {
+ AffinePageReference,
+ AffineSharedPageReference,
+} from '../../affine/reference-link';
import * as styles from './bi-directional-link-panel.css';
+import {
+ patchReferenceRenderer,
+ type ReferenceReactRenderer,
+} from './specs/custom/spec-patchers';
+import { createPageModeSpecs } from './specs/page';
+
+const BlocksuiteTextRenderer = createReactComponentFromLit({
+ react: React,
+ elementClass: TextRenderer,
+});
+
+const CollapsibleSection = ({
+ title,
+ children,
+ length,
+}: {
+ title: ReactNode;
+ children: ReactNode;
+ length?: number;
+}) => {
+ const [open, setOpen] = useState(false);
+ return (
+
+
+ {title}
+ {length ? (
+
+ ) : null}
+
+ {children}
+
+ );
+};
+
+const usePreviewExtensions = () => {
+ const [reactToLit, portals] = useLitPortalFactory();
+ const framework = useFramework();
+
+ const { workspaceService } = useServices({
+ WorkspaceService,
+ });
+
+ const referenceRenderer: ReferenceReactRenderer = useMemo(() => {
+ return function customReference(reference) {
+ const data = reference.delta.attributes?.reference;
+ if (!data) return ;
+
+ const pageId = data.pageId;
+ if (!pageId) return ;
+
+ const params = toURLSearchParams(data.params);
+
+ if (workspaceService.workspace.openOptions.isSharedMode) {
+ return (
+
+ );
+ }
+
+ return ;
+ };
+ }, [workspaceService]);
+
+ const extensions = useMemo(() => {
+ const specs = createPageModeSpecs(framework);
+ return [patchReferenceRenderer(reactToLit, referenceRenderer), ...specs];
+ }, [reactToLit, referenceRenderer, framework]);
+
+ return [extensions, portals] as const;
+};
export const BiDirectionalLinkPanel = () => {
const [show, setShow] = useState(false);
- const { docLinksService } = useServices({
+ const { docLinksService, workspaceService } = useServices({
DocLinksService,
+ WorkspaceService,
});
+
+ const [extensions, portals] = usePreviewExtensions();
const t = useI18n();
const links = useLiveData(
show ? docLinksService.links.links$ : new LiveData([] as Link[])
);
- const backlinks = useLiveData(
- show ? docLinksService.backlinks.backlinks$ : new LiveData([] as Backlink[])
+ const backlinkGroups = useLiveData(
+ LiveData.computed(get => {
+ if (!show) {
+ return [];
+ }
+
+ const links = get(docLinksService.backlinks.backlinks$);
+
+ // group by docId
+ const groupedLinks = links.reduce(
+ (acc, link) => {
+ acc[link.docId] = [...(acc[link.docId] || []), link];
+ return acc;
+ },
+ {} as Record
+ );
+
+ return Object.entries(groupedLinks).map(([docId, links]) => ({
+ docId,
+ title: links[0].title, // title should be the same for all blocks
+ links,
+ }));
+ })
);
+
+ const backlinkCount = useMemo(() => {
+ return backlinkGroups.reduce((acc, link) => acc + link.links.length, 0);
+ }, [backlinkGroups]);
+
const handleClickShow = useCallback(() => {
setShow(!show);
}, [show]);
+ const textRendererOptions = useMemo(() => {
+ const docLinkBaseURLMiddleware: JobMiddleware = ({ adapterConfigs }) => {
+ adapterConfigs.set(
+ 'docLinkBaseUrl',
+ `/workspace/${workspaceService.workspace.id}`
+ );
+ };
+
+ return {
+ customHeading: true,
+ extensions,
+ additionalMiddlewares: [docLinkBaseURLMiddleware],
+ };
+ }, [extensions, workspaceService.workspace.id]);
+
return (
{!show && (
@@ -37,9 +184,11 @@ export const BiDirectionalLinkPanel = () => {
Bi-Directional Links
-
- {show ? 'Hide' : 'Show'}
-
+
{show && (
@@ -49,17 +198,78 @@ export const BiDirectionalLinkPanel = () => {
- {t['com.affine.page-properties.backlinks']()} · {backlinks.length}
+ {t['com.affine.page-properties.backlinks']()} · {backlinkCount}
- {backlinks.map(link => (
-
-
-
+ {backlinkGroups.map(linkGroup => (
+
}
+ length={linkGroup.links.length}
+ >
+
+ {linkGroup.links.map(link => {
+ if (!link.markdownPreview) {
+ return null;
+ }
+ const searchParams = new URLSearchParams();
+ const displayMode = link.displayMode || 'page';
+ searchParams.set('mode', displayMode);
+
+ let blockId = link.blockId;
+ if (
+ link.parentFlavour === 'affine:database' &&
+ link.parentBlockId
+ ) {
+ // if parentBlockFlavour is 'affine:database',
+ // we will fallback to the database block instead
+ blockId = link.parentBlockId;
+ } else if (displayMode === 'edgeless' && link.noteBlockId) {
+ // if note has displayMode === 'edgeless' && has noteBlockId,
+ // set noteBlockId as blockId
+ blockId = link.noteBlockId;
+ }
+
+ searchParams.set('blockIds', blockId);
+
+ const to = {
+ pathname: '/' + linkGroup.docId,
+ search: '?' + searchParams.toString(),
+ hash: '',
+ };
+
+ // if this backlink has no noteBlock && displayMode is edgeless, we will render
+ // the link as a page link
+ const edgelessLink =
+ displayMode === 'edgeless' && !link.noteBlockId;
+
+ return (
+
+ {edgelessLink ? (
+ <>
+ [Edgeless]
+
+ >
+ ) : (
+
+ )}
+
+ );
+ })}
-
-
{link.markdownPreview}
-
-
+
))}
@@ -78,6 +288,13 @@ export const BiDirectionalLinkPanel = () => {
>
)}
+ {
+ <>
+ {portals.map(p => (
+ {p.portal}
+ ))}
+ >
+ }
);
};
diff --git a/packages/frontend/core/src/desktop/dialogs/doc-info/info-modal.tsx b/packages/frontend/core/src/desktop/dialogs/doc-info/info-modal.tsx
index 65bbef0a3c149..daeaedd375e0f 100644
--- a/packages/frontend/core/src/desktop/dialogs/doc-info/info-modal.tsx
+++ b/packages/frontend/core/src/desktop/dialogs/doc-info/info-modal.tsx
@@ -48,11 +48,23 @@ export const InfoTable = ({
[docId, docsSearchService]
)
);
+
const backlinks = useLiveData(
- useMemo(
- () => LiveData.from(docsSearchService.watchRefsTo(docId), null),
- [docId, docsSearchService]
- )
+ useMemo(() => {
+ return LiveData.from(docsSearchService.watchRefsTo(docId), []).map(
+ links => {
+ const visitedDoc = new Set();
+ // for each doc, we only show the first block
+ return links.filter(link => {
+ if (visitedDoc.has(link.docId)) {
+ return false;
+ }
+ visitedDoc.add(link.docId);
+ return true;
+ });
+ }
+ );
+ }, [docId, docsSearchService])
);
const onBacklinkPropertyChange = useCallback(
diff --git a/packages/frontend/core/src/mobile/pages/workspace/detail/sheets/doc-info.tsx b/packages/frontend/core/src/mobile/pages/workspace/detail/sheets/doc-info.tsx
index 29e6d44bdfdb8..89bc1b52943ae 100644
--- a/packages/frontend/core/src/mobile/pages/workspace/detail/sheets/doc-info.tsx
+++ b/packages/frontend/core/src/mobile/pages/workspace/detail/sheets/doc-info.tsx
@@ -47,10 +47,21 @@ export const DocInfoSheet = ({
)
);
const backlinks = useLiveData(
- useMemo(
- () => LiveData.from(docsSearchService.watchRefsTo(docId), null),
- [docId, docsSearchService]
- )
+ useMemo(() => {
+ return LiveData.from(docsSearchService.watchRefsTo(docId), []).map(
+ links => {
+ const visitedDoc = new Set();
+ // for each doc, we only show the first block
+ return links.filter(link => {
+ if (visitedDoc.has(link.docId)) {
+ return false;
+ }
+ visitedDoc.add(link.docId);
+ return true;
+ });
+ }
+ );
+ }, [docId, docsSearchService])
);
const [newPropertyId, setNewPropertyId] = useState(null);
diff --git a/packages/frontend/core/src/modules/doc-link/entities/doc-backlinks.ts b/packages/frontend/core/src/modules/doc-link/entities/doc-backlinks.ts
index cee54f9810a42..fbd0e7251a662 100644
--- a/packages/frontend/core/src/modules/doc-link/entities/doc-backlinks.ts
+++ b/packages/frontend/core/src/modules/doc-link/entities/doc-backlinks.ts
@@ -7,6 +7,10 @@ export interface Backlink {
docId: string;
blockId: string;
title: string;
+ noteBlockId?: string;
+ displayMode?: string;
+ parentBlockId?: string;
+ parentFlavour?: string;
markdownPreview?: string;
}
diff --git a/packages/frontend/core/src/modules/docs-search/entities/docs-indexer.ts b/packages/frontend/core/src/modules/docs-search/entities/docs-indexer.ts
index f10d77f521755..205e5920ff520 100644
--- a/packages/frontend/core/src/modules/docs-search/entities/docs-indexer.ts
+++ b/packages/frontend/core/src/modules/docs-search/entities/docs-indexer.ts
@@ -146,6 +146,7 @@ export class DocsIndexer extends Entity {
allIndexedDocs,
rootDocBuffer,
reindexAll: isUpgrade,
+ rootDocId: this.workspaceId,
});
} else {
const rootDocBuffer =
@@ -167,6 +168,7 @@ export class DocsIndexer extends Entity {
docBuffer,
storageDocId,
rootDocBuffer,
+ rootDocId: this.workspaceId,
});
}
diff --git a/packages/frontend/core/src/modules/docs-search/schema.ts b/packages/frontend/core/src/modules/docs-search/schema.ts
index 9c8fe09ed15d7..f7f1e6dc45090 100644
--- a/packages/frontend/core/src/modules/docs-search/schema.ts
+++ b/packages/frontend/core/src/modules/docs-search/schema.ts
@@ -26,7 +26,7 @@ export const blockIndexSchema = defineSchema({
// parent block id
parentBlockId: 'String',
// additional info
- // { "databaseName": "xxx" }
+ // { "databaseName": "xxx", "displayMode": "page/edgeless", "noteBlockId": "xxx" }
additional: { type: 'String', index: false },
markdownPreview: { type: 'String', index: false },
});
diff --git a/packages/frontend/core/src/modules/docs-search/services/docs-search.ts b/packages/frontend/core/src/modules/docs-search/services/docs-search.ts
index fe3d05ee4b227..f95f353cd2c7e 100644
--- a/packages/frontend/core/src/modules/docs-search/services/docs-search.ts
+++ b/packages/frontend/core/src/modules/docs-search/services/docs-search.ts
@@ -478,9 +478,16 @@ export class DocsSearchService extends Service {
'docId',
{
hits: {
- fields: ['docId', 'blockId', 'markdownPreview'],
+ fields: [
+ 'docId',
+ 'blockId',
+ 'parentBlockId',
+ 'parentFlavour',
+ 'additional',
+ 'markdownPreview',
+ ],
pagination: {
- limit: 1,
+ limit: 5, // the max number of backlinks to show for each doc
},
},
pagination: {
@@ -495,21 +502,60 @@ export class DocsSearchService extends Service {
buckets.map(bucket => bucket.key)
);
- return buckets.map(bucket => {
+ return buckets.flatMap(bucket => {
const title =
docData.find(doc => doc.id === bucket.key)?.get('title') ?? '';
- const blockId = bucket.hits.nodes[0]?.fields.blockId ?? '';
- const markdownPreview =
- bucket.hits.nodes[0]?.fields.markdownPreview ?? '';
- return {
- docId: bucket.key,
- blockId: typeof blockId === 'string' ? blockId : blockId[0],
- title: typeof title === 'string' ? title : title[0],
- markdownPreview:
- typeof markdownPreview === 'string'
- ? markdownPreview
- : markdownPreview[0],
- };
+
+ return bucket.hits.nodes.map(node => {
+ const blockId = node.fields.blockId ?? '';
+ const markdownPreview = node.fields.markdownPreview ?? '';
+ const additional =
+ typeof node.fields.additional === 'string'
+ ? node.fields.additional
+ : node.fields.additional[0];
+
+ const additionalData: {
+ displayMode?: string;
+ noteBlockId?: string;
+ } = JSON.parse(additional || '{}');
+
+ const displayMode = additionalData.displayMode ?? '';
+ const noteBlockId = additionalData.noteBlockId ?? '';
+ const parentBlockId =
+ typeof node.fields.parentBlockId === 'string'
+ ? node.fields.parentBlockId
+ : node.fields.parentBlockId[0];
+ const parentFlavour =
+ typeof node.fields.parentFlavour === 'string'
+ ? node.fields.parentFlavour
+ : node.fields.parentFlavour[0];
+
+ return {
+ docId: bucket.key,
+ blockId: typeof blockId === 'string' ? blockId : blockId[0],
+ title: typeof title === 'string' ? title : title[0],
+ markdownPreview:
+ typeof markdownPreview === 'string'
+ ? markdownPreview
+ : markdownPreview[0],
+ displayMode:
+ typeof displayMode === 'string'
+ ? displayMode
+ : displayMode[0],
+ noteBlockId:
+ typeof noteBlockId === 'string'
+ ? noteBlockId
+ : noteBlockId[0],
+ parentBlockId:
+ typeof parentBlockId === 'string'
+ ? parentBlockId
+ : parentBlockId[0],
+ parentFlavour:
+ typeof parentFlavour === 'string'
+ ? parentFlavour
+ : parentFlavour[0],
+ };
+ });
});
});
})
diff --git a/packages/frontend/core/src/modules/docs-search/worker/in-worker.ts b/packages/frontend/core/src/modules/docs-search/worker/in-worker.ts
index 16c35761d2ff2..d97fa7281f090 100644
--- a/packages/frontend/core/src/modules/docs-search/worker/in-worker.ts
+++ b/packages/frontend/core/src/modules/docs-search/worker/in-worker.ts
@@ -1,12 +1,17 @@
-import {
- type AffineTextAttributes,
- MarkdownAdapter,
+import type {
+ AffineTextAttributes,
+ AttachmentBlockModel,
+ BookmarkBlockModel,
+ EmbedBlockModel,
+ ImageBlockModel,
} from '@blocksuite/affine/blocks';
+import { MarkdownAdapter } from '@blocksuite/affine/blocks';
import {
createYProxy,
DocCollection,
type DraftModel,
Job,
+ type JobMiddleware,
type YBlock,
} from '@blocksuite/affine/store';
import type { DeltaInsert } from '@blocksuite/inline';
@@ -73,42 +78,6 @@ async function getOrCreateCachedYDoc(data: Uint8Array) {
}
}
-function yblockToDraftModal(yblock: YBlock): DraftModel | null {
- const flavour = yblock.get('sys:flavour');
- const blockSchema = blocksuiteSchema.flavourSchemaMap.get(flavour);
- if (!blockSchema) {
- return null;
- }
- const keys = Array.from(yblock.keys())
- .filter(key => key.startsWith('prop:'))
- .map(key => key.substring(5));
-
- const props = Object.fromEntries(
- keys.map(key => [key, createYProxy(yblock.get(`prop:${key}`))])
- );
-
- return {
- ...props,
- id: yblock.get('sys:id'),
- flavour,
- children: [],
- role: blockSchema.model.role,
- version: (yblock.get('sys:version') as number) ?? blockSchema.version,
- keys: Array.from(yblock.keys())
- .filter(key => key.startsWith('prop:'))
- .map(key => key.substring(5)),
- };
-}
-
-const markdownAdapter = new MarkdownAdapter(
- new Job({
- collection: new DocCollection({
- id: 'indexer',
- schema: blocksuiteSchema,
- }),
- })
-);
-
interface BlockDocumentInfo {
docId: string;
blockId: string;
@@ -119,50 +88,364 @@ interface BlockDocumentInfo {
ref?: string[];
parentFlavour?: string;
parentBlockId?: string;
- additional?: { databaseName?: string };
+ additional?: {
+ databaseName?: string;
+ displayMode?: string;
+ noteBlockId?: string;
+ };
yblock: YMap;
markdownPreview?: string;
}
-const markdownPreviewCache = new WeakMap();
-const generateMarkdownPreview = async (block: BlockDocumentInfo) => {
- if (markdownPreviewCache.has(block)) {
- return markdownPreviewCache.get(block);
+const bookmarkFlavours = new Set([
+ 'affine:bookmark',
+ 'affine:embed-youtube',
+ 'affine:embed-figma',
+ 'affine:embed-github',
+ 'affine:embed-loom',
+]);
+
+function generateMarkdownPreviewBuilder(
+ yRootDoc: YDoc,
+ workspaceId: string,
+ blocks: BlockDocumentInfo[]
+) {
+ function yblockToDraftModal(yblock: YBlock): DraftModel | null {
+ const flavour = yblock.get('sys:flavour');
+ const blockSchema = blocksuiteSchema.flavourSchemaMap.get(flavour);
+ if (!blockSchema) {
+ return null;
+ }
+ const keys = Array.from(yblock.keys())
+ .filter(key => key.startsWith('prop:'))
+ .map(key => key.substring(5));
+
+ const props = Object.fromEntries(
+ keys.map(key => [key, createYProxy(yblock.get(`prop:${key}`))])
+ );
+
+ return {
+ ...props,
+ id: yblock.get('sys:id'),
+ flavour,
+ children: [],
+ role: blockSchema.model.role,
+ version: (yblock.get('sys:version') as number) ?? blockSchema.version,
+ keys: Array.from(yblock.keys())
+ .filter(key => key.startsWith('prop:'))
+ .map(key => key.substring(5)),
+ };
}
- const flavour = block.flavour;
- let markdown: string | null = null;
- if (
- flavour === 'affine:paragraph' ||
- flavour === 'affine:list' ||
- flavour === 'affine:code'
- ) {
- const draftModel = yblockToDraftModal(block.yblock);
- markdown =
- block.parentFlavour === 'affine:database'
- ? `database · ${block.additional?.databaseName}\n`
- : ((draftModel ? await markdownAdapter.fromBlock(draftModel) : null)
- ?.file ?? null);
+
+ const titleMiddleware: JobMiddleware = ({ adapterConfigs }) => {
+ const pages = yRootDoc.getMap('meta').get('pages');
+ if (!(pages instanceof YArray)) {
+ return;
+ }
+ for (const meta of pages.toArray()) {
+ adapterConfigs.set(
+ 'title:' + meta.get('id'),
+ meta.get('title')?.toString() ?? 'Untitled'
+ );
+ }
+ };
+
+ const baseUrl = `/workspace/${workspaceId}`;
+
+ function getDocLink(docId: string, blockId: string) {
+ const searchParams = new URLSearchParams();
+ searchParams.set('blockIds', blockId);
+ return `${baseUrl}/${docId}?${searchParams.toString()}`;
}
- if (
- flavour === 'affine:embed-linked-doc' ||
- flavour === 'affine:embed-synced-doc'
- ) {
- markdown = '🔗\n';
+
+ const docLinkBaseURLMiddleware: JobMiddleware = ({ adapterConfigs }) => {
+ adapterConfigs.set('docLinkBaseUrl', baseUrl);
+ };
+
+ const markdownAdapter = new MarkdownAdapter(
+ new Job({
+ collection: new DocCollection({
+ id: 'indexer',
+ schema: blocksuiteSchema,
+ }),
+ middlewares: [docLinkBaseURLMiddleware, titleMiddleware],
+ })
+ );
+
+ const markdownPreviewCache = new WeakMap();
+
+ function trimCodeBlock(markdown: string) {
+ const lines = markdown.split('\n').filter(line => line.trim() !== '');
+ if (lines.length > 5) {
+ return [...lines.slice(0, 4), '...', lines.at(-1), ''].join('\n');
+ }
+ return [...lines, ''].join('\n');
}
- if (flavour === 'affine:attachment') {
- markdown = '📃\n';
+
+ function trimParagraph(markdown: string) {
+ const lines = markdown.split('\n').filter(line => line.trim() !== '');
+
+ if (lines.length > 3) {
+ return [...lines.slice(0, 3), '...', lines.at(-1), ''].join('\n');
+ }
+
+ return [...lines, ''].join('\n');
}
- if (flavour === 'affine:image') {
- markdown = '🖼️\n';
+
+ function getListDepth(block: BlockDocumentInfo) {
+ let parentBlockCount = 0;
+ let currentBlock: BlockDocumentInfo | undefined = block;
+ do {
+ currentBlock = blocks.find(
+ b => b.blockId === currentBlock?.parentBlockId
+ );
+
+ // reach the root block. do not count it.
+ if (!currentBlock || currentBlock.flavour !== 'affine:list') {
+ break;
+ }
+ parentBlockCount++;
+ } while (currentBlock);
+ return parentBlockCount;
}
- markdownPreviewCache.set(block, markdown);
- return markdown;
-};
+
+ // only works for list block
+ function indentMarkdown(markdown: string, depth: number) {
+ if (depth <= 0) {
+ return markdown;
+ }
+
+ return (
+ markdown
+ .split('\n')
+ .map(line => ' '.repeat(depth) + line)
+ .join('\n') + '\n'
+ );
+ }
+
+ const generateDatabaseMarkdownPreview = (block: BlockDocumentInfo) => {
+ const isDatabaseBlock = (block: BlockDocumentInfo) => {
+ return block.flavour === 'affine:database';
+ };
+
+ const model = yblockToDraftModal(block.yblock);
+
+ if (!model) {
+ return null;
+ }
+
+ let dbBlock: BlockDocumentInfo | null = null;
+
+ if (isDatabaseBlock(block)) {
+ dbBlock = block;
+ } else {
+ const parentBlock = blocks.find(b => b.blockId === block.parentBlockId);
+
+ if (parentBlock && isDatabaseBlock(parentBlock)) {
+ dbBlock = parentBlock;
+ }
+ }
+
+ if (!dbBlock) {
+ return null;
+ }
+
+ const url = getDocLink(block.docId, dbBlock.blockId);
+ const title = dbBlock.additional?.databaseName;
+
+ return `[database · ${title || 'Untitled'}][](${url})\n`;
+ };
+
+ const generateImageMarkdownPreview = (block: BlockDocumentInfo) => {
+ const isImageModel = (
+ model: DraftModel | null
+ ): model is DraftModel => {
+ return model?.flavour === 'affine:image';
+ };
+
+ const model = yblockToDraftModal(block.yblock);
+
+ if (!isImageModel(model)) {
+ return null;
+ }
+
+ const info = ['an image block'];
+
+ if (model.sourceId) {
+ info.push(`file id ${model.sourceId}`);
+ }
+
+ if (model.caption) {
+ info.push(`with caption ${model.caption}`);
+ }
+
+ return info.join(', ') + '\n';
+ };
+
+ const generateEmbedMarkdownPreview = (block: BlockDocumentInfo) => {
+ const isEmbedModel = (
+ model: DraftModel | null
+ ): model is DraftModel => {
+ return (
+ model?.flavour === 'affine:embed-linked-doc' ||
+ model?.flavour === 'affine:embed-synced-doc'
+ );
+ };
+
+ const draftModel = yblockToDraftModal(block.yblock);
+ if (!isEmbedModel(draftModel)) {
+ return null;
+ }
+
+ const url = getDocLink(block.docId, draftModel.id);
+
+ return `[](${url})\n`;
+ };
+
+ const generateLatexMarkdownPreview = (block: BlockDocumentInfo) => {
+ let content =
+ typeof block.content === 'string'
+ ? block.content.trim()
+ : block.content?.join('').trim();
+
+ content = content?.split('\n').join(' ') ?? '';
+
+ return `LaTeX, with value ${content}\n`;
+ };
+
+ const generateBookmarkMarkdownPreview = (block: BlockDocumentInfo) => {
+ const isBookmarkModel = (
+ model: DraftModel | null
+ ): model is DraftModel => {
+ return bookmarkFlavours.has(model?.flavour ?? '');
+ };
+
+ const draftModel = yblockToDraftModal(block.yblock);
+ if (!isBookmarkModel(draftModel)) {
+ return null;
+ }
+ const title = draftModel.title;
+ const url = draftModel.url;
+ return `[${title}](${url})\n`;
+ };
+
+ const generateAttachmentMarkdownPreview = (block: BlockDocumentInfo) => {
+ const isAttachmentModel = (
+ model: DraftModel | null
+ ): model is DraftModel => {
+ return model?.flavour === 'affine:attachment';
+ };
+
+ const draftModel = yblockToDraftModal(block.yblock);
+ if (!isAttachmentModel(draftModel)) {
+ return null;
+ }
+
+ return `[${draftModel.name}](${draftModel.sourceId})\n`;
+ };
+
+ const generateMarkdownPreview = async (block: BlockDocumentInfo) => {
+ if (markdownPreviewCache.has(block)) {
+ return markdownPreviewCache.get(block);
+ }
+ const flavour = block.flavour;
+ let markdown: string | null = null;
+
+ if (
+ flavour === 'affine:paragraph' ||
+ flavour === 'affine:list' ||
+ flavour === 'affine:code'
+ ) {
+ const draftModel = yblockToDraftModal(block.yblock);
+ markdown =
+ block.parentFlavour === 'affine:database'
+ ? generateDatabaseMarkdownPreview(block)
+ : ((draftModel ? await markdownAdapter.fromBlock(draftModel) : null)
+ ?.file ?? null);
+
+ if (markdown) {
+ if (flavour === 'affine:code') {
+ markdown = trimCodeBlock(markdown);
+ } else if (flavour === 'affine:paragraph') {
+ markdown = trimParagraph(markdown);
+ }
+ }
+ } else if (flavour === 'affine:database') {
+ markdown = generateDatabaseMarkdownPreview(block);
+ } else if (
+ flavour === 'affine:embed-linked-doc' ||
+ flavour === 'affine:embed-synced-doc'
+ ) {
+ markdown = generateEmbedMarkdownPreview(block);
+ } else if (flavour === 'affine:attachment') {
+ markdown = generateAttachmentMarkdownPreview(block);
+ } else if (flavour === 'affine:image') {
+ markdown = generateImageMarkdownPreview(block);
+ } else if (flavour === 'affine:surface' || flavour === 'affine:page') {
+ // skip
+ } else if (flavour === 'affine:latex') {
+ markdown = generateLatexMarkdownPreview(block);
+ } else if (bookmarkFlavours.has(flavour)) {
+ markdown = generateBookmarkMarkdownPreview(block);
+ } else {
+ console.warn(`unknown flavour: ${flavour}`);
+ }
+
+ if (markdown && flavour === 'affine:list') {
+ const blockDepth = getListDepth(block);
+ markdown = indentMarkdown(markdown, Math.max(0, blockDepth));
+ }
+
+ markdownPreviewCache.set(block, markdown);
+ return markdown;
+ };
+
+ return generateMarkdownPreview;
+}
+
+// remove the indent of the first line of list
+// e.g.,
+// ```
+// - list item 1
+// - list item 2
+// ```
+// becomes
+// ```
+// - list item 1
+// - list item 2
+// ```
+function unindentMarkdown(markdown: string) {
+ const lines = markdown.split('\n');
+ const res: string[] = [];
+ let firstListFound = false;
+ let baseIndent = 0;
+
+ for (let current of lines) {
+ const indent = current.match(/^\s*/)?.[0]?.length ?? 0;
+
+ if (indent > 0) {
+ if (!firstListFound) {
+ // For the first list item, remove all indentation
+ firstListFound = true;
+ baseIndent = indent;
+ current = current.trimStart();
+ } else {
+ // For subsequent list items, maintain relative indentation
+ current = ' '.repeat(indent - baseIndent) + current.trimStart();
+ }
+ }
+
+ res.push(current);
+ }
+
+ return res.join('\n');
+}
async function crawlingDocData({
docBuffer,
storageDocId,
rootDocBuffer,
+ rootDocId,
}: WorkerInput & { type: 'doc' }): Promise {
if (isEmptyUpdate(rootDocBuffer)) {
console.warn('[worker]: Empty root doc buffer');
@@ -210,12 +493,49 @@ async function crawlingDocData({
let summary = '';
const blockDocuments: BlockDocumentInfo[] = [];
+ const generateMarkdownPreview = generateMarkdownPreviewBuilder(
+ yRootDoc,
+ rootDocId,
+ blockDocuments
+ );
+
const blocks = ydoc.getMap('blocks');
+ // build a parent map for quick lookup
+ // for each block, record its parent id
+ const parentMap: Record = {};
+ for (const [id, block] of blocks.entries()) {
+ const children = block.get('sys:children') as YArray | undefined;
+ if (children instanceof YArray && children.length) {
+ for (const child of children) {
+ parentMap[child] = id;
+ }
+ }
+ }
+
if (blocks.size === 0) {
return { deletedDoc: [docId] };
}
+ // find the nearest block that satisfies the predicate
+ const nearest = (
+ blockId: string,
+ predicate: (block: YMap) => boolean
+ ) => {
+ let current: string | null = blockId;
+ while (current) {
+ const block = blocks.get(current);
+ if (block && predicate(block)) {
+ return block;
+ }
+ current = parentMap[current] ?? null;
+ }
+ return null;
+ };
+
+ const nearestByFlavour = (blockId: string, flavour: string) =>
+ nearest(blockId, block => block.get('sys:flavour') === flavour);
+
let rootBlockId: string | null = null;
for (const block of blocks.values()) {
const flavour = block.get('sys:flavour')?.toString();
@@ -261,21 +581,40 @@ async function crawlingDocData({
const flavour = block.get('sys:flavour')?.toString();
const parentFlavour = parentBlock?.get('sys:flavour')?.toString();
+ const noteBlock = nearestByFlavour(blockId, 'affine:note');
+
+ // display mode:
+ // - both: page and edgeless -> fallback to page
+ // - page: only page -> page
+ // - edgeless: only edgeless -> edgeless
+ // - undefined: edgeless (assuming it is a normal element on the edgeless)
+ let displayMode = noteBlock?.get('prop:displayMode') ?? 'edgeless';
+
+ if (displayMode === 'both') {
+ displayMode = 'page';
+ }
+
+ const noteBlockId: string | undefined = noteBlock
+ ?.get('sys:id')
+ ?.toString();
pushChildren(blockId, block);
+ const commonBlockProps = {
+ docId,
+ flavour,
+ blockId,
+ yblock: block,
+ additional: { displayMode, noteBlockId },
+ };
+
if (flavour === 'affine:page') {
docTitle = block.get('prop:title').toString();
blockDocuments.push({
- docId,
- flavour,
- blockId,
+ ...commonBlockProps,
content: docTitle,
- yblock: block,
});
- }
-
- if (
+ } else if (
flavour === 'affine:paragraph' ||
flavour === 'affine:list' ||
flavour === 'affine:code'
@@ -313,9 +652,7 @@ async function crawlingDocData({
: undefined;
blockDocuments.push({
- docId,
- flavour,
- blockId,
+ ...commonBlockProps,
content: text.toString(),
...refs.reduce<{ refDocId: string[]; ref: string[] }>(
(prev, curr) => {
@@ -327,17 +664,14 @@ async function crawlingDocData({
),
parentFlavour,
parentBlockId,
- additional: { databaseName },
- yblock: block,
+ additional: { ...commonBlockProps.additional, databaseName },
});
if (summaryLenNeeded > 0) {
summary += text.toString();
summaryLenNeeded -= text.length;
}
- }
-
- if (
+ } else if (
flavour === 'affine:embed-linked-doc' ||
flavour === 'affine:embed-synced-doc'
) {
@@ -346,34 +680,27 @@ async function crawlingDocData({
// reference info
const params = block.get('prop:params') ?? {};
blockDocuments.push({
- docId,
- flavour,
- blockId,
+ ...commonBlockProps,
refDocId: [pageId],
ref: [JSON.stringify({ docId: pageId, ...params })],
parentFlavour,
parentBlockId,
- yblock: block,
});
}
- }
-
- if (flavour === 'affine:attachment' || flavour === 'affine:image') {
+ } else if (
+ flavour === 'affine:attachment' ||
+ flavour === 'affine:image'
+ ) {
const blobId = block.get('prop:sourceId');
if (typeof blobId === 'string') {
blockDocuments.push({
- docId,
- flavour,
- blockId,
+ ...commonBlockProps,
blob: [blobId],
parentFlavour,
parentBlockId,
- yblock: block,
});
}
- }
-
- if (flavour === 'affine:surface') {
+ } else if (flavour === 'affine:surface') {
const texts = [];
const elementsObj = block.get('prop:elements');
@@ -403,17 +730,12 @@ async function crawlingDocData({
}
blockDocuments.push({
- docId,
- flavour,
- blockId,
+ ...commonBlockProps,
content: texts,
parentFlavour,
parentBlockId,
- yblock: block,
});
- }
-
- if (flavour === 'affine:database') {
+ } else if (flavour === 'affine:database') {
const texts = [];
const columnsObj = block.get('prop:columns');
const databaseTitle = block.get('prop:title');
@@ -450,11 +772,21 @@ async function crawlingDocData({
}
blockDocuments.push({
- docId,
- flavour,
- blockId,
+ ...commonBlockProps,
content: texts,
- yblock: block,
+ additional: {
+ ...commonBlockProps.additional,
+ databaseName: databaseTitle?.toString(),
+ },
+ });
+ } else if (flavour === 'affine:latex') {
+ blockDocuments.push({
+ ...commonBlockProps,
+ content: block.get('prop:latex')?.toString() ?? '',
+ });
+ } else if (bookmarkFlavours.has(flavour)) {
+ blockDocuments.push({
+ ...commonBlockProps,
});
}
}
@@ -464,15 +796,28 @@ async function crawlingDocData({
const TARGET_PREVIEW_CHARACTER = 500;
const TARGET_PREVIOUS_BLOCK = 1;
const TARGET_FOLLOW_BLOCK = 4;
- for (let i = 0; i < blockDocuments.length; i++) {
- const block = blockDocuments[i];
- if (block.ref) {
+ for (const block of blockDocuments) {
+ if (block.ref?.length) {
+ const target = block;
+
+ // should only generate the markdown preview belong to the same affine:note
+ const noteBlock = nearestByFlavour(block.blockId, 'affine:note');
+
+ const sameNoteBlocks = noteBlock
+ ? blockDocuments.filter(
+ candidate =>
+ nearestByFlavour(candidate.blockId, 'affine:note') === noteBlock
+ )
+ : [];
+
// only generate markdown preview for reference blocks
- let previewText = (await generateMarkdownPreview(block)) ?? '';
+ let previewText = (await generateMarkdownPreview(target)) ?? '';
let previousBlock = 0;
let followBlock = 0;
- let previousIndex = i;
- let followIndex = i;
+ let previousIndex = sameNoteBlocks.findIndex(
+ block => block.blockId === target.blockId
+ );
+ let followIndex = previousIndex;
while (
!(
@@ -480,14 +825,14 @@ async function crawlingDocData({
previewText.length > TARGET_PREVIEW_CHARACTER || // stop if preview text reaches the limit
((previousBlock >= TARGET_PREVIOUS_BLOCK || previousIndex < 0) &&
(followBlock >= TARGET_FOLLOW_BLOCK ||
- followIndex >= blockDocuments.length))
+ followIndex >= sameNoteBlocks.length))
) // stop if no more blocks, or preview block reaches the limit
)
) {
if (previousBlock < TARGET_PREVIOUS_BLOCK) {
previousIndex--;
const block =
- previousIndex >= 0 ? blockDocuments.at(previousIndex) : null;
+ previousIndex >= 0 ? sameNoteBlocks.at(previousIndex) : null;
const markdown = block
? await generateMarkdownPreview(block)
: null;
@@ -497,14 +842,14 @@ async function crawlingDocData({
markdown
) /* A small hack to skip blocks with the same content */
) {
- previewText = markdown + previewText;
+ previewText = markdown + '\n' + previewText;
previousBlock++;
}
}
if (followBlock < TARGET_FOLLOW_BLOCK) {
followIndex++;
- const block = blockDocuments.at(followIndex);
+ const block = sameNoteBlocks.at(followIndex);
const markdown = block
? await generateMarkdownPreview(block)
: null;
@@ -514,13 +859,13 @@ async function crawlingDocData({
markdown
) /* A small hack to skip blocks with the same content */
) {
- previewText = previewText + markdown;
+ previewText = previewText + '\n' + markdown;
followBlock++;
}
}
}
- block.markdownPreview = previewText;
+ block.markdownPreview = unindentMarkdown(previewText);
}
}
// #endregion
diff --git a/packages/frontend/core/src/modules/docs-search/worker/types.ts b/packages/frontend/core/src/modules/docs-search/worker/types.ts
index b3d33a8ec7347..82d88b32421ac 100644
--- a/packages/frontend/core/src/modules/docs-search/worker/types.ts
+++ b/packages/frontend/core/src/modules/docs-search/worker/types.ts
@@ -30,12 +30,14 @@ export type WorkerInput =
| {
type: 'rootDoc';
rootDocBuffer: Uint8Array;
+ rootDocId: string;
allIndexedDocs: string[];
reindexAll?: boolean;
}
| {
type: 'doc';
storageDocId: string;
+ rootDocId: string;
rootDocBuffer: Uint8Array;
docBuffer: Uint8Array;
};
diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json
index 9df5cfc21d01c..6e3e1668a7913 100644
--- a/packages/frontend/i18n/src/resources/en.json
+++ b/packages/frontend/i18n/src/resources/en.json
@@ -1486,5 +1486,7 @@
"com.affine.editor.at-menu.import": "Import",
"com.affine.editor.at-menu.more-docs-hint": "{{count}} more docs",
"com.affine.editor.at-menu.journal": "Journal",
- "com.affine.editor.at-menu.date-picker": "Select a specific date"
+ "com.affine.editor.at-menu.date-picker": "Select a specific date",
+ "com.affine.editor.bi-directional-link-panel.show": "Show",
+ "com.affine.editor.bi-directional-link-panel.hide": "Hide"
}
diff --git a/tests/affine-local/e2e/links.spec.ts b/tests/affine-local/e2e/links.spec.ts
index d35b30d937bcf..103fc70a5e7eb 100644
--- a/tests/affine-local/e2e/links.spec.ts
+++ b/tests/affine-local/e2e/links.spec.ts
@@ -7,6 +7,7 @@ import { coreUrl, openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
createLinkedPage,
+ getBlockSuiteEditorTitle,
waitForEmptyEditor,
} from '@affine-test/kit/utils/page-logic';
import { expect, type Locator } from '@playwright/test';
@@ -418,3 +419,31 @@ test('@ popover with click "select a specific date" should show a date picker',
page.locator('affine-reference:has-text("' + date + '")')
).toBeVisible();
});
+
+test('linked doc should show markdown preview in the backlink section', async ({
+ page,
+}) => {
+ await waitForEmptyEditor(page);
+ await page.keyboard.type('source page');
+ await page.keyboard.press('Enter');
+
+ await page.keyboard.type('some inline content');
+ await page.keyboard.press('Enter');
+
+ await createLinkedPage(page, 'Test Page');
+ await page.locator('affine-reference:has-text("Test Page")').click();
+
+ await expect(getBlockSuiteEditorTitle(page)).toHaveText('Test Page');
+ await page
+ .getByRole('button', {
+ name: 'Show',
+ })
+ .click();
+
+ await page.getByRole('button', { name: 'source page' }).click();
+
+ await expect(page.locator('text-renderer')).toContainText(
+ 'some inline content'
+ );
+ await expect(page.locator('text-renderer')).toContainText('Test Page');
+});