mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
feat(core): support block links on cmdk (#8192)
Upstreams: https://github.com/toeverything/blocksuite/pull/8260 Closes: [BS-1323](https://linear.app/affine-design/issue/BS-1323/粘贴-link-to-block-到-link-弹窗,不符合预期)
This commit is contained in:
@@ -16,7 +16,7 @@ import type { ActivePeekView } from '@affine/core/modules/peek-view/entities/pee
|
|||||||
import {
|
import {
|
||||||
CreationQuickSearchSession,
|
CreationQuickSearchSession,
|
||||||
DocsQuickSearchSession,
|
DocsQuickSearchSession,
|
||||||
type QuickSearchItem,
|
LinksQuickSearchSession,
|
||||||
QuickSearchService,
|
QuickSearchService,
|
||||||
RecentDocsQuickSearchSession,
|
RecentDocsQuickSearchSession,
|
||||||
} from '@affine/core/modules/quicksearch';
|
} from '@affine/core/modules/quicksearch';
|
||||||
@@ -32,6 +32,8 @@ import type {
|
|||||||
AffineReference,
|
AffineReference,
|
||||||
DocMode,
|
DocMode,
|
||||||
DocModeProvider,
|
DocModeProvider,
|
||||||
|
QuickSearchResult,
|
||||||
|
ReferenceParams,
|
||||||
RootService,
|
RootService,
|
||||||
} from '@blocksuite/blocks';
|
} from '@blocksuite/blocks';
|
||||||
import {
|
import {
|
||||||
@@ -46,7 +48,6 @@ import {
|
|||||||
QuickSearchProvider,
|
QuickSearchProvider,
|
||||||
ReferenceNodeConfigExtension,
|
ReferenceNodeConfigExtension,
|
||||||
} from '@blocksuite/blocks';
|
} from '@blocksuite/blocks';
|
||||||
import { LinkIcon } from '@blocksuite/icons/rc';
|
|
||||||
import { AIChatBlockSchema } from '@blocksuite/presets';
|
import { AIChatBlockSchema } from '@blocksuite/presets';
|
||||||
import type { BlockSnapshot } from '@blocksuite/store';
|
import type { BlockSnapshot } from '@blocksuite/store';
|
||||||
import {
|
import {
|
||||||
@@ -282,10 +283,7 @@ export function patchDocModeService(
|
|||||||
export function patchQuickSearchService(framework: FrameworkProvider) {
|
export function patchQuickSearchService(framework: FrameworkProvider) {
|
||||||
const QuickSearch = QuickSearchExtension({
|
const QuickSearch = QuickSearchExtension({
|
||||||
async searchDoc(options) {
|
async searchDoc(options) {
|
||||||
let searchResult:
|
let searchResult: QuickSearchResult = null;
|
||||||
| { docId: string; isNewDoc?: boolean }
|
|
||||||
| { userInput: string }
|
|
||||||
| null = null;
|
|
||||||
if (options.skipSelection) {
|
if (options.skipSelection) {
|
||||||
const query = options.userInput;
|
const query = options.userInput;
|
||||||
if (!query) {
|
if (!query) {
|
||||||
@@ -319,43 +317,45 @@ export function patchQuickSearchService(framework: FrameworkProvider) {
|
|||||||
framework.get(QuickSearchService).quickSearch.show(
|
framework.get(QuickSearchService).quickSearch.show(
|
||||||
[
|
[
|
||||||
framework.get(RecentDocsQuickSearchSession),
|
framework.get(RecentDocsQuickSearchSession),
|
||||||
framework.get(DocsQuickSearchSession),
|
|
||||||
framework.get(CreationQuickSearchSession),
|
framework.get(CreationQuickSearchSession),
|
||||||
(query: string) => {
|
framework.get(DocsQuickSearchSession),
|
||||||
if (
|
framework.get(LinksQuickSearchSession),
|
||||||
(query.startsWith('http://') ||
|
|
||||||
query.startsWith('https://')) &&
|
|
||||||
resolveLinkToDoc(query) === null
|
|
||||||
) {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: 'link',
|
|
||||||
source: 'link',
|
|
||||||
icon: LinkIcon,
|
|
||||||
label: {
|
|
||||||
key: 'com.affine.cmdk.affine.insert-link',
|
|
||||||
},
|
|
||||||
payload: { url: query },
|
|
||||||
} as QuickSearchItem<'link', { url: string }>,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
result => {
|
result => {
|
||||||
if (result === null) {
|
if (result === null) {
|
||||||
resolve(null);
|
resolve(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (result.source === 'docs' || result.source === 'recent-doc') {
|
|
||||||
|
if (result.source === 'docs') {
|
||||||
resolve({
|
resolve({
|
||||||
docId: result.payload.docId,
|
docId: result.payload.docId,
|
||||||
});
|
});
|
||||||
} else if (result.source === 'link') {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.source === 'recent-doc') {
|
||||||
resolve({
|
resolve({
|
||||||
userInput: result.payload.url,
|
docId: result.payload.docId,
|
||||||
});
|
});
|
||||||
} else if (result.source === 'creation') {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.source === 'link') {
|
||||||
|
if (result.payload.external) {
|
||||||
|
const userInput = result.payload.external.url;
|
||||||
|
resolve({ userInput });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.payload.internal) {
|
||||||
|
const { docId, params } = result.payload.internal;
|
||||||
|
resolve({ docId, params });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.source === 'creation') {
|
||||||
const docsService = framework.get(DocsService);
|
const docsService = framework.get(DocsService);
|
||||||
const mode =
|
const mode =
|
||||||
result.id === 'creation:create-edgeless'
|
result.id === 'creation:create-edgeless'
|
||||||
@@ -365,10 +365,12 @@ export function patchQuickSearchService(framework: FrameworkProvider) {
|
|||||||
primaryMode: mode,
|
primaryMode: mode,
|
||||||
title: result.payload.title,
|
title: result.payload.title,
|
||||||
});
|
});
|
||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
docId: newDoc.id,
|
docId: newDoc.id,
|
||||||
isNewDoc: true,
|
isNewDoc: true,
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -413,17 +415,27 @@ export function patchQuickSearchService(framework: FrameworkProvider) {
|
|||||||
const linkedDoc = std.collection.getDoc(result.docId);
|
const linkedDoc = std.collection.getDoc(result.docId);
|
||||||
if (!linkedDoc) return;
|
if (!linkedDoc) return;
|
||||||
|
|
||||||
host.doc.addSiblingBlocks(model, [
|
const props: {
|
||||||
{
|
flavour: string;
|
||||||
flavour: 'affine:embed-linked-doc',
|
pageId: string;
|
||||||
pageId: linkedDoc.id,
|
params?: ReferenceParams;
|
||||||
},
|
} = {
|
||||||
]);
|
flavour: 'affine:embed-linked-doc',
|
||||||
|
pageId: linkedDoc.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!result.isNewDoc && result.params) {
|
||||||
|
props.params = result.params;
|
||||||
|
}
|
||||||
|
|
||||||
|
host.doc.addSiblingBlocks(model, [props]);
|
||||||
|
|
||||||
if (result.isNewDoc) {
|
if (result.isNewDoc) {
|
||||||
track.doc.editor.slashMenu.createDoc({ control: 'linkDoc' });
|
track.doc.editor.slashMenu.createDoc({ control: 'linkDoc' });
|
||||||
track.doc.editor.slashMenu.linkDoc({ control: 'createDoc' });
|
track.doc.editor.slashMenu.linkDoc({ control: 'createDoc' });
|
||||||
|
} else {
|
||||||
|
track.doc.editor.slashMenu.linkDoc({ control: 'linkDoc' });
|
||||||
}
|
}
|
||||||
track.doc.editor.slashMenu.linkDoc({ control: 'linkDoc' });
|
|
||||||
} else if ('userInput' in result) {
|
} else if ('userInput' in result) {
|
||||||
const embedOptions = std
|
const embedOptions = std
|
||||||
.get(EmbedOptionProvider)
|
.get(EmbedOptionProvider)
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import { truncate } from 'lodash-es';
|
|||||||
import { EMPTY, map, mergeMap, of, switchMap } from 'rxjs';
|
import { EMPTY, map, mergeMap, of, switchMap } from 'rxjs';
|
||||||
|
|
||||||
import type { DocsSearchService } from '../../docs-search';
|
import type { DocsSearchService } from '../../docs-search';
|
||||||
import { resolveLinkToDoc } from '../../navigation';
|
|
||||||
import type { QuickSearchSession } from '../providers/quick-search-provider';
|
import type { QuickSearchSession } from '../providers/quick-search-provider';
|
||||||
import type { DocDisplayMetaService } from '../services/doc-display-meta';
|
import type { DocDisplayMetaService } from '../services/doc-display-meta';
|
||||||
import type { QuickSearchItem } from '../types/item';
|
import type { QuickSearchItem } from '../types/item';
|
||||||
@@ -55,29 +54,7 @@ export class DocsQuickSearchSession
|
|||||||
if (!query) {
|
if (!query) {
|
||||||
out = of([] as QuickSearchItem<'docs', DocsPayload>[]);
|
out = of([] as QuickSearchItem<'docs', DocsPayload>[]);
|
||||||
} else {
|
} else {
|
||||||
const resolvedDoc = resolveLinkToDoc(query);
|
|
||||||
const resolvedDocId = resolvedDoc?.docId;
|
|
||||||
const resolvedBlockId = resolvedDoc?.blockIds?.[0];
|
|
||||||
|
|
||||||
out = this.docsSearchService.search$(query).pipe(
|
out = this.docsSearchService.search$(query).pipe(
|
||||||
map(docs => {
|
|
||||||
if (
|
|
||||||
resolvedDocId &&
|
|
||||||
!docs.some(doc => doc.docId === resolvedDocId)
|
|
||||||
) {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
docId: resolvedDocId,
|
|
||||||
score: 100,
|
|
||||||
blockId: resolvedBlockId,
|
|
||||||
blockContent: '',
|
|
||||||
},
|
|
||||||
...docs,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return docs;
|
|
||||||
}),
|
|
||||||
map(docs =>
|
map(docs =>
|
||||||
docs
|
docs
|
||||||
.map(doc => {
|
.map(doc => {
|
||||||
|
|||||||
107
packages/frontend/core/src/modules/quicksearch/impls/links.ts
Normal file
107
packages/frontend/core/src/modules/quicksearch/impls/links.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import type { ReferenceParams } from '@blocksuite/blocks';
|
||||||
|
import { BlockLinkIcon, LinkIcon } from '@blocksuite/icons/rc';
|
||||||
|
import type { DocsService } from '@toeverything/infra';
|
||||||
|
import { Entity, LiveData } from '@toeverything/infra';
|
||||||
|
import { isEmpty, pick, truncate } from 'lodash-es';
|
||||||
|
|
||||||
|
import { resolveLinkToDoc } from '../../navigation';
|
||||||
|
import type { QuickSearchSession } from '../providers/quick-search-provider';
|
||||||
|
import type { DocDisplayMetaService } from '../services/doc-display-meta';
|
||||||
|
import type { QuickSearchItem } from '../types/item';
|
||||||
|
|
||||||
|
type LinkPayload = {
|
||||||
|
internal?: {
|
||||||
|
docId: string;
|
||||||
|
title?: string;
|
||||||
|
blockId?: string;
|
||||||
|
blockContent?: string;
|
||||||
|
params?: ReferenceParams;
|
||||||
|
};
|
||||||
|
external?: {
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export class LinksQuickSearchSession
|
||||||
|
extends Entity
|
||||||
|
implements QuickSearchSession<'link', LinkPayload>
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
private readonly docsService: DocsService,
|
||||||
|
private readonly docDisplayMetaService: DocDisplayMetaService
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
query$ = new LiveData('');
|
||||||
|
|
||||||
|
items$ = LiveData.computed(get => {
|
||||||
|
const query = get(this.query$);
|
||||||
|
if (!query) return [];
|
||||||
|
|
||||||
|
const isLink = query.startsWith('http://') || query.startsWith('https://');
|
||||||
|
if (!isLink) return [];
|
||||||
|
|
||||||
|
const resolvedDoc = resolveLinkToDoc(query);
|
||||||
|
if (!resolvedDoc) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'link',
|
||||||
|
source: 'link',
|
||||||
|
icon: LinkIcon,
|
||||||
|
label: {
|
||||||
|
key: 'com.affine.cmdk.affine.insert-link',
|
||||||
|
},
|
||||||
|
payload: { external: { url: query } },
|
||||||
|
} as QuickSearchItem<'link', LinkPayload>,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const docId = resolvedDoc.docId;
|
||||||
|
const doc = this.docsService.list.doc$(docId).value;
|
||||||
|
if (!doc || get(doc.trash$)) return [];
|
||||||
|
|
||||||
|
const params = pick(resolvedDoc, ['mode', 'blockIds', 'elementIds']);
|
||||||
|
const { title, icon, updatedDate } =
|
||||||
|
this.docDisplayMetaService.getDocDisplayMeta(doc);
|
||||||
|
const blockId = params?.blockIds?.[0];
|
||||||
|
const linkToNode = Boolean(blockId);
|
||||||
|
const score = 100;
|
||||||
|
const internal = {
|
||||||
|
docId,
|
||||||
|
score,
|
||||||
|
blockId,
|
||||||
|
blockContent: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (linkToNode && !isEmpty(params)) {
|
||||||
|
Object.assign(internal, { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: ['doc', doc.id, linkToNode ? blockId : ''].join(':'),
|
||||||
|
source: 'link',
|
||||||
|
group: {
|
||||||
|
id: 'docs',
|
||||||
|
label: {
|
||||||
|
key: 'com.affine.quicksearch.group.searchfor',
|
||||||
|
options: { query: truncate(query) },
|
||||||
|
},
|
||||||
|
score: 5,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
title: title,
|
||||||
|
},
|
||||||
|
score,
|
||||||
|
icon: linkToNode ? BlockLinkIcon : icon,
|
||||||
|
timestamp: updatedDate,
|
||||||
|
payload: { internal },
|
||||||
|
} as QuickSearchItem<'link', LinkPayload>,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
query(query: string) {
|
||||||
|
this.query$.next(query);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import { CollectionsQuickSearchSession } from './impls/collections';
|
|||||||
import { CommandsQuickSearchSession } from './impls/commands';
|
import { CommandsQuickSearchSession } from './impls/commands';
|
||||||
import { CreationQuickSearchSession } from './impls/creation';
|
import { CreationQuickSearchSession } from './impls/creation';
|
||||||
import { DocsQuickSearchSession } from './impls/docs';
|
import { DocsQuickSearchSession } from './impls/docs';
|
||||||
|
import { LinksQuickSearchSession } from './impls/links';
|
||||||
import { RecentDocsQuickSearchSession } from './impls/recent-docs';
|
import { RecentDocsQuickSearchSession } from './impls/recent-docs';
|
||||||
import { TagsQuickSearchSession } from './impls/tags';
|
import { TagsQuickSearchSession } from './impls/tags';
|
||||||
import { CMDKQuickSearchService } from './services/cmdk';
|
import { CMDKQuickSearchService } from './services/cmdk';
|
||||||
@@ -29,6 +30,7 @@ export { CollectionsQuickSearchSession } from './impls/collections';
|
|||||||
export { CommandsQuickSearchSession } from './impls/commands';
|
export { CommandsQuickSearchSession } from './impls/commands';
|
||||||
export { CreationQuickSearchSession } from './impls/creation';
|
export { CreationQuickSearchSession } from './impls/creation';
|
||||||
export { DocsQuickSearchSession } from './impls/docs';
|
export { DocsQuickSearchSession } from './impls/docs';
|
||||||
|
export { LinksQuickSearchSession } from './impls/links';
|
||||||
export { RecentDocsQuickSearchSession } from './impls/recent-docs';
|
export { RecentDocsQuickSearchSession } from './impls/recent-docs';
|
||||||
export { TagsQuickSearchSession } from './impls/tags';
|
export { TagsQuickSearchSession } from './impls/tags';
|
||||||
export type { QuickSearchItem } from './types/item';
|
export type { QuickSearchItem } from './types/item';
|
||||||
@@ -53,6 +55,7 @@ export function configureQuickSearchModule(framework: Framework) {
|
|||||||
DocsService,
|
DocsService,
|
||||||
DocDisplayMetaService,
|
DocDisplayMetaService,
|
||||||
])
|
])
|
||||||
|
.entity(LinksQuickSearchSession, [DocsService, DocDisplayMetaService])
|
||||||
.entity(CreationQuickSearchSession)
|
.entity(CreationQuickSearchSession)
|
||||||
.entity(CollectionsQuickSearchSession, [CollectionService])
|
.entity(CollectionsQuickSearchSession, [CollectionService])
|
||||||
.entity(TagsQuickSearchSession, [TagService])
|
.entity(TagsQuickSearchSession, [TagService])
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { CollectionsQuickSearchSession } from '../impls/collections';
|
|||||||
import { CommandsQuickSearchSession } from '../impls/commands';
|
import { CommandsQuickSearchSession } from '../impls/commands';
|
||||||
import { CreationQuickSearchSession } from '../impls/creation';
|
import { CreationQuickSearchSession } from '../impls/creation';
|
||||||
import { DocsQuickSearchSession } from '../impls/docs';
|
import { DocsQuickSearchSession } from '../impls/docs';
|
||||||
|
import { LinksQuickSearchSession } from '../impls/links';
|
||||||
import { RecentDocsQuickSearchSession } from '../impls/recent-docs';
|
import { RecentDocsQuickSearchSession } from '../impls/recent-docs';
|
||||||
import { TagsQuickSearchSession } from '../impls/tags';
|
import { TagsQuickSearchSession } from '../impls/tags';
|
||||||
import type { QuickSearchService } from './quick-search';
|
import type { QuickSearchService } from './quick-search';
|
||||||
@@ -31,20 +32,33 @@ export class CMDKQuickSearchService extends Service {
|
|||||||
this.framework.createEntity(CommandsQuickSearchSession),
|
this.framework.createEntity(CommandsQuickSearchSession),
|
||||||
this.framework.createEntity(CreationQuickSearchSession),
|
this.framework.createEntity(CreationQuickSearchSession),
|
||||||
this.framework.createEntity(DocsQuickSearchSession),
|
this.framework.createEntity(DocsQuickSearchSession),
|
||||||
|
this.framework.createEntity(LinksQuickSearchSession),
|
||||||
this.framework.createEntity(TagsQuickSearchSession),
|
this.framework.createEntity(TagsQuickSearchSession),
|
||||||
],
|
],
|
||||||
result => {
|
result => {
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.source === 'commands') {
|
if (result.source === 'commands') {
|
||||||
result.payload.run()?.catch(err => {
|
result.payload.run()?.catch(err => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
});
|
});
|
||||||
} else if (
|
return;
|
||||||
result.source === 'recent-doc' ||
|
}
|
||||||
result.source === 'docs'
|
|
||||||
) {
|
if (result.source === 'link') {
|
||||||
|
if (result.payload.internal) {
|
||||||
|
const { docId, params } = result.payload.internal;
|
||||||
|
this.workbenchService.workbench.openDoc({
|
||||||
|
docId,
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.source === 'recent-doc' || result.source === 'docs') {
|
||||||
const doc: {
|
const doc: {
|
||||||
docId: string;
|
docId: string;
|
||||||
blockId?: string;
|
blockId?: string;
|
||||||
@@ -62,13 +76,22 @@ export class CMDKQuickSearchService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.workbenchService.workbench.openDoc(options);
|
this.workbenchService.workbench.openDoc(options);
|
||||||
} else if (result.source === 'collections') {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.source === 'collections') {
|
||||||
this.workbenchService.workbench.openCollection(
|
this.workbenchService.workbench.openCollection(
|
||||||
result.payload.collectionId
|
result.payload.collectionId
|
||||||
);
|
);
|
||||||
} else if (result.source === 'tags') {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.source === 'tags') {
|
||||||
this.workbenchService.workbench.openTag(result.payload.tagId);
|
this.workbenchService.workbench.openTag(result.payload.tagId);
|
||||||
} else if (result.source === 'creation') {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.source === 'creation') {
|
||||||
if (result.id === 'creation:create-page') {
|
if (result.id === 'creation:create-page') {
|
||||||
const newDoc = this.docsService.createDoc({
|
const newDoc = this.docsService.createDoc({
|
||||||
primaryMode: 'page',
|
primaryMode: 'page',
|
||||||
@@ -82,6 +105,7 @@ export class CMDKQuickSearchService extends Service {
|
|||||||
});
|
});
|
||||||
this.workbenchService.workbench.openDoc(newDoc.id);
|
this.workbenchService.workbench.openDoc(newDoc.id);
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user