mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-21 08:17:10 +08:00
fix: some regression issues on quick search refactor (#7410)
- fix PD-1370. doc link resolve issue on pasting - fix AF-1029 - implement doc creation in doc search added two test cases to cover the above two issues.
This commit is contained in:
@@ -29,11 +29,11 @@ import {
|
|||||||
type RootService,
|
type RootService,
|
||||||
} from '@blocksuite/blocks';
|
} from '@blocksuite/blocks';
|
||||||
import { LinkIcon } from '@blocksuite/icons/rc';
|
import { LinkIcon } from '@blocksuite/icons/rc';
|
||||||
import type {
|
import {
|
||||||
DocMode,
|
type DocMode,
|
||||||
DocService,
|
type DocService,
|
||||||
DocsService,
|
DocsService,
|
||||||
FrameworkProvider,
|
type FrameworkProvider,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
import { type TemplateResult } from 'lit';
|
import { type TemplateResult } from 'lit';
|
||||||
import { customElement } from 'lit/decorators.js';
|
import { customElement } from 'lit/decorators.js';
|
||||||
@@ -337,13 +337,27 @@ export function patchQuickSearchService(
|
|||||||
if (!query) {
|
if (!query) {
|
||||||
logger.error('No user input provided');
|
logger.error('No user input provided');
|
||||||
} else {
|
} else {
|
||||||
const searchedDoc = (
|
const resolvedDoc = resolveLinkToDoc(query);
|
||||||
await framework.get(DocsSearchService).search(query)
|
if (resolvedDoc) {
|
||||||
).at(0);
|
|
||||||
if (searchedDoc) {
|
|
||||||
searchResult = {
|
searchResult = {
|
||||||
docId: searchedDoc.docId,
|
docId: resolvedDoc.docId,
|
||||||
};
|
};
|
||||||
|
} else if (
|
||||||
|
query.startsWith('http://') ||
|
||||||
|
query.startsWith('https://')
|
||||||
|
) {
|
||||||
|
searchResult = {
|
||||||
|
userInput: query,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const searchedDoc = (
|
||||||
|
await framework.get(DocsSearchService).search(query)
|
||||||
|
).at(0);
|
||||||
|
if (searchedDoc) {
|
||||||
|
searchResult = {
|
||||||
|
docId: searchedDoc.docId,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -394,11 +408,15 @@ export function patchQuickSearchService(
|
|||||||
result.source === 'creation' &&
|
result.source === 'creation' &&
|
||||||
result.id === 'creation:create-page'
|
result.id === 'creation:create-page'
|
||||||
) {
|
) {
|
||||||
throw new Error('Not implemented');
|
const docsService = framework.get(DocsService);
|
||||||
// resolve({
|
const newDoc = docsService.createDoc({
|
||||||
// docId: 'new-doc',
|
mode: 'page',
|
||||||
// isNewDoc: true,
|
title: result.payload.title,
|
||||||
// });
|
});
|
||||||
|
resolve({
|
||||||
|
docId: newDoc.id,
|
||||||
|
isNewDoc: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { EdgelessIcon, PageIcon, TodayIcon } from '@blocksuite/icons/rc';
|
import type { DocRecord, DocsService } from '@toeverything/infra';
|
||||||
import type { DocsService } from '@toeverything/infra';
|
|
||||||
import {
|
import {
|
||||||
effect,
|
effect,
|
||||||
Entity,
|
Entity,
|
||||||
@@ -12,8 +11,8 @@ 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 { resolveLinkToDoc } from '../../navigation';
|
||||||
import type { WorkspacePropertiesAdapter } from '../../properties';
|
|
||||||
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 { QuickSearchItem } from '../types/item';
|
import type { QuickSearchItem } from '../types/item';
|
||||||
|
|
||||||
interface DocsPayload {
|
interface DocsPayload {
|
||||||
@@ -30,7 +29,7 @@ export class DocsQuickSearchSession
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly docsSearchService: DocsSearchService,
|
private readonly docsSearchService: DocsSearchService,
|
||||||
private readonly docsService: DocsService,
|
private readonly docsService: DocsService,
|
||||||
private readonly propertiesAdapter: WorkspacePropertiesAdapter
|
private readonly docDisplayMetaService: DocDisplayMetaService
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@@ -56,62 +55,40 @@ export class DocsQuickSearchSession
|
|||||||
if (!query) {
|
if (!query) {
|
||||||
out = of([] as QuickSearchItem<'docs', DocsPayload>[]);
|
out = of([] as QuickSearchItem<'docs', DocsPayload>[]);
|
||||||
} else {
|
} else {
|
||||||
const maybeLink = resolveLinkToDoc(query);
|
out = this.docsSearchService.search$(query).pipe(
|
||||||
const docRecord = maybeLink
|
map(docs => {
|
||||||
? this.docsService.list.doc$(maybeLink.docId).value
|
const resolvedDoc = resolveLinkToDoc(query);
|
||||||
: null;
|
if (
|
||||||
|
resolvedDoc &&
|
||||||
if (docRecord) {
|
!docs.some(doc => doc.docId === resolvedDoc.docId)
|
||||||
const docMode = docRecord?.mode$.value;
|
) {
|
||||||
const icon = this.propertiesAdapter.getJournalPageDateString(
|
return [
|
||||||
docRecord.id
|
{
|
||||||
) /* is journal */
|
docId: resolvedDoc.docId,
|
||||||
? TodayIcon
|
score: 100,
|
||||||
: docMode === 'edgeless'
|
blockId: resolvedDoc.blockId,
|
||||||
? EdgelessIcon
|
blockContent: '',
|
||||||
: PageIcon;
|
|
||||||
|
|
||||||
out = of([
|
|
||||||
{
|
|
||||||
id: 'doc:' + docRecord.id,
|
|
||||||
source: 'docs',
|
|
||||||
group: {
|
|
||||||
id: 'docs',
|
|
||||||
label: {
|
|
||||||
key: 'com.affine.quicksearch.group.searchfor',
|
|
||||||
options: { query: truncate(query) },
|
|
||||||
},
|
},
|
||||||
score: 5,
|
...docs,
|
||||||
},
|
];
|
||||||
label: {
|
} else {
|
||||||
title: docRecord.title$.value || { key: 'Untitled' },
|
return docs;
|
||||||
},
|
}
|
||||||
score: 100,
|
}),
|
||||||
icon,
|
map(docs =>
|
||||||
timestamp: docRecord.meta$.value.updatedDate,
|
docs
|
||||||
payload: {
|
.map(doc => {
|
||||||
docId: docRecord.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
] as QuickSearchItem<'docs', DocsPayload>[]);
|
|
||||||
} else {
|
|
||||||
out = this.docsSearchService.search$(query).pipe(
|
|
||||||
map(docs =>
|
|
||||||
docs.map(doc => {
|
|
||||||
const docRecord = this.docsService.list.doc$(doc.docId).value;
|
const docRecord = this.docsService.list.doc$(doc.docId).value;
|
||||||
const docMode = docRecord?.mode$.value;
|
return [doc, docRecord] as const;
|
||||||
const updatedTime = docRecord?.meta$.value.updatedDate;
|
})
|
||||||
|
.filter(
|
||||||
const icon = this.propertiesAdapter.getJournalPageDateString(
|
(props): props is [(typeof props)[0], DocRecord] => !!props[1]
|
||||||
doc.docId
|
)
|
||||||
) /* is journal */
|
.map(([doc, docRecord]) => {
|
||||||
? TodayIcon
|
const { title, icon, updatedDate } =
|
||||||
: docMode === 'edgeless'
|
this.docDisplayMetaService.getDocDisplayMeta(docRecord);
|
||||||
? EdgelessIcon
|
|
||||||
: PageIcon;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: 'doc:' + doc.docId,
|
id: 'doc:' + docRecord.id,
|
||||||
source: 'docs',
|
source: 'docs',
|
||||||
group: {
|
group: {
|
||||||
id: 'docs',
|
id: 'docs',
|
||||||
@@ -122,18 +99,17 @@ export class DocsQuickSearchSession
|
|||||||
score: 5,
|
score: 5,
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
title: doc.title || { key: 'Untitled' },
|
title: title,
|
||||||
subTitle: doc.blockContent,
|
subTitle: doc.blockContent,
|
||||||
},
|
},
|
||||||
score: doc.score,
|
score: doc.score,
|
||||||
icon,
|
icon,
|
||||||
timestamp: updatedTime,
|
timestamp: updatedDate,
|
||||||
payload: doc,
|
payload: doc,
|
||||||
} as QuickSearchItem<'docs', DocsPayload>;
|
} as QuickSearchItem<'docs', DocsPayload>;
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return out.pipe(
|
return out.pipe(
|
||||||
mergeMap((items: QuickSearchItem<'docs', DocsPayload>[]) => {
|
mergeMap((items: QuickSearchItem<'docs', DocsPayload>[]) => {
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { EdgelessIcon, PageIcon, TodayIcon } from '@blocksuite/icons/rc';
|
|
||||||
import { Entity, LiveData } from '@toeverything/infra';
|
import { Entity, LiveData } from '@toeverything/infra';
|
||||||
|
|
||||||
import type { WorkspacePropertiesAdapter } from '../../properties';
|
|
||||||
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 { RecentDocsService } from '../services/recent-pages';
|
import type { RecentDocsService } from '../services/recent-pages';
|
||||||
import type { QuickSearchGroup } from '../types/group';
|
import type { QuickSearchGroup } from '../types/group';
|
||||||
import type { QuickSearchItem } from '../types/item';
|
import type { QuickSearchItem } from '../types/item';
|
||||||
@@ -21,7 +20,7 @@ export class RecentDocsQuickSearchSession
|
|||||||
{
|
{
|
||||||
constructor(
|
constructor(
|
||||||
private readonly recentDocsService: RecentDocsService,
|
private readonly recentDocsService: RecentDocsService,
|
||||||
private readonly propertiesAdapter: WorkspacePropertiesAdapter
|
private readonly docDisplayMetaService: DocDisplayMetaService
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@@ -40,20 +39,15 @@ export class RecentDocsQuickSearchSession
|
|||||||
|
|
||||||
return docRecords.map<QuickSearchItem<'recent-doc', { docId: string }>>(
|
return docRecords.map<QuickSearchItem<'recent-doc', { docId: string }>>(
|
||||||
docRecord => {
|
docRecord => {
|
||||||
const icon = this.propertiesAdapter.getJournalPageDateString(
|
const { title, icon } =
|
||||||
docRecord.id
|
this.docDisplayMetaService.getDocDisplayMeta(docRecord);
|
||||||
) /* is journal */
|
|
||||||
? TodayIcon
|
|
||||||
: docRecord.mode$.value === 'edgeless'
|
|
||||||
? EdgelessIcon
|
|
||||||
: PageIcon;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: 'recent-doc:' + docRecord.id,
|
id: 'recent-doc:' + docRecord.id,
|
||||||
source: 'recent-doc',
|
source: 'recent-doc',
|
||||||
group: group,
|
group: group,
|
||||||
label: {
|
label: {
|
||||||
title: docRecord.meta$.value.title || { key: 'Untitled' },
|
title: title,
|
||||||
},
|
},
|
||||||
score: 0,
|
score: 0,
|
||||||
icon,
|
icon,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { CreationQuickSearchSession } from './impls/creation';
|
|||||||
import { DocsQuickSearchSession } from './impls/docs';
|
import { DocsQuickSearchSession } from './impls/docs';
|
||||||
import { RecentDocsQuickSearchSession } from './impls/recent-docs';
|
import { RecentDocsQuickSearchSession } from './impls/recent-docs';
|
||||||
import { CMDKQuickSearchService } from './services/cmdk';
|
import { CMDKQuickSearchService } from './services/cmdk';
|
||||||
|
import { DocDisplayMetaService } from './services/doc-display-meta';
|
||||||
import { QuickSearchService } from './services/quick-search';
|
import { QuickSearchService } from './services/quick-search';
|
||||||
import { RecentDocsService } from './services/recent-pages';
|
import { RecentDocsService } from './services/recent-pages';
|
||||||
|
|
||||||
@@ -40,17 +41,18 @@ export function configureQuickSearchModule(framework: Framework) {
|
|||||||
DocsService,
|
DocsService,
|
||||||
])
|
])
|
||||||
.service(RecentDocsService, [WorkspaceLocalState, DocsService])
|
.service(RecentDocsService, [WorkspaceLocalState, DocsService])
|
||||||
|
.service(DocDisplayMetaService, [WorkspacePropertiesAdapter])
|
||||||
.entity(QuickSearch)
|
.entity(QuickSearch)
|
||||||
.entity(CommandsQuickSearchSession, [GlobalContextService])
|
.entity(CommandsQuickSearchSession, [GlobalContextService])
|
||||||
.entity(DocsQuickSearchSession, [
|
.entity(DocsQuickSearchSession, [
|
||||||
DocsSearchService,
|
DocsSearchService,
|
||||||
DocsService,
|
DocsService,
|
||||||
WorkspacePropertiesAdapter,
|
DocDisplayMetaService,
|
||||||
])
|
])
|
||||||
.entity(CreationQuickSearchSession)
|
.entity(CreationQuickSearchSession)
|
||||||
.entity(CollectionsQuickSearchSession, [CollectionService])
|
.entity(CollectionsQuickSearchSession, [CollectionService])
|
||||||
.entity(RecentDocsQuickSearchSession, [
|
.entity(RecentDocsQuickSearchSession, [
|
||||||
RecentDocsService,
|
RecentDocsService,
|
||||||
WorkspacePropertiesAdapter,
|
DocDisplayMetaService,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { i18nTime } from '@affine/i18n';
|
||||||
|
import { EdgelessIcon, PageIcon, TodayIcon } from '@blocksuite/icons/rc';
|
||||||
|
import type { DocRecord } from '@toeverything/infra';
|
||||||
|
import { Service } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import type { WorkspacePropertiesAdapter } from '../../properties';
|
||||||
|
|
||||||
|
export class DocDisplayMetaService extends Service {
|
||||||
|
constructor(private readonly propertiesAdapter: WorkspacePropertiesAdapter) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
getDocDisplayMeta(docRecord: DocRecord) {
|
||||||
|
const journalDateString = this.propertiesAdapter.getJournalPageDateString(
|
||||||
|
docRecord.id
|
||||||
|
);
|
||||||
|
const icon = journalDateString
|
||||||
|
? TodayIcon
|
||||||
|
: docRecord.mode$.value === 'edgeless'
|
||||||
|
? EdgelessIcon
|
||||||
|
: PageIcon;
|
||||||
|
|
||||||
|
const title = journalDateString
|
||||||
|
? i18nTime(journalDateString, { absolute: { accuracy: 'day' } })
|
||||||
|
: docRecord.meta$.value.title ||
|
||||||
|
({
|
||||||
|
key: 'Untitled',
|
||||||
|
} as const);
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: title,
|
||||||
|
icon: icon,
|
||||||
|
updatedDate: docRecord.meta$.value.updatedDate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -507,3 +507,73 @@ test('can use @ to open quick search to search for doc and insert into canvas',
|
|||||||
await page.locator('affine-embed-linked-doc-block').dblclick({ force: true });
|
await page.locator('affine-embed-linked-doc-block').dblclick({ force: true });
|
||||||
await expect(page.getByTestId('peek-view-modal')).toBeVisible();
|
await expect(page.getByTestId('peek-view-modal')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('can paste a doc link to create link reference', async ({ page }) => {
|
||||||
|
await openHomePage(page);
|
||||||
|
await waitForEditorLoad(page);
|
||||||
|
const url = page.url();
|
||||||
|
await clickNewPageButton(page);
|
||||||
|
|
||||||
|
// goto main content
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
|
||||||
|
// paste the url
|
||||||
|
await page.evaluate(
|
||||||
|
async ([url]) => {
|
||||||
|
const clipData = {
|
||||||
|
'text/plain': url,
|
||||||
|
};
|
||||||
|
const e = new ClipboardEvent('paste', {
|
||||||
|
clipboardData: new DataTransfer(),
|
||||||
|
});
|
||||||
|
Object.defineProperty(e, 'target', {
|
||||||
|
writable: false,
|
||||||
|
value: document,
|
||||||
|
});
|
||||||
|
Object.entries(clipData).forEach(([key, value]) => {
|
||||||
|
e.clipboardData?.setData(key, value);
|
||||||
|
});
|
||||||
|
document.dispatchEvent(e);
|
||||||
|
},
|
||||||
|
[url]
|
||||||
|
);
|
||||||
|
|
||||||
|
// check the link reference
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
await expect(
|
||||||
|
page.locator('affine-reference:has-text("Write, Draw, Plan all at Once.")')
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// can ctrl-z to revert to normal link
|
||||||
|
await page.keyboard.press('ControlOrMeta+z');
|
||||||
|
|
||||||
|
// check the normal link
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
await expect(page.locator(`affine-link:has-text("${url}")`)).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can use slash menu to insert a newly created doc card', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await openHomePage(page);
|
||||||
|
await clickNewPageButton(page);
|
||||||
|
|
||||||
|
// goto main content
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
|
||||||
|
// open slash menu
|
||||||
|
await page.keyboard.type('/linkedoc', {
|
||||||
|
delay: 50,
|
||||||
|
});
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await expect(page.getByTestId('cmdk-quick-search')).toBeVisible();
|
||||||
|
|
||||||
|
const testTitle = 'test title';
|
||||||
|
await page.locator('[cmdk-input]').fill(testTitle);
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
|
||||||
|
await expect(page.locator('affine-embed-linked-doc-block')).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.locator('.affine-embed-linked-doc-content-title')
|
||||||
|
).toContainText(testTitle);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user