mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-23 09:22:38 +03:00
feat: add search doc modal (#7136)
This commit is contained in:
parent
de81527e29
commit
1439d00b61
@ -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<CreateWorkspaceMode>(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<AuthAtom>({
|
||||
|
||||
export const openDisableCloudAlertModalAtom = atom(false);
|
||||
|
||||
export const recentPageIdsBaseAtom = atomWithStorage<string[]>(
|
||||
'recentPageSettings',
|
||||
[]
|
||||
);
|
||||
|
||||
export type AllPageFilterOption = 'docs' | 'collections' | 'tags';
|
||||
export const allPageFilterSelectAtom = atom<AllPageFilterOption>('docs');
|
||||
|
||||
|
@ -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<typeof useAFFiNEI18N>;
|
||||
store: ReturnType<typeof createStore>;
|
||||
@ -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: <SettingsIcon />,
|
||||
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(
|
||||
|
@ -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,
|
||||
|
@ -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<string, RootService>;
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -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 () => {
|
||||
|
@ -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(() => {
|
||||
|
@ -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 (
|
||||
<CMDKQuickSearchModal
|
||||
open={openQuickSearchModal}
|
||||
open={open}
|
||||
onOpenChange={onToggleQuickSearch}
|
||||
pageMeta={pageMeta}
|
||||
/>
|
||||
@ -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);
|
||||
|
||||
|
146
packages/frontend/core/src/modules/cmdk/entities/quick-search.ts
Normal file
146
packages/frontend/core/src/modules/cmdk/entities/quick-search.ts
Normal file
@ -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<SearchCallbackResult | null>();
|
||||
|
||||
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<string>();
|
||||
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<typeof res> => !!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;
|
||||
}
|
||||
}
|
22
packages/frontend/core/src/modules/cmdk/index.ts
Normal file
22
packages/frontend/core/src/modules/cmdk/index.ts
Normal file
@ -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]);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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<string[] | null>(RECENT_PAGES_KEY) || EMPTY_ARRAY
|
||||
);
|
||||
}
|
||||
}
|
@ -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<typeof useNavigateHelper>,
|
||||
run: () => void,
|
||||
getPageTitle: ReturnType<typeof useGetDocCollectionPageTitle>,
|
||||
isPageJournal: (pageId: string) => boolean,
|
||||
t: ReturnType<typeof useAFFiNEI18N>,
|
||||
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<number>(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<string, SearchResultsValue>;
|
||||
const resultValues = Array.from(searchResults.values());
|
||||
return quickSearch
|
||||
.getSearchedDocs(query)
|
||||
.map(({ blockId, content, doc, source }) => {
|
||||
const category = 'affine:pages';
|
||||
|
||||
const reverseMapping: Map<string, string> = 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: <TodayIcon />,
|
||||
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: <PageIcon />,
|
||||
});
|
||||
|
||||
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: <EdgelessIcon />,
|
||||
});
|
||||
}
|
||||
}
|
||||
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: <TodayIcon />,
|
||||
});
|
||||
|
||||
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: <PageIcon />,
|
||||
});
|
||||
|
||||
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: <EdgelessIcon />,
|
||||
});
|
||||
}
|
||||
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: <PageIcon />,
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}, [
|
||||
searchedDocsCommands,
|
||||
query,
|
||||
t,
|
||||
pageHelper,
|
||||
pageMetaHelper,
|
||||
onSelectPage,
|
||||
]);
|
||||
};
|
||||
|
||||
export const collectionToCommand = (
|
||||
collection: Collection,
|
||||
navigationHelper: ReturnType<typeof useNavigateHelper>,
|
||||
@ -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]);
|
||||
};
|
@ -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))',
|
@ -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<DocMeta>;
|
||||
inputLabel?: ReactNode;
|
||||
groups: ReturnType<typeof useCMDKCommandGroups>;
|
||||
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 ? (
|
||||
<div className={styles.pageTitleWrapper}>
|
||||
<span className={styles.pageTitle}>
|
||||
{pageMeta.title ? pageMeta.title : t['Untitled']()}
|
||||
</span>
|
||||
<span className={styles.pageTitle}>{inputLabel}</span>
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
className={clsx(className, styles.searchInputContainer, {
|
||||
inEditor: isInEditor,
|
||||
[styles.hasInputLabel]: inputLabel,
|
||||
})}
|
||||
>
|
||||
{showLoading ? (
|
||||
@ -239,20 +245,41 @@ const CMDKQuickSearchModalInner = ({
|
||||
open,
|
||||
...props
|
||||
}: CMDKModalProps & { pageMeta?: Partial<DocMeta> }) => {
|
||||
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 (
|
||||
<CMDKContainer
|
||||
className={styles.root}
|
||||
query={query}
|
||||
groups={groups}
|
||||
onQueryChange={setQuery}
|
||||
pageMeta={pageMeta}
|
||||
onQueryChange={quickSearch.setQuery}
|
||||
inputLabel={
|
||||
pageMeta ? (pageMeta.title ? pageMeta.title : t['Untitled']()) : null
|
||||
}
|
||||
open={open}
|
||||
>
|
||||
<QuickSearchCommands groups={groups} onOpenChange={props.onOpenChange} />
|
||||
</CMDKContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const CMDKQuickSearchCallbackModalInner = ({
|
||||
open,
|
||||
...props
|
||||
}: CMDKModalProps & { pageMeta?: Partial<DocMeta> }) => {
|
||||
const quickSearch = useService(QuickSearchService).quickSearch;
|
||||
const query = useLiveData(quickSearch.query$);
|
||||
const groups = useSearchCallbackCommandGroups();
|
||||
const t = useAFFiNEI18N();
|
||||
return (
|
||||
<CMDKContainer
|
||||
className={styles.root}
|
||||
query={query}
|
||||
groups={groups}
|
||||
onQueryChange={quickSearch.setQuery}
|
||||
inputLabel={t['com.affine.cmdk.insert-links']()}
|
||||
open={open}
|
||||
>
|
||||
<QuickSearchCommands groups={groups} onOpenChange={props.onOpenChange} />
|
||||
@ -265,10 +292,17 @@ export const CMDKQuickSearchModal = ({
|
||||
open,
|
||||
...props
|
||||
}: CMDKModalProps & { pageMeta?: Partial<DocMeta> }) => {
|
||||
const quickSearch = useService(QuickSearchService).quickSearch;
|
||||
const mode = useLiveData(quickSearch.mode$);
|
||||
const InnerComp =
|
||||
mode === 'commands'
|
||||
? CMDKQuickSearchModalInner
|
||||
: CMDKQuickSearchCallbackModalInner;
|
||||
|
||||
return (
|
||||
<CMDKModal open={open} {...props}>
|
||||
<Suspense fallback={<Command.Loading />}>
|
||||
<CMDKQuickSearchModalInner
|
||||
<InnerComp
|
||||
pageMeta={pageMeta}
|
||||
open={open}
|
||||
onOpenChange={props.onOpenChange}
|
@ -1,15 +1,20 @@
|
||||
import { QuickSearchService } from '@affine/core/modules/cmdk';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { SearchIcon } from '@blocksuite/icons';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCommandState } from 'cmdk';
|
||||
import { useAtomValue } from 'jotai';
|
||||
|
||||
import { cmdkQueryAtom } from './data-hooks';
|
||||
import * as styles from './not-found.css';
|
||||
|
||||
export const NotFoundGroup = () => {
|
||||
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 = () => {
|
||||
<div className={styles.notFoundIcon}>
|
||||
<SearchIcon />
|
||||
</div>
|
||||
<div className={styles.notFoundText}>No results found</div>
|
||||
<div className={styles.notFoundText}>
|
||||
{t['com.affine.cmdk.no-results']()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
@ -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) {
|
||||
|
@ -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<typeof resolveLinkToDoc>
|
||||
) {
|
||||
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<typeof resolveLinkToDoc>][] = [
|
||||
['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);
|
||||
}
|
@ -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';
|
||||
|
40
packages/frontend/core/src/modules/navigation/utils.ts
Normal file
40
packages/frontend/core/src/modules/navigation/utils.ts
Normal file
@ -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;
|
||||
}
|
||||
};
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user