diff --git a/packages/frontend/core/src/atoms/index.ts b/packages/frontend/core/src/atoms/index.ts index e90313aad6..5f107830e0 100644 --- a/packages/frontend/core/src/atoms/index.ts +++ b/packages/frontend/core/src/atoms/index.ts @@ -1,5 +1,4 @@ import { atom } from 'jotai'; -import { atomWithStorage } from 'jotai/utils'; import type { AuthProps } from '../components/affine/auth'; import type { CreateWorkspaceMode } from '../components/affine/create-workspace-modal'; @@ -7,7 +6,6 @@ import type { SettingProps } from '../components/affine/setting-modal'; // modal atoms export const openWorkspacesModalAtom = atom(false); export const openCreateWorkspaceModalAtom = atom(false); -export const openQuickSearchModalAtom = atom(false); export const openSignOutModalAtom = atom(false); export const openPaymentDisableAtom = atom(false); export const openQuotaModalAtom = atom(false); @@ -46,11 +44,6 @@ export const authAtom = atom({ export const openDisableCloudAlertModalAtom = atom(false); -export const recentPageIdsBaseAtom = atomWithStorage( - 'recentPageSettings', - [] -); - export type AllPageFilterOption = 'docs' | 'collections' | 'tags'; export const allPageFilterSelectAtom = atom('docs'); diff --git a/packages/frontend/core/src/commands/affine-settings.tsx b/packages/frontend/core/src/commands/affine-settings.tsx index 1bc2b56e6f..e6c52aef04 100644 --- a/packages/frontend/core/src/commands/affine-settings.tsx +++ b/packages/frontend/core/src/commands/affine-settings.tsx @@ -5,17 +5,14 @@ import { appSettingAtom } from '@toeverything/infra'; import type { createStore } from 'jotai'; import type { useTheme } from 'next-themes'; -import { openQuickSearchModalAtom } from '../atoms'; import type { useLanguageHelper } from '../hooks/affine/use-language-helper'; -import { mixpanel } from '../utils'; -import { PreconditionStrategy, registerAffineCommand } from './registry'; +import { registerAffineCommand } from './registry'; export function registerAffineSettingsCommands({ t, store, theme, languageHelper, - editor, }: { t: ReturnType; store: ReturnType; @@ -25,36 +22,6 @@ export function registerAffineSettingsCommands({ }) { const unsubs: Array<() => void> = []; const { onLanguageChange, languagesList, currentLanguage } = languageHelper; - unsubs.push( - registerAffineCommand({ - id: 'affine:show-quick-search', - preconditionStrategy: PreconditionStrategy.Never, - category: 'affine:general', - keyBinding: { - binding: '$mod+K', - }, - label: '', - icon: , - run() { - mixpanel.track('QuickSearchOpened', { - control: 'shortcut', - }); - const quickSearchModalState = store.get(openQuickSearchModalAtom); - - if (!editor) { - return store.set(openQuickSearchModalAtom, !quickSearchModalState); - } - // Due to a conflict with the shortcut for creating a link after selecting text in blocksuite, - // opening the quick search modal is disabled when link-popup is visitable. - const textSelection = editor.host?.std.selection.find('text'); - if (textSelection && textSelection.from.length > 0) { - const linkPopup = document.querySelector('link-popup'); - if (linkPopup) return; - } - return store.set(openQuickSearchModalAtom, !quickSearchModalState); - }, - }) - ); // color modes unsubs.push( diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx index ffec63a11e..ed4b7793f1 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx @@ -5,6 +5,7 @@ import { usePromptModal, } from '@affine/component'; import { useJournalInfoHelper } from '@affine/core/hooks/use-journal'; +import { QuickSearchService } from '@affine/core/modules/cmdk'; import { PeekViewService } from '@affine/core/modules/peek-view'; import { WorkbenchService } from '@affine/core/modules/workbench'; import type { BlockSpec } from '@blocksuite/block-std'; @@ -33,6 +34,7 @@ import { BlocksuiteEditorJournalDocTitle } from './journal-doc-title'; import { patchNotificationService, patchPeekViewService, + patchQuickSearchService, patchReferenceRenderer, type ReferenceReactRenderer, } from './specs/custom/spec-patchers'; @@ -70,6 +72,7 @@ interface BlocksuiteEditorProps { const usePatchSpecs = (page: Doc, specs: BlockSpec[]) => { const [reactToLit, portals] = useLitPortalFactory(); const peekViewService = useService(PeekViewService); + const quickSearchService = useService(QuickSearchService); const referenceRenderer: ReferenceReactRenderer = useMemo(() => { return function customReference(reference) { const pageId = reference.delta.attributes?.reference?.pageId; @@ -91,12 +94,16 @@ const usePatchSpecs = (page: Doc, specs: BlockSpec[]) => { if (!page.readonly && runtimeConfig.enablePeekView) { patched = patchPeekViewService(patched, peekViewService); } + if (!page.readonly) { + patched = patchQuickSearchService(patched, quickSearchService); + } return patched; }, [ confirmModal, openPromptModal, page.readonly, peekViewService, + quickSearchService, reactToLit, referenceRenderer, specs, diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/spec-patchers.ts b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/spec-patchers.ts index 1db44347f5..604d536c59 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/spec-patchers.ts +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/spec-patchers.ts @@ -7,6 +7,7 @@ import { type useConfirmModal, type usePromptModal, } from '@affine/component'; +import type { QuickSearchService } from '@affine/core/modules/cmdk'; import type { PeekViewService } from '@affine/core/modules/peek-view'; import type { ActivePeekView } from '@affine/core/modules/peek-view/entities/peek-view'; import { DebugLogger } from '@affine/debug'; @@ -237,3 +238,37 @@ export function patchPeekViewService( return specs; } + +export function patchQuickSearchService( + specs: BlockSpec[], + service: QuickSearchService +) { + const rootSpec = specs.find( + spec => spec.schema.model.flavour === 'affine:page' + ) as BlockSpec; + + if (!rootSpec) { + return specs; + } + + patchSpecService(rootSpec, pageService => { + pageService.quickSearchService = { + async searchDoc(options) { + const result = await service.quickSearch.search(options.userInput); + if (result) { + if ('docId' in result) { + return result; + } else { + return { + userInput: result.query, + action: 'insert', + }; + } + } + return null; + }, + }; + }); + + return specs; +} diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-page-list/utils.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-page-list/utils.tsx index 9886a12ecf..35b5e69ece 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-page-list/utils.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-page-list/utils.tsx @@ -22,19 +22,22 @@ export const usePageHelper = (docCollection: DocCollection) => { ); const createPageAndOpen = useCallback( - (mode?: 'page' | 'edgeless') => { + (mode?: 'page' | 'edgeless', open?: boolean) => { const page = createDoc(); initEmptyPage(page); docRecordList.doc$(page.id).value?.setMode(mode || 'page'); - openPage(docCollection.id, page.id); + if (open !== false) openPage(docCollection.id, page.id); return page; }, [docCollection.id, createDoc, openPage, docRecordList] ); - const createEdgelessAndOpen = useCallback(() => { - return createPageAndOpen('edgeless'); - }, [createPageAndOpen]); + const createEdgelessAndOpen = useCallback( + (open?: boolean) => { + return createPageAndOpen('edgeless', open); + }, + [createPageAndOpen] + ); const importFileAndOpen = useMemo( () => async () => { diff --git a/packages/frontend/core/src/hooks/use-register-workspace-commands.ts b/packages/frontend/core/src/hooks/use-register-workspace-commands.ts index 46d379fd7e..f6787534a5 100644 --- a/packages/frontend/core/src/hooks/use-register-workspace-commands.ts +++ b/packages/frontend/core/src/hooks/use-register-workspace-commands.ts @@ -1,10 +1,13 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import type { AffineEditorContainer } from '@blocksuite/presets'; import { useService, WorkspaceService } from '@toeverything/infra'; import { useStore } from 'jotai'; import { useTheme } from 'next-themes'; import { useEffect } from 'react'; import { + PreconditionStrategy, + registerAffineCommand, registerAffineCreationCommands, registerAffineHelpCommands, registerAffineLayoutCommands, @@ -13,10 +16,46 @@ import { registerAffineUpdatesCommands, } from '../commands'; import { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils'; +import { QuickSearchService } from '../modules/cmdk'; import { useLanguageHelper } from './affine/use-language-helper'; import { useActiveBlocksuiteEditor } from './use-block-suite-editor'; import { useNavigateHelper } from './use-navigate-helper'; +function hasLinkPopover(editor: AffineEditorContainer | null) { + const textSelection = editor?.host?.std.selection.find('text'); + if (textSelection && textSelection.from.length > 0) { + const linkPopup = document.querySelector('link-popup'); + if (linkPopup) { + return true; + } + } + return false; +} + +function registerCMDKCommand( + qsService: QuickSearchService, + editor: AffineEditorContainer | null +) { + return registerAffineCommand({ + id: 'affine:show-quick-search', + preconditionStrategy: PreconditionStrategy.Never, + category: 'affine:general', + keyBinding: { + binding: '$mod+K', + }, + label: '', + icon: '', + run() { + // Due to a conflict with the shortcut for creating a link after selecting text in blocksuite, + // opening the quick search modal is disabled when link-popup is visitable. + if (hasLinkPopover(editor)) { + return; + } + qsService.quickSearch.toggle(); + }, + }); +} + export function useRegisterWorkspaceCommands() { const store = useStore(); const t = useAFFiNEI18N(); @@ -26,6 +65,15 @@ export function useRegisterWorkspaceCommands() { const pageHelper = usePageHelper(currentWorkspace.docCollection); const navigationHelper = useNavigateHelper(); const [editor] = useActiveBlocksuiteEditor(); + const quickSearch = useService(QuickSearchService); + + useEffect(() => { + const unsub = registerCMDKCommand(quickSearch, editor); + + return () => { + unsub(); + }; + }, [editor, quickSearch]); // register AffineUpdatesCommands useEffect(() => { diff --git a/packages/frontend/core/src/layouts/workspace-layout.tsx b/packages/frontend/core/src/layouts/workspace-layout.tsx index 89692e25b1..5c91181ecf 100644 --- a/packages/frontend/core/src/layouts/workspace-layout.tsx +++ b/packages/frontend/core/src/layouts/workspace-layout.tsx @@ -14,13 +14,13 @@ import { useService, WorkspaceService, } from '@toeverything/infra'; -import { useAtom, useAtomValue, useSetAtom } from 'jotai'; +import { useAtomValue, useSetAtom } from 'jotai'; import type { PropsWithChildren, ReactNode } from 'react'; import { lazy, useCallback, useEffect, useMemo, useState } from 'react'; import { createPortal } from 'react-dom'; import { Map as YMap } from 'yjs'; -import { openQuickSearchModalAtom, openSettingModalAtom } from '../atoms'; +import { openSettingModalAtom } from '../atoms'; import { WorkspaceAIOnboarding } from '../components/affine/ai-onboarding'; import { AppContainer } from '../components/affine/app-container'; import { SyncAwareness } from '../components/affine/awareness'; @@ -38,6 +38,7 @@ import { import { useRegisterFindInPageCommands } from '../hooks/affine/use-register-find-in-page-commands'; import { useNavigateHelper } from '../hooks/use-navigate-helper'; import { useRegisterWorkspaceCommands } from '../hooks/use-register-workspace-commands'; +import { QuickSearchService } from '../modules/cmdk'; import { useRegisterNavigationCommands } from '../modules/navigation/view/use-register-navigation-commands'; import { WorkbenchService } from '../modules/workbench'; import { @@ -50,21 +51,25 @@ import { mixpanel } from '../utils'; import * as styles from './styles.css'; const CMDKQuickSearchModal = lazy(() => - import('../components/pure/cmdk').then(module => ({ + import('../modules/cmdk/views').then(module => ({ default: module.CMDKQuickSearchModal, })) ); export const QuickSearch = () => { - const [openQuickSearchModal, setOpenQuickSearchModalAtom] = useAtom( - openQuickSearchModalAtom - ); + const quickSearch = useService(QuickSearchService).quickSearch; + const open = useLiveData(quickSearch.show$); const onToggleQuickSearch = useCallback( (open: boolean) => { - setOpenQuickSearchModalAtom(open); + if (open) { + // should never be here + quickSearch.show(); + } else { + quickSearch.hide(); + } }, - [setOpenQuickSearchModalAtom] + [quickSearch] ); const docRecordList = useService(DocsService).list; @@ -78,7 +83,7 @@ export const QuickSearch = () => { return ( @@ -145,14 +150,14 @@ export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => { return pageHelper.createPage(); }, [pageHelper]); - const [, setOpenQuickSearchModalAtom] = useAtom(openQuickSearchModalAtom); + const quickSearch = useService(QuickSearchService).quickSearch; const handleOpenQuickSearchModal = useCallback(() => { - setOpenQuickSearchModalAtom(true); + quickSearch.show(); mixpanel.track('QuickSearchOpened', { segment: 'navigation panel', control: 'search button', }); - }, [setOpenQuickSearchModalAtom]); + }, [quickSearch]); const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom); diff --git a/packages/frontend/core/src/modules/cmdk/entities/quick-search.ts b/packages/frontend/core/src/modules/cmdk/entities/quick-search.ts new file mode 100644 index 0000000000..7c2c3d414d --- /dev/null +++ b/packages/frontend/core/src/modules/cmdk/entities/quick-search.ts @@ -0,0 +1,146 @@ +import type { + DocRecord, + DocsService, + WorkspaceService, +} from '@toeverything/infra'; +import { Entity, LiveData } from '@toeverything/infra'; + +import { resolveLinkToDoc } from '../../navigation'; + +type QuickSearchMode = 'commands' | 'docs'; + +export type SearchCallbackResult = + | { + docId: string; + blockId?: string; + } + | { + query: string; + action: 'insert'; + }; + +// todo: move command registry to entity as well +export class QuickSearch extends Entity { + constructor( + private readonly docsService: DocsService, + private readonly workspaceService: WorkspaceService + ) { + super(); + } + private readonly state$ = new LiveData<{ + mode: QuickSearchMode; + query: string; + callback?: (result: SearchCallbackResult | null) => void; + } | null>(null); + + readonly show$ = this.state$.map(s => !!s); + + show = ( + mode: QuickSearchMode | null = 'commands', + opts: { + callback?: (res: SearchCallbackResult | null) => void; + query?: string; + } = {} + ) => { + if (this.state$.value?.callback) { + this.state$.value.callback(null); + } + if (mode === null) { + this.state$.next(null); + } else { + this.state$.next({ + mode, + query: opts.query ?? '', + callback: opts.callback, + }); + } + }; + + mode$ = this.state$.map(s => s?.mode); + query$ = this.state$.map(s => s?.query || ''); + + setQuery = (query: string) => { + if (!this.state$.value) return; + this.state$.next({ + ...this.state$.value, + query, + }); + }; + + hide() { + return this.show(null); + } + + toggle() { + return this.show$.value ? this.hide() : this.show(); + } + + search(query?: string) { + const { promise, resolve } = + Promise.withResolvers(); + + this.show('docs', { + callback: resolve, + query, + }); + + return promise; + } + + setSearchCallbackResult(result: SearchCallbackResult) { + if (this.state$.value?.callback) { + this.state$.value.callback(result); + } + } + + getSearchedDocs(query: string) { + const searchResults = this.workspaceService.workspace.docCollection.search( + query + ) as unknown as Map< + string, + { + space: string; + content: string; + } + >; + // make sure we don't add the same page multiple times + const added = new Set(); + const docs = this.docsService.list.docs$.value; + const searchedDocs: { + doc: DocRecord; + blockId: string; + content?: string; + source: 'search' | 'link-ref'; + }[] = Array.from(searchResults.entries()) + .map(([blockId, { space, content }]) => { + const doc = docs.find(doc => doc.id === space && !added.has(doc.id)); + if (!doc) return null; + added.add(doc.id); + + return { + doc, + blockId, + content, + source: 'search' as const, + }; + }) + .filter((res): res is NonNullable => !!res); + + const maybeRefLink = resolveLinkToDoc(query); + + if (maybeRefLink) { + const doc = this.docsService.list.docs$.value.find( + doc => doc.id === maybeRefLink.docId + ); + if (doc) { + searchedDocs.push({ + doc, + blockId: maybeRefLink.blockId, + source: 'link-ref', + }); + } + } + + return searchedDocs; + } +} diff --git a/packages/frontend/core/src/modules/cmdk/index.ts b/packages/frontend/core/src/modules/cmdk/index.ts new file mode 100644 index 0000000000..a1df1e7317 --- /dev/null +++ b/packages/frontend/core/src/modules/cmdk/index.ts @@ -0,0 +1,22 @@ +import { + DocsService, + type Framework, + WorkspaceLocalState, + WorkspaceScope, + WorkspaceService, +} from '@toeverything/infra'; + +import { QuickSearch } from './entities/quick-search'; +import { QuickSearchService } from './services/quick-search'; +import { RecentPagesService } from './services/recent-pages'; + +export * from './entities/quick-search'; +export { QuickSearchService, RecentPagesService }; + +export function configureQuickSearchModule(framework: Framework) { + framework + .scope(WorkspaceScope) + .service(QuickSearchService) + .service(RecentPagesService, [WorkspaceLocalState, DocsService]) + .entity(QuickSearch, [DocsService, WorkspaceService]); +} diff --git a/packages/frontend/core/src/modules/cmdk/services/quick-search.ts b/packages/frontend/core/src/modules/cmdk/services/quick-search.ts new file mode 100644 index 0000000000..98d1da75ac --- /dev/null +++ b/packages/frontend/core/src/modules/cmdk/services/quick-search.ts @@ -0,0 +1,7 @@ +import { Service } from '@toeverything/infra'; + +import { QuickSearch } from '../entities/quick-search'; + +export class QuickSearchService extends Service { + public readonly quickSearch = this.framework.createEntity(QuickSearch); +} diff --git a/packages/frontend/core/src/modules/cmdk/services/recent-pages.ts b/packages/frontend/core/src/modules/cmdk/services/recent-pages.ts new file mode 100644 index 0000000000..27bd9c5362 --- /dev/null +++ b/packages/frontend/core/src/modules/cmdk/services/recent-pages.ts @@ -0,0 +1,43 @@ +import type { + DocRecord, + DocsService, + WorkspaceLocalState, +} from '@toeverything/infra'; +import { Service } from '@toeverything/infra'; + +const RECENT_PAGES_LIMIT = 3; // adjust this? +const RECENT_PAGES_KEY = 'recent-pages'; + +const EMPTY_ARRAY: string[] = []; + +export class RecentPagesService extends Service { + constructor( + private readonly localState: WorkspaceLocalState, + private readonly docsService: DocsService + ) { + super(); + } + + addRecentDoc(pageId: string) { + let recentPages = this.getRecentDocIds(); + recentPages = recentPages.filter(id => id !== pageId); + if (recentPages.length >= RECENT_PAGES_LIMIT) { + recentPages.pop(); + } + recentPages.unshift(pageId); + this.localState.set(RECENT_PAGES_KEY, recentPages); + } + + getRecentDocs() { + const docs = this.docsService.list.docs$.value; + return this.getRecentDocIds() + .map(id => docs.find(doc => doc.id === id)) + .filter((d): d is DocRecord => !!d); + } + + private getRecentDocIds() { + return ( + this.localState.get(RECENT_PAGES_KEY) || EMPTY_ARRAY + ); + } +} diff --git a/packages/frontend/core/src/components/pure/cmdk/__tests__/command.score.spec.ts b/packages/frontend/core/src/modules/cmdk/views/__tests__/command.score.spec.ts similarity index 100% rename from packages/frontend/core/src/components/pure/cmdk/__tests__/command.score.spec.ts rename to packages/frontend/core/src/modules/cmdk/views/__tests__/command.score.spec.ts diff --git a/packages/frontend/core/src/components/pure/cmdk/__tests__/filter-commands.spec.ts b/packages/frontend/core/src/modules/cmdk/views/__tests__/filter-commands.spec.ts similarity index 100% rename from packages/frontend/core/src/components/pure/cmdk/__tests__/filter-commands.spec.ts rename to packages/frontend/core/src/modules/cmdk/views/__tests__/filter-commands.spec.ts diff --git a/packages/frontend/core/src/components/pure/cmdk/__tests__/use-highlight.spec.ts b/packages/frontend/core/src/modules/cmdk/views/__tests__/use-highlight.spec.ts similarity index 100% rename from packages/frontend/core/src/components/pure/cmdk/__tests__/use-highlight.spec.ts rename to packages/frontend/core/src/modules/cmdk/views/__tests__/use-highlight.spec.ts diff --git a/packages/frontend/core/src/components/pure/cmdk/command-score.ts b/packages/frontend/core/src/modules/cmdk/views/command-score.ts similarity index 100% rename from packages/frontend/core/src/components/pure/cmdk/command-score.ts rename to packages/frontend/core/src/modules/cmdk/views/command-score.ts diff --git a/packages/frontend/core/src/components/pure/cmdk/data-hooks.tsx b/packages/frontend/core/src/modules/cmdk/views/data-hooks.tsx similarity index 50% rename from packages/frontend/core/src/components/pure/cmdk/data-hooks.tsx rename to packages/frontend/core/src/modules/cmdk/views/data-hooks.tsx index 1ca9021901..466f060cfa 100644 --- a/packages/frontend/core/src/components/pure/cmdk/data-hooks.tsx +++ b/packages/frontend/core/src/modules/cmdk/views/data-hooks.tsx @@ -7,6 +7,11 @@ import { import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta'; import { useGetDocCollectionPageTitle } from '@affine/core/hooks/use-block-suite-workspace-page-title'; import { useJournalHelper } from '@affine/core/hooks/use-journal'; +import { + QuickSearchService, + RecentPagesService, + type SearchCallbackResult, +} from '@affine/core/modules/cmdk'; import { CollectionService } from '@affine/core/modules/collection'; import { WorkspaceSubPath } from '@affine/core/shared'; import { mixpanel } from '@affine/core/utils'; @@ -20,27 +25,19 @@ import { } from '@blocksuite/icons'; import type { DocRecord, Workspace } from '@toeverything/infra'; import { - DocsService, GlobalContextService, useLiveData, useService, WorkspaceService, } from '@toeverything/infra'; -import { atom, useAtomValue } from 'jotai'; +import { atom } from 'jotai'; import { useCallback, useEffect, useMemo, useState } from 'react'; -import { recentPageIdsBaseAtom } from '../../../atoms'; +import { usePageHelper } from '../../../components/blocksuite/block-suite-page-list/utils'; import { useNavigateHelper } from '../../../hooks/use-navigate-helper'; -import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils'; import { filterSortAndGroupCommands } from './filter-commands'; import type { CMDKCommand, CommandContext } from './types'; -interface SearchResultsValue { - space: string; - content: string; -} - -export const cmdkQueryAtom = atom(''); export const cmdkValueAtom = atom(''); function filterCommandByContext( @@ -75,29 +72,14 @@ function getAllCommand(context: CommandContext) { }); } -const useRecentDocs = () => { - const docs = useLiveData(useService(DocsService).list.docs$); - const recentPageIds = useAtomValue(recentPageIdsBaseAtom); - return useMemo(() => { - return recentPageIds - .map(pageId => { - const page = docs.find(page => page.id === pageId); - return page; - }) - .filter((p): p is DocRecord => !!p); - }, [recentPageIds, docs]); -}; - -export const docToCommand = ( +const docToCommand = ( category: CommandCategory, doc: DocRecord, - navigationHelper: ReturnType, + run: () => void, getPageTitle: ReturnType, isPageJournal: (pageId: string) => boolean, t: ReturnType, - workspace: Workspace, - subTitle?: string, - blockId?: string + subTitle?: string ): CMDKCommand => { const docMode = doc.mode$.value; @@ -107,8 +89,6 @@ export const docToCommand = ( subTitle: subTitle, }; - // hack: when comparing, the part between >>> and <<< will be ignored - // adding this patch so that CMDK will not complain about duplicated commands const id = category + '.' + doc.id; const icon = isPageJournal(doc.id) ? ( @@ -123,33 +103,23 @@ export const docToCommand = ( id, label: commandLabel, category: category, - run: () => { - if (!workspace) { - console.error('current workspace not found'); - return; - } - if (blockId) { - return navigationHelper.jumpToPageBlock(workspace.id, doc.id, blockId); - } - return navigationHelper.jumpToPage(workspace.id, doc.id); - }, + originalValue: title, + run: run, icon: icon, timestamp: doc.meta?.updatedDate, }; }; -export const usePageCommands = () => { - const recentDocs = useRecentDocs(); - const docs = useLiveData(useService(DocsService).list.docs$); +function useSearchedDocCommands( + onSelect: (opts: { docId: string; blockId?: string }) => void +) { + const quickSearch = useService(QuickSearchService).quickSearch; + const recentPages = useService(RecentPagesService); + const query = useLiveData(quickSearch.query$); const workspace = useService(WorkspaceService).workspace; - const pageHelper = usePageHelper(workspace.docCollection); - const pageMetaHelper = useDocMetaHelper(workspace.docCollection); - const query = useAtomValue(cmdkQueryAtom); - const navigationHelper = useNavigateHelper(); - const journalHelper = useJournalHelper(workspace.docCollection); - const t = useAFFiNEI18N(); const getPageTitle = useGetDocCollectionPageTitle(workspace.docCollection); const { isPageJournal } = useJournalHelper(workspace.docCollection); + const t = useAFFiNEI18N(); const [searchTime, setSearchTime] = useState(0); @@ -170,134 +140,229 @@ export const usePageCommands = () => { return useMemo(() => { searchTime; // hack to make the searchTime as a dependency - let results: CMDKCommand[] = []; - if (query.trim() === '') { - results = recentDocs.map(doc => { + if (query.trim().length === 0) { + return recentPages.getRecentDocs().map(doc => { return docToCommand( 'affine:recent', doc, - navigationHelper, + () => onSelect({ docId: doc.id }), getPageTitle, isPageJournal, - t, - workspace + t ); }); } else { - // queried pages that has matched contents - // TODO: we shall have a debounce for global search here - const searchResults = workspace.docCollection.search({ - query, - }) as unknown as Map; - const resultValues = Array.from(searchResults.values()); + return quickSearch + .getSearchedDocs(query) + .map(({ blockId, content, doc, source }) => { + const category = 'affine:pages'; - const reverseMapping: Map = new Map(); - searchResults.forEach((value, key) => { - reverseMapping.set(value.space, key); - }); + const command = docToCommand( + category, + doc, + () => + onSelect({ + docId: doc.id, + blockId, + }), + getPageTitle, + isPageJournal, + t, + content + ); - results = docs.map(doc => { - const category = 'affine:pages'; + if (source === 'link-ref') { + command.alwaysShow = true; + command.originalValue = query; + } - const subTitle = resultValues.find( - result => result.space === doc.id - )?.content; - - const blockId = reverseMapping.get(doc.id); - - const command = docToCommand( - category, - doc, - navigationHelper, - getPageTitle, - isPageJournal, - t, - workspace, - subTitle, - blockId - ); - return command; - }); - - // check if the pages have exact match. if not, we should show the "create page" command - if (results.every(command => command.originalValue !== query)) { - results.push({ - id: 'affine:pages:append-to-journal', - label: t['com.affine.journal.cmdk.append-to-today'](), - alwaysShow: true, - category: 'affine:creation', - run: async () => { - const appendRes = await journalHelper.appendContentToToday(query); - if (!appendRes) return; - const { page, blockId } = appendRes; - navigationHelper.jumpToPageBlock( - page.collection.id, - page.id, - blockId - ); - mixpanel.track('AppendToJournal', { - control: 'cmdk', - }); - }, - icon: , + return command; }); - - results.push({ - id: 'affine:pages:create-page', - label: t['com.affine.cmdk.affine.create-new-page-as']({ - keyWord: query, - }), - alwaysShow: true, - category: 'affine:creation', - run: async () => { - const page = pageHelper.createPage(); - page.load(); - pageMetaHelper.setDocTitle(page.id, query); - mixpanel.track('DocCreated', { - control: 'cmdk', - type: 'doc', - }); - }, - icon: , - }); - - results.push({ - id: 'affine:pages:create-edgeless', - label: t['com.affine.cmdk.affine.create-new-edgeless-as']({ - keyWord: query, - }), - alwaysShow: true, - category: 'affine:creation', - run: async () => { - const page = pageHelper.createEdgeless(); - page.load(); - pageMetaHelper.setDocTitle(page.id, query); - mixpanel.track('DocCreated', { - control: 'cmdk', - type: 'whiteboard', - }); - }, - icon: , - }); - } } - return results; }, [ searchTime, query, - recentDocs, - navigationHelper, + recentPages, getPageTitle, isPageJournal, t, - workspace, - docs, + onSelect, + quickSearch, + ]); +} + +export const usePageCommands = () => { + const quickSearch = useService(QuickSearchService).quickSearch; + const workspace = useService(WorkspaceService).workspace; + const pageHelper = usePageHelper(workspace.docCollection); + const pageMetaHelper = useDocMetaHelper(workspace.docCollection); + const query = useLiveData(quickSearch.query$); + const navigationHelper = useNavigateHelper(); + const journalHelper = useJournalHelper(workspace.docCollection); + const t = useAFFiNEI18N(); + + const onSelectPage = useCallback( + (opts: { docId: string; blockId?: string }) => { + if (!workspace) { + console.error('current workspace not found'); + return; + } + + if (opts.blockId) { + navigationHelper.jumpToPageBlock( + workspace.id, + opts.docId, + opts.blockId + ); + } else { + navigationHelper.jumpToPage(workspace.id, opts.docId); + } + }, + [navigationHelper, workspace] + ); + + const searchedDocsCommands = useSearchedDocCommands(onSelectPage); + + return useMemo(() => { + const results: CMDKCommand[] = [...searchedDocsCommands]; + + // check if the pages have exact match. if not, we should show the "create page" command + if ( + results.every(command => command.originalValue !== query) && + query.trim() + ) { + results.push({ + id: 'affine:pages:append-to-journal', + label: t['com.affine.journal.cmdk.append-to-today'](), + alwaysShow: true, + category: 'affine:creation', + run: async () => { + const appendRes = await journalHelper.appendContentToToday(query); + if (!appendRes) return; + const { page, blockId } = appendRes; + navigationHelper.jumpToPageBlock( + page.collection.id, + page.id, + blockId + ); + mixpanel.track('AppendToJournal', { + control: 'cmdk', + }); + }, + icon: , + }); + + results.push({ + id: 'affine:pages:create-page', + label: t['com.affine.cmdk.affine.create-new-page-as']({ + keyWord: query, + }), + alwaysShow: true, + category: 'affine:creation', + run: async () => { + const page = pageHelper.createPage(); + page.load(); + pageMetaHelper.setDocTitle(page.id, query); + mixpanel.track('DocCreated', { + control: 'cmdk', + type: 'doc', + }); + }, + icon: , + }); + + results.push({ + id: 'affine:pages:create-edgeless', + label: t['com.affine.cmdk.affine.create-new-edgeless-as']({ + keyWord: query, + }), + alwaysShow: true, + category: 'affine:creation', + run: async () => { + const page = pageHelper.createEdgeless(); + page.load(); + pageMetaHelper.setDocTitle(page.id, query); + mixpanel.track('DocCreated', { + control: 'cmdk', + type: 'whiteboard', + }); + }, + icon: , + }); + } + return results; + }, [ + searchedDocsCommands, + t, + query, journalHelper, + navigationHelper, pageHelper, pageMetaHelper, ]); }; +// todo: refactor to reduce duplication with usePageCommands +export const useSearchCallbackCommands = () => { + const quickSearch = useService(QuickSearchService).quickSearch; + const workspace = useService(WorkspaceService).workspace; + const pageHelper = usePageHelper(workspace.docCollection); + const pageMetaHelper = useDocMetaHelper(workspace.docCollection); + const query = useLiveData(quickSearch.query$); + const t = useAFFiNEI18N(); + + const onSelectPage = useCallback( + (searchResult: SearchCallbackResult) => { + if (!workspace) { + console.error('current workspace not found'); + return; + } + quickSearch.setSearchCallbackResult(searchResult); + }, + [quickSearch, workspace] + ); + + const searchedDocsCommands = useSearchedDocCommands(onSelectPage); + + return useMemo(() => { + const results: CMDKCommand[] = [...searchedDocsCommands]; + + // check if the pages have exact match. if not, we should show the "create page" command + if ( + results.every(command => command.originalValue !== query) && + query.trim() + ) { + results.push({ + id: 'affine:pages:create-page', + label: t['com.affine.cmdk.affine.create-new-doc-and-insert']({ + keyWord: query, + }), + alwaysShow: true, + category: 'affine:creation', + run: async () => { + const page = pageHelper.createPage('page', false); + page.load(); + pageMetaHelper.setDocTitle(page.id, query); + mixpanel.track('DocCreated', { + control: 'cmdk', + type: 'doc', + }); + onSelectPage({ docId: page.id }); + }, + icon: , + }); + } + return results; + }, [ + searchedDocsCommands, + query, + t, + pageHelper, + pageMetaHelper, + onSelectPage, + ]); +}; + export const collectionToCommand = ( collection: Collection, navigationHelper: ReturnType, @@ -323,7 +388,8 @@ export const useCollectionsCommands = () => { // todo: considering collections for searching pages const collectionService = useService(CollectionService); const collections = useLiveData(collectionService.collections$); - const query = useAtomValue(cmdkQueryAtom); + const quickSearch = useService(QuickSearchService).quickSearch; + const query = useLiveData(quickSearch.query$); const navigationHelper = useNavigateHelper(); const t = useAFFiNEI18N(); const workspace = useService(WorkspaceService).workspace; @@ -365,7 +431,8 @@ export const useCMDKCommandGroups = () => { docMode: currentDocMode, }); }, [currentDocMode]); - const query = useAtomValue(cmdkQueryAtom).trim(); + const quickSearch = useService(QuickSearchService).quickSearch; + const query = useLiveData(quickSearch.query$).trim(); return useMemo(() => { const commands = [ @@ -376,3 +443,15 @@ export const useCMDKCommandGroups = () => { return filterSortAndGroupCommands(commands, query); }, [affineCommands, collectionCommands, pageCommands, query]); }; + +export const useSearchCallbackCommandGroups = () => { + const searchCallbackCommands = useSearchCallbackCommands(); + + const quickSearch = useService(QuickSearchService).quickSearch; + const query = useLiveData(quickSearch.query$).trim(); + + return useMemo(() => { + const commands = [...searchCallbackCommands]; + return filterSortAndGroupCommands(commands, query); + }, [searchCallbackCommands, query]); +}; diff --git a/packages/frontend/core/src/components/pure/cmdk/filter-commands.ts b/packages/frontend/core/src/modules/cmdk/views/filter-commands.ts similarity index 100% rename from packages/frontend/core/src/components/pure/cmdk/filter-commands.ts rename to packages/frontend/core/src/modules/cmdk/views/filter-commands.ts diff --git a/packages/frontend/core/src/components/pure/cmdk/highlight.css.ts b/packages/frontend/core/src/modules/cmdk/views/highlight.css.ts similarity index 100% rename from packages/frontend/core/src/components/pure/cmdk/highlight.css.ts rename to packages/frontend/core/src/modules/cmdk/views/highlight.css.ts diff --git a/packages/frontend/core/src/components/pure/cmdk/highlight.tsx b/packages/frontend/core/src/modules/cmdk/views/highlight.tsx similarity index 100% rename from packages/frontend/core/src/components/pure/cmdk/highlight.tsx rename to packages/frontend/core/src/modules/cmdk/views/highlight.tsx diff --git a/packages/frontend/core/src/components/pure/cmdk/index.tsx b/packages/frontend/core/src/modules/cmdk/views/index.tsx similarity index 100% rename from packages/frontend/core/src/components/pure/cmdk/index.tsx rename to packages/frontend/core/src/modules/cmdk/views/index.tsx diff --git a/packages/frontend/core/src/components/pure/cmdk/main.css.ts b/packages/frontend/core/src/modules/cmdk/views/main.css.ts similarity index 96% rename from packages/frontend/core/src/components/pure/cmdk/main.css.ts rename to packages/frontend/core/src/modules/cmdk/views/main.css.ts index 2e67650af7..eec19d0a2e 100644 --- a/packages/frontend/core/src/components/pure/cmdk/main.css.ts +++ b/packages/frontend/core/src/modules/cmdk/views/main.css.ts @@ -15,13 +15,16 @@ export const searchInputContainer = style({ gap: 12, borderBottom: `1px solid ${cssVar('borderColor')}`, flexShrink: 0, - selectors: { - '&.inEditor': { - paddingTop: '12px', - paddingBottom: '18px', - }, - }, }); + +export const hasInputLabel = style([ + searchInputContainer, + { + paddingTop: '12px', + paddingBottom: '18px', + }, +]); + export const searchInput = style({ color: cssVar('textPrimaryColor'), fontSize: cssVar('fontH5'), @@ -109,7 +112,7 @@ globalStyle(`${root} [cmdk-group][hidden]`, { }); globalStyle(`${root} [cmdk-list]`, { maxHeight: 400, - minHeight: 120, + minHeight: 80, overflow: 'auto', overscrollBehavior: 'contain', height: 'min(330px, calc(var(--cmdk-list-height) + 8px))', diff --git a/packages/frontend/core/src/components/pure/cmdk/main.tsx b/packages/frontend/core/src/modules/cmdk/views/main.tsx similarity index 82% rename from packages/frontend/core/src/components/pure/cmdk/main.tsx rename to packages/frontend/core/src/modules/cmdk/views/main.tsx index 95588701f0..0f8449f996 100644 --- a/packages/frontend/core/src/components/pure/cmdk/main.tsx +++ b/packages/frontend/core/src/modules/cmdk/views/main.tsx @@ -3,18 +3,27 @@ import type { CommandCategory } from '@affine/core/commands'; import { formatDate } from '@affine/core/components/page-list'; import { useDocEngineStatus } from '@affine/core/hooks/affine/use-doc-engine-status'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; +import { QuickSearchService } from '@affine/core/modules/cmdk'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import type { DocMeta } from '@blocksuite/store'; +import { useLiveData, useService } from '@toeverything/infra'; import clsx from 'clsx'; import { Command } from 'cmdk'; import { useDebouncedValue } from 'foxact/use-debounced-value'; import { useAtom } from 'jotai'; -import { Suspense, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { + type ReactNode, + Suspense, + useLayoutEffect, + useMemo, + useRef, + useState, +} from 'react'; import { - cmdkQueryAtom, cmdkValueAtom, useCMDKCommandGroups, + useSearchCallbackCommandGroups, } from './data-hooks'; import { HighlightLabel } from './highlight'; import * as styles from './main.css'; @@ -59,18 +68,18 @@ const QuickSearchGroup = ({ }) => { const t = useAFFiNEI18N(); const i18nKey = categoryToI18nKey[category]; - const [query, setQuery] = useAtom(cmdkQueryAtom); + const quickSearch = useService(QuickSearchService).quickSearch; + const query = useLiveData(quickSearch.query$); const onCommendSelect = useAsyncCallback( async (command: CMDKCommand) => { try { await command.run(); } finally { - setQuery(''); onOpenChange?.(false); } }, - [setQuery, onOpenChange] + [onOpenChange] ); return ( @@ -149,20 +158,19 @@ export const CMDKContainer = ({ onQueryChange, query, children, - pageMeta, + inputLabel, open, ...rest }: React.PropsWithChildren<{ open: boolean; className?: string; query: string; - pageMeta?: Partial; + inputLabel?: ReactNode; groups: ReturnType; onQueryChange: (query: string) => void; }>) => { const t = useAFFiNEI18N(); const [value, setValue] = useAtom(cmdkValueAtom); - const isInEditor = pageMeta !== undefined; const [opening, setOpening] = useState(open); const { syncing, progress } = useDocEngineStatus(); const showLoading = useDebouncedValue(syncing, 500); @@ -197,16 +205,14 @@ export const CMDKContainer = ({ loop > {/* todo: add page context here */} - {isInEditor ? ( + {inputLabel ? (
- - {pageMeta.title ? pageMeta.title : t['Untitled']()} - + {inputLabel}
) : null}
{showLoading ? ( @@ -239,20 +245,41 @@ const CMDKQuickSearchModalInner = ({ open, ...props }: CMDKModalProps & { pageMeta?: Partial }) => { - const [query, setQuery] = useAtom(cmdkQueryAtom); - useLayoutEffect(() => { - if (open) { - setQuery(''); - } - }, [open, setQuery]); + const quickSearch = useService(QuickSearchService).quickSearch; + const query = useLiveData(quickSearch.query$); const groups = useCMDKCommandGroups(); + const t = useAFFiNEI18N(); return ( + + + ); +}; + +const CMDKQuickSearchCallbackModalInner = ({ + open, + ...props +}: CMDKModalProps & { pageMeta?: Partial }) => { + const quickSearch = useService(QuickSearchService).quickSearch; + const query = useLiveData(quickSearch.query$); + const groups = useSearchCallbackCommandGroups(); + const t = useAFFiNEI18N(); + return ( + @@ -265,10 +292,17 @@ export const CMDKQuickSearchModal = ({ open, ...props }: CMDKModalProps & { pageMeta?: Partial }) => { + const quickSearch = useService(QuickSearchService).quickSearch; + const mode = useLiveData(quickSearch.mode$); + const InnerComp = + mode === 'commands' + ? CMDKQuickSearchModalInner + : CMDKQuickSearchCallbackModalInner; + return ( }> - { - const query = useAtomValue(cmdkQueryAtom); - // hack: we know that the filtered count is 3 when there is no result (create page & edgeless & append to journal) + const quickSearch = useService(QuickSearchService).quickSearch; + const query = useLiveData(quickSearch.query$); + const mode = useLiveData(quickSearch.mode$); + // hack: we know that the filtered count is 3 when there is no result (create page & edgeless & append to journal, for mode === 'cmdk') const renderNoResult = - useCommandState(state => state.filtered.count === 3) || false; + useCommandState(state => state.filtered.count === 3) && mode === 'commands'; + + const t = useAFFiNEI18N(); if (!renderNoResult) { return null; @@ -24,7 +29,9 @@ export const NotFoundGroup = () => {
-
No results found
+
+ {t['com.affine.cmdk.no-results']()} +
); diff --git a/packages/frontend/core/src/components/pure/cmdk/types.ts b/packages/frontend/core/src/modules/cmdk/views/types.ts similarity index 100% rename from packages/frontend/core/src/components/pure/cmdk/types.ts rename to packages/frontend/core/src/modules/cmdk/views/types.ts diff --git a/packages/frontend/core/src/components/pure/cmdk/use-highlight.ts b/packages/frontend/core/src/modules/cmdk/views/use-highlight.ts similarity index 100% rename from packages/frontend/core/src/components/pure/cmdk/use-highlight.ts rename to packages/frontend/core/src/modules/cmdk/views/use-highlight.ts diff --git a/packages/frontend/core/src/modules/index.ts b/packages/frontend/core/src/modules/index.ts index 746c04c757..79f25736ff 100644 --- a/packages/frontend/core/src/modules/index.ts +++ b/packages/frontend/core/src/modules/index.ts @@ -2,6 +2,7 @@ import { configureQuotaModule } from '@affine/core/modules/quota'; import { configureInfraModules, type Framework } from '@toeverything/infra'; import { configureCloudModule } from './cloud'; +import { configureQuickSearchModule } from './cmdk'; import { configureCollectionModule } from './collection'; import { configureFindInPageModule } from './find-in-page'; import { configureNavigationModule } from './navigation'; @@ -30,6 +31,7 @@ export function configureCommonModules(framework: Framework) { configureTelemetryModule(framework); configureFindInPageModule(framework); configurePeekViewModule(framework); + configureQuickSearchModule(framework); } export function configureImpls(framework: Framework) { diff --git a/packages/frontend/core/src/modules/navigation/__tests__/utils.spec.ts b/packages/frontend/core/src/modules/navigation/__tests__/utils.spec.ts new file mode 100644 index 0000000000..c8459202a5 --- /dev/null +++ b/packages/frontend/core/src/modules/navigation/__tests__/utils.spec.ts @@ -0,0 +1,67 @@ +import { afterEach } from 'node:test'; + +import { beforeEach, expect, test, vi } from 'vitest'; + +import { resolveLinkToDoc } from '../utils'; + +function defineTest( + input: string, + expected: ReturnType +) { + test(`resolveLinkToDoc(${input})`, () => { + const result = resolveLinkToDoc(input); + expect(result).toEqual(expected); + }); +} + +beforeEach(() => { + vi.stubGlobal('location', { origin: 'http://affine.pro' }); +}); + +afterEach(() => { + vi.restoreAllMocks(); +}); + +const testCases: [string, ReturnType][] = [ + ['http://example.com/', null], + [ + '/workspace/48__RTCSwASvWZxyAk3Jw/-Uge-K6SYcAbcNYfQ5U-j#xxxx', + { + workspaceId: '48__RTCSwASvWZxyAk3Jw', + docId: '-Uge-K6SYcAbcNYfQ5U-j', + blockId: 'xxxx', + }, + ], + [ + 'http://affine.pro/workspace/48__RTCSwASvWZxyAk3Jw/-Uge-K6SYcAbcNYfQ5U-j#xxxx', + { + workspaceId: '48__RTCSwASvWZxyAk3Jw', + docId: '-Uge-K6SYcAbcNYfQ5U-j', + blockId: 'xxxx', + }, + ], + ['http://affine.pro/workspace/48__RTCSwASvWZxyAk3Jw/all', null], + ['http://affine.pro/workspace/48__RTCSwASvWZxyAk3Jw/collection', null], + ['http://affine.pro/workspace/48__RTCSwASvWZxyAk3Jw/tag', null], + ['http://affine.pro/workspace/48__RTCSwASvWZxyAk3Jw/trash', null], + [ + 'file//./workspace/48__RTCSwASvWZxyAk3Jw/-Uge-K6SYcAbcNYfQ5U-j#xxxx', + { + workspaceId: '48__RTCSwASvWZxyAk3Jw', + docId: '-Uge-K6SYcAbcNYfQ5U-j', + blockId: 'xxxx', + }, + ], + [ + 'http//localhost:8000/workspace/48__RTCSwASvWZxyAk3Jw/-Uge-K6SYcAbcNYfQ5U-j#xxxx', + { + workspaceId: '48__RTCSwASvWZxyAk3Jw', + docId: '-Uge-K6SYcAbcNYfQ5U-j', + blockId: 'xxxx', + }, + ], +]; + +for (const [input, expected] of testCases) { + defineTest(input, expected); +} diff --git a/packages/frontend/core/src/modules/navigation/index.ts b/packages/frontend/core/src/modules/navigation/index.ts index 380a3139ac..a67197d7c5 100644 --- a/packages/frontend/core/src/modules/navigation/index.ts +++ b/packages/frontend/core/src/modules/navigation/index.ts @@ -1,4 +1,5 @@ export { Navigator } from './entities/navigator'; +export { resolveLinkToDoc } from './utils'; export { NavigationButtons } from './view/navigation-buttons'; import { type Framework, WorkspaceScope } from '@toeverything/infra'; diff --git a/packages/frontend/core/src/modules/navigation/utils.ts b/packages/frontend/core/src/modules/navigation/utils.ts new file mode 100644 index 0000000000..2ba75a0cea --- /dev/null +++ b/packages/frontend/core/src/modules/navigation/utils.ts @@ -0,0 +1,40 @@ +function maybeAffineOrigin(origin: string) { + return ( + origin.startsWith('file://.') || + origin.startsWith('affine://') || + origin.endsWith('affine.pro') || // stable/beta + origin.endsWith('affine.fail') || // canary + origin.includes('localhost') // dev + ); +} + +export const resolveLinkToDoc = (href: string) => { + try { + const url = new URL(href, location.origin); + + // check if origin is one of affine's origins + + if (!maybeAffineOrigin(url.origin)) { + return null; + } + + // http://xxx/workspace/48__RTCSwASvWZxyAk3Jw/-Uge-K6SYcAbcNYfQ5U-j#xxxx + // to { workspaceId: '48__RTCSwASvWZxyAk3Jw', docId: '-Uge-K6SYcAbcNYfQ5U-j', blockId: 'xxxx' } + + const [_, workspaceId, docId, blockId] = + url.toString().match(/\/workspace\/([^/]+)\/([^#]+)(?:#(.+))?/) || []; + + /** + * @see /packages/frontend/core/src/router.tsx + */ + const excludedPaths = ['all', 'collection', 'tag', 'trash']; + + if (!docId || excludedPaths.includes(docId)) { + return null; + } + + return { workspaceId, docId, blockId }; + } catch { + return null; + } +}; diff --git a/packages/frontend/core/src/modules/peek-view/entities/peek-view.ts b/packages/frontend/core/src/modules/peek-view/entities/peek-view.ts index 8839ffef49..6e4a46db1a 100644 --- a/packages/frontend/core/src/modules/peek-view/entities/peek-view.ts +++ b/packages/frontend/core/src/modules/peek-view/entities/peek-view.ts @@ -29,6 +29,8 @@ export type ActivePeekView = { import type { BlockModel } from '@blocksuite/store'; +import { resolveLinkToDoc } from '../../navigation'; + const EMBED_DOC_FLAVOURS = [ 'affine:embed-linked-doc', 'affine:embed-synced-doc', @@ -46,25 +48,6 @@ const isSurfaceRefModel = ( return blockModel.flavour === 'affine:surface-ref'; }; -const resolveLinkToDoc = (href: string) => { - // http://xxx/workspace/48__RTCSwASvWZxyAk3Jw/-Uge-K6SYcAbcNYfQ5U-j#xxxx - // to { workspaceId: '48__RTCSwASvWZxyAk3Jw', docId: '-Uge-K6SYcAbcNYfQ5U-j', blockId: 'xxxx' } - - const [_, workspaceId, docId, blockId] = - href.match(/\/workspace\/([^/]+)\/([^#]+)(?:#(.+))?/) || []; - - /** - * @see /packages/frontend/core/src/router.tsx - */ - const excludedPaths = ['all', 'collection', 'tag', 'trash']; - - if (!docId || excludedPaths.includes(docId)) { - return null; - } - - return { workspaceId, docId, blockId }; -}; - function resolvePeekInfoFromPeekTarget( peekTarget?: PeekViewTarget ): DocPeekViewInfo | null { diff --git a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx index 9220712c1a..47935e34c6 100644 --- a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx +++ b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx @@ -2,6 +2,7 @@ import { Scrollable } from '@affine/component'; import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton'; import { PageAIOnboarding } from '@affine/core/components/affine/ai-onboarding'; import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper'; +import { RecentPagesService } from '@affine/core/modules/cmdk'; import type { PageRootService } from '@blocksuite/blocks'; import { BookmarkBlockService, @@ -28,13 +29,11 @@ import { WorkspaceService, } from '@toeverything/infra'; import clsx from 'clsx'; -import { useSetAtom } from 'jotai'; import type { ReactElement } from 'react'; import { memo, useCallback, useEffect, useLayoutEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; import type { Map as YMap } from 'yjs'; -import { recentPageIdsBaseAtom } from '../../../atoms'; import { AffineErrorBoundary } from '../../../components/affine/affine-error-boundary'; import { GlobalPageHistoryModal } from '../../../components/affine/page-history-modal'; import { ImagePreviewModal } from '../../../components/image-preview'; @@ -369,19 +368,16 @@ export const Component = () => { performanceRenderLogger.info('DetailPage'); const params = useParams(); - const setRecentPageIds = useSetAtom(recentPageIdsBaseAtom); + const recentPages = useService(RecentPagesService); useEffect(() => { if (params.pageId) { const pageId = params.pageId; localStorage.setItem('last_page_id', pageId); - setRecentPageIds(ids => { - // pick 3 recent page ids - return [...new Set([pageId, ...ids]).values()].slice(0, 3); - }); + recentPages.addRecentDoc(pageId); } - }, [params, setRecentPageIds]); + }, [params, recentPages]); const pageId = params.pageId; diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 8d06757674..f853062957 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -555,6 +555,7 @@ "com.affine.cmdk.affine.contact-us": "Contact Us", "com.affine.cmdk.affine.create-new-edgeless-as": "New \"{{keyWord}}\" Edgeless", "com.affine.cmdk.affine.create-new-page-as": "New \"{{keyWord}}\" Page", + "com.affine.cmdk.affine.create-new-doc-and-insert": "Create \"{{keyWord}}\" Doc and insert", "com.affine.cmdk.affine.display-language.to": "Change Display Language to", "com.affine.cmdk.affine.editor.add-to-favourites": "Add to Favourites", "com.affine.cmdk.affine.editor.edgeless.presentation-start": "Start Presentation", @@ -585,6 +586,9 @@ "com.affine.cmdk.affine.translucent-ui-on-the-sidebar.to": "Change Translucent UI On The Sidebar to", "com.affine.cmdk.affine.whats-new": "What's New", "com.affine.cmdk.placeholder": "Type a command or search anything...", + "com.affine.cmdk.no-results": "No results found", + "com.affine.cmdk.no-results-for": "No results found for", + "com.affine.cmdk.insert-links": "Insert links", "com.affine.collection-bar.action.tooltip.delete": "Delete", "com.affine.collection-bar.action.tooltip.edit": "Edit", "com.affine.collection-bar.action.tooltip.pin": "Pin to Sidebar",