mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-23 13:01:59 +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 { atom } from 'jotai';
|
||||||
import { atomWithStorage } from 'jotai/utils';
|
|
||||||
|
|
||||||
import type { AuthProps } from '../components/affine/auth';
|
import type { AuthProps } from '../components/affine/auth';
|
||||||
import type { CreateWorkspaceMode } from '../components/affine/create-workspace-modal';
|
import type { CreateWorkspaceMode } from '../components/affine/create-workspace-modal';
|
||||||
@ -7,7 +6,6 @@ import type { SettingProps } from '../components/affine/setting-modal';
|
|||||||
// modal atoms
|
// modal atoms
|
||||||
export const openWorkspacesModalAtom = atom(false);
|
export const openWorkspacesModalAtom = atom(false);
|
||||||
export const openCreateWorkspaceModalAtom = atom<CreateWorkspaceMode>(false);
|
export const openCreateWorkspaceModalAtom = atom<CreateWorkspaceMode>(false);
|
||||||
export const openQuickSearchModalAtom = atom(false);
|
|
||||||
export const openSignOutModalAtom = atom(false);
|
export const openSignOutModalAtom = atom(false);
|
||||||
export const openPaymentDisableAtom = atom(false);
|
export const openPaymentDisableAtom = atom(false);
|
||||||
export const openQuotaModalAtom = atom(false);
|
export const openQuotaModalAtom = atom(false);
|
||||||
@ -46,11 +44,6 @@ export const authAtom = atom<AuthAtom>({
|
|||||||
|
|
||||||
export const openDisableCloudAlertModalAtom = atom(false);
|
export const openDisableCloudAlertModalAtom = atom(false);
|
||||||
|
|
||||||
export const recentPageIdsBaseAtom = atomWithStorage<string[]>(
|
|
||||||
'recentPageSettings',
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
export type AllPageFilterOption = 'docs' | 'collections' | 'tags';
|
export type AllPageFilterOption = 'docs' | 'collections' | 'tags';
|
||||||
export const allPageFilterSelectAtom = atom<AllPageFilterOption>('docs');
|
export const allPageFilterSelectAtom = atom<AllPageFilterOption>('docs');
|
||||||
|
|
||||||
|
@ -5,17 +5,14 @@ import { appSettingAtom } from '@toeverything/infra';
|
|||||||
import type { createStore } from 'jotai';
|
import type { createStore } from 'jotai';
|
||||||
import type { useTheme } from 'next-themes';
|
import type { useTheme } from 'next-themes';
|
||||||
|
|
||||||
import { openQuickSearchModalAtom } from '../atoms';
|
|
||||||
import type { useLanguageHelper } from '../hooks/affine/use-language-helper';
|
import type { useLanguageHelper } from '../hooks/affine/use-language-helper';
|
||||||
import { mixpanel } from '../utils';
|
import { registerAffineCommand } from './registry';
|
||||||
import { PreconditionStrategy, registerAffineCommand } from './registry';
|
|
||||||
|
|
||||||
export function registerAffineSettingsCommands({
|
export function registerAffineSettingsCommands({
|
||||||
t,
|
t,
|
||||||
store,
|
store,
|
||||||
theme,
|
theme,
|
||||||
languageHelper,
|
languageHelper,
|
||||||
editor,
|
|
||||||
}: {
|
}: {
|
||||||
t: ReturnType<typeof useAFFiNEI18N>;
|
t: ReturnType<typeof useAFFiNEI18N>;
|
||||||
store: ReturnType<typeof createStore>;
|
store: ReturnType<typeof createStore>;
|
||||||
@ -25,36 +22,6 @@ export function registerAffineSettingsCommands({
|
|||||||
}) {
|
}) {
|
||||||
const unsubs: Array<() => void> = [];
|
const unsubs: Array<() => void> = [];
|
||||||
const { onLanguageChange, languagesList, currentLanguage } = languageHelper;
|
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
|
// color modes
|
||||||
unsubs.push(
|
unsubs.push(
|
||||||
|
@ -5,6 +5,7 @@ import {
|
|||||||
usePromptModal,
|
usePromptModal,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
|
import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
|
||||||
|
import { QuickSearchService } from '@affine/core/modules/cmdk';
|
||||||
import { PeekViewService } from '@affine/core/modules/peek-view';
|
import { PeekViewService } from '@affine/core/modules/peek-view';
|
||||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
import type { BlockSpec } from '@blocksuite/block-std';
|
import type { BlockSpec } from '@blocksuite/block-std';
|
||||||
@ -33,6 +34,7 @@ import { BlocksuiteEditorJournalDocTitle } from './journal-doc-title';
|
|||||||
import {
|
import {
|
||||||
patchNotificationService,
|
patchNotificationService,
|
||||||
patchPeekViewService,
|
patchPeekViewService,
|
||||||
|
patchQuickSearchService,
|
||||||
patchReferenceRenderer,
|
patchReferenceRenderer,
|
||||||
type ReferenceReactRenderer,
|
type ReferenceReactRenderer,
|
||||||
} from './specs/custom/spec-patchers';
|
} from './specs/custom/spec-patchers';
|
||||||
@ -70,6 +72,7 @@ interface BlocksuiteEditorProps {
|
|||||||
const usePatchSpecs = (page: Doc, specs: BlockSpec[]) => {
|
const usePatchSpecs = (page: Doc, specs: BlockSpec[]) => {
|
||||||
const [reactToLit, portals] = useLitPortalFactory();
|
const [reactToLit, portals] = useLitPortalFactory();
|
||||||
const peekViewService = useService(PeekViewService);
|
const peekViewService = useService(PeekViewService);
|
||||||
|
const quickSearchService = useService(QuickSearchService);
|
||||||
const referenceRenderer: ReferenceReactRenderer = useMemo(() => {
|
const referenceRenderer: ReferenceReactRenderer = useMemo(() => {
|
||||||
return function customReference(reference) {
|
return function customReference(reference) {
|
||||||
const pageId = reference.delta.attributes?.reference?.pageId;
|
const pageId = reference.delta.attributes?.reference?.pageId;
|
||||||
@ -91,12 +94,16 @@ const usePatchSpecs = (page: Doc, specs: BlockSpec[]) => {
|
|||||||
if (!page.readonly && runtimeConfig.enablePeekView) {
|
if (!page.readonly && runtimeConfig.enablePeekView) {
|
||||||
patched = patchPeekViewService(patched, peekViewService);
|
patched = patchPeekViewService(patched, peekViewService);
|
||||||
}
|
}
|
||||||
|
if (!page.readonly) {
|
||||||
|
patched = patchQuickSearchService(patched, quickSearchService);
|
||||||
|
}
|
||||||
return patched;
|
return patched;
|
||||||
}, [
|
}, [
|
||||||
confirmModal,
|
confirmModal,
|
||||||
openPromptModal,
|
openPromptModal,
|
||||||
page.readonly,
|
page.readonly,
|
||||||
peekViewService,
|
peekViewService,
|
||||||
|
quickSearchService,
|
||||||
reactToLit,
|
reactToLit,
|
||||||
referenceRenderer,
|
referenceRenderer,
|
||||||
specs,
|
specs,
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
type useConfirmModal,
|
type useConfirmModal,
|
||||||
type usePromptModal,
|
type usePromptModal,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
|
import type { QuickSearchService } from '@affine/core/modules/cmdk';
|
||||||
import type { PeekViewService } from '@affine/core/modules/peek-view';
|
import type { PeekViewService } from '@affine/core/modules/peek-view';
|
||||||
import type { ActivePeekView } from '@affine/core/modules/peek-view/entities/peek-view';
|
import type { ActivePeekView } from '@affine/core/modules/peek-view/entities/peek-view';
|
||||||
import { DebugLogger } from '@affine/debug';
|
import { DebugLogger } from '@affine/debug';
|
||||||
@ -237,3 +238,37 @@ export function patchPeekViewService(
|
|||||||
|
|
||||||
return specs;
|
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(
|
const createPageAndOpen = useCallback(
|
||||||
(mode?: 'page' | 'edgeless') => {
|
(mode?: 'page' | 'edgeless', open?: boolean) => {
|
||||||
const page = createDoc();
|
const page = createDoc();
|
||||||
initEmptyPage(page);
|
initEmptyPage(page);
|
||||||
docRecordList.doc$(page.id).value?.setMode(mode || 'page');
|
docRecordList.doc$(page.id).value?.setMode(mode || 'page');
|
||||||
openPage(docCollection.id, page.id);
|
if (open !== false) openPage(docCollection.id, page.id);
|
||||||
return page;
|
return page;
|
||||||
},
|
},
|
||||||
[docCollection.id, createDoc, openPage, docRecordList]
|
[docCollection.id, createDoc, openPage, docRecordList]
|
||||||
);
|
);
|
||||||
|
|
||||||
const createEdgelessAndOpen = useCallback(() => {
|
const createEdgelessAndOpen = useCallback(
|
||||||
return createPageAndOpen('edgeless');
|
(open?: boolean) => {
|
||||||
}, [createPageAndOpen]);
|
return createPageAndOpen('edgeless', open);
|
||||||
|
},
|
||||||
|
[createPageAndOpen]
|
||||||
|
);
|
||||||
|
|
||||||
const importFileAndOpen = useMemo(
|
const importFileAndOpen = useMemo(
|
||||||
() => async () => {
|
() => async () => {
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
|
import type { AffineEditorContainer } from '@blocksuite/presets';
|
||||||
import { useService, WorkspaceService } from '@toeverything/infra';
|
import { useService, WorkspaceService } from '@toeverything/infra';
|
||||||
import { useStore } from 'jotai';
|
import { useStore } from 'jotai';
|
||||||
import { useTheme } from 'next-themes';
|
import { useTheme } from 'next-themes';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
PreconditionStrategy,
|
||||||
|
registerAffineCommand,
|
||||||
registerAffineCreationCommands,
|
registerAffineCreationCommands,
|
||||||
registerAffineHelpCommands,
|
registerAffineHelpCommands,
|
||||||
registerAffineLayoutCommands,
|
registerAffineLayoutCommands,
|
||||||
@ -13,10 +16,46 @@ import {
|
|||||||
registerAffineUpdatesCommands,
|
registerAffineUpdatesCommands,
|
||||||
} from '../commands';
|
} from '../commands';
|
||||||
import { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils';
|
import { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils';
|
||||||
|
import { QuickSearchService } from '../modules/cmdk';
|
||||||
import { useLanguageHelper } from './affine/use-language-helper';
|
import { useLanguageHelper } from './affine/use-language-helper';
|
||||||
import { useActiveBlocksuiteEditor } from './use-block-suite-editor';
|
import { useActiveBlocksuiteEditor } from './use-block-suite-editor';
|
||||||
import { useNavigateHelper } from './use-navigate-helper';
|
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() {
|
export function useRegisterWorkspaceCommands() {
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
@ -26,6 +65,15 @@ export function useRegisterWorkspaceCommands() {
|
|||||||
const pageHelper = usePageHelper(currentWorkspace.docCollection);
|
const pageHelper = usePageHelper(currentWorkspace.docCollection);
|
||||||
const navigationHelper = useNavigateHelper();
|
const navigationHelper = useNavigateHelper();
|
||||||
const [editor] = useActiveBlocksuiteEditor();
|
const [editor] = useActiveBlocksuiteEditor();
|
||||||
|
const quickSearch = useService(QuickSearchService);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsub = registerCMDKCommand(quickSearch, editor);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsub();
|
||||||
|
};
|
||||||
|
}, [editor, quickSearch]);
|
||||||
|
|
||||||
// register AffineUpdatesCommands
|
// register AffineUpdatesCommands
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -14,13 +14,13 @@ import {
|
|||||||
useService,
|
useService,
|
||||||
WorkspaceService,
|
WorkspaceService,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
import { useAtomValue, useSetAtom } from 'jotai';
|
||||||
import type { PropsWithChildren, ReactNode } from 'react';
|
import type { PropsWithChildren, ReactNode } from 'react';
|
||||||
import { lazy, useCallback, useEffect, useMemo, useState } from 'react';
|
import { lazy, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { Map as YMap } from 'yjs';
|
import { Map as YMap } from 'yjs';
|
||||||
|
|
||||||
import { openQuickSearchModalAtom, openSettingModalAtom } from '../atoms';
|
import { openSettingModalAtom } from '../atoms';
|
||||||
import { WorkspaceAIOnboarding } from '../components/affine/ai-onboarding';
|
import { WorkspaceAIOnboarding } from '../components/affine/ai-onboarding';
|
||||||
import { AppContainer } from '../components/affine/app-container';
|
import { AppContainer } from '../components/affine/app-container';
|
||||||
import { SyncAwareness } from '../components/affine/awareness';
|
import { SyncAwareness } from '../components/affine/awareness';
|
||||||
@ -38,6 +38,7 @@ import {
|
|||||||
import { useRegisterFindInPageCommands } from '../hooks/affine/use-register-find-in-page-commands';
|
import { useRegisterFindInPageCommands } from '../hooks/affine/use-register-find-in-page-commands';
|
||||||
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||||
import { useRegisterWorkspaceCommands } from '../hooks/use-register-workspace-commands';
|
import { useRegisterWorkspaceCommands } from '../hooks/use-register-workspace-commands';
|
||||||
|
import { QuickSearchService } from '../modules/cmdk';
|
||||||
import { useRegisterNavigationCommands } from '../modules/navigation/view/use-register-navigation-commands';
|
import { useRegisterNavigationCommands } from '../modules/navigation/view/use-register-navigation-commands';
|
||||||
import { WorkbenchService } from '../modules/workbench';
|
import { WorkbenchService } from '../modules/workbench';
|
||||||
import {
|
import {
|
||||||
@ -50,21 +51,25 @@ import { mixpanel } from '../utils';
|
|||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
const CMDKQuickSearchModal = lazy(() =>
|
const CMDKQuickSearchModal = lazy(() =>
|
||||||
import('../components/pure/cmdk').then(module => ({
|
import('../modules/cmdk/views').then(module => ({
|
||||||
default: module.CMDKQuickSearchModal,
|
default: module.CMDKQuickSearchModal,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
export const QuickSearch = () => {
|
export const QuickSearch = () => {
|
||||||
const [openQuickSearchModal, setOpenQuickSearchModalAtom] = useAtom(
|
const quickSearch = useService(QuickSearchService).quickSearch;
|
||||||
openQuickSearchModalAtom
|
const open = useLiveData(quickSearch.show$);
|
||||||
);
|
|
||||||
|
|
||||||
const onToggleQuickSearch = useCallback(
|
const onToggleQuickSearch = useCallback(
|
||||||
(open: boolean) => {
|
(open: boolean) => {
|
||||||
setOpenQuickSearchModalAtom(open);
|
if (open) {
|
||||||
|
// should never be here
|
||||||
|
quickSearch.show();
|
||||||
|
} else {
|
||||||
|
quickSearch.hide();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[setOpenQuickSearchModalAtom]
|
[quickSearch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const docRecordList = useService(DocsService).list;
|
const docRecordList = useService(DocsService).list;
|
||||||
@ -78,7 +83,7 @@ export const QuickSearch = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<CMDKQuickSearchModal
|
<CMDKQuickSearchModal
|
||||||
open={openQuickSearchModal}
|
open={open}
|
||||||
onOpenChange={onToggleQuickSearch}
|
onOpenChange={onToggleQuickSearch}
|
||||||
pageMeta={pageMeta}
|
pageMeta={pageMeta}
|
||||||
/>
|
/>
|
||||||
@ -145,14 +150,14 @@ export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => {
|
|||||||
return pageHelper.createPage();
|
return pageHelper.createPage();
|
||||||
}, [pageHelper]);
|
}, [pageHelper]);
|
||||||
|
|
||||||
const [, setOpenQuickSearchModalAtom] = useAtom(openQuickSearchModalAtom);
|
const quickSearch = useService(QuickSearchService).quickSearch;
|
||||||
const handleOpenQuickSearchModal = useCallback(() => {
|
const handleOpenQuickSearchModal = useCallback(() => {
|
||||||
setOpenQuickSearchModalAtom(true);
|
quickSearch.show();
|
||||||
mixpanel.track('QuickSearchOpened', {
|
mixpanel.track('QuickSearchOpened', {
|
||||||
segment: 'navigation panel',
|
segment: 'navigation panel',
|
||||||
control: 'search button',
|
control: 'search button',
|
||||||
});
|
});
|
||||||
}, [setOpenQuickSearchModalAtom]);
|
}, [quickSearch]);
|
||||||
|
|
||||||
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
|
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 { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||||
import { useGetDocCollectionPageTitle } from '@affine/core/hooks/use-block-suite-workspace-page-title';
|
import { useGetDocCollectionPageTitle } from '@affine/core/hooks/use-block-suite-workspace-page-title';
|
||||||
import { useJournalHelper } from '@affine/core/hooks/use-journal';
|
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 { CollectionService } from '@affine/core/modules/collection';
|
||||||
import { WorkspaceSubPath } from '@affine/core/shared';
|
import { WorkspaceSubPath } from '@affine/core/shared';
|
||||||
import { mixpanel } from '@affine/core/utils';
|
import { mixpanel } from '@affine/core/utils';
|
||||||
@ -20,27 +25,19 @@ import {
|
|||||||
} from '@blocksuite/icons';
|
} from '@blocksuite/icons';
|
||||||
import type { DocRecord, Workspace } from '@toeverything/infra';
|
import type { DocRecord, Workspace } from '@toeverything/infra';
|
||||||
import {
|
import {
|
||||||
DocsService,
|
|
||||||
GlobalContextService,
|
GlobalContextService,
|
||||||
useLiveData,
|
useLiveData,
|
||||||
useService,
|
useService,
|
||||||
WorkspaceService,
|
WorkspaceService,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
import { atom, useAtomValue } from 'jotai';
|
import { atom } from 'jotai';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
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 { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
||||||
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
|
|
||||||
import { filterSortAndGroupCommands } from './filter-commands';
|
import { filterSortAndGroupCommands } from './filter-commands';
|
||||||
import type { CMDKCommand, CommandContext } from './types';
|
import type { CMDKCommand, CommandContext } from './types';
|
||||||
|
|
||||||
interface SearchResultsValue {
|
|
||||||
space: string;
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const cmdkQueryAtom = atom('');
|
|
||||||
export const cmdkValueAtom = atom('');
|
export const cmdkValueAtom = atom('');
|
||||||
|
|
||||||
function filterCommandByContext(
|
function filterCommandByContext(
|
||||||
@ -75,29 +72,14 @@ function getAllCommand(context: CommandContext) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const useRecentDocs = () => {
|
const docToCommand = (
|
||||||
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 = (
|
|
||||||
category: CommandCategory,
|
category: CommandCategory,
|
||||||
doc: DocRecord,
|
doc: DocRecord,
|
||||||
navigationHelper: ReturnType<typeof useNavigateHelper>,
|
run: () => void,
|
||||||
getPageTitle: ReturnType<typeof useGetDocCollectionPageTitle>,
|
getPageTitle: ReturnType<typeof useGetDocCollectionPageTitle>,
|
||||||
isPageJournal: (pageId: string) => boolean,
|
isPageJournal: (pageId: string) => boolean,
|
||||||
t: ReturnType<typeof useAFFiNEI18N>,
|
t: ReturnType<typeof useAFFiNEI18N>,
|
||||||
workspace: Workspace,
|
subTitle?: string
|
||||||
subTitle?: string,
|
|
||||||
blockId?: string
|
|
||||||
): CMDKCommand => {
|
): CMDKCommand => {
|
||||||
const docMode = doc.mode$.value;
|
const docMode = doc.mode$.value;
|
||||||
|
|
||||||
@ -107,8 +89,6 @@ export const docToCommand = (
|
|||||||
subTitle: subTitle,
|
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 id = category + '.' + doc.id;
|
||||||
|
|
||||||
const icon = isPageJournal(doc.id) ? (
|
const icon = isPageJournal(doc.id) ? (
|
||||||
@ -123,33 +103,23 @@ export const docToCommand = (
|
|||||||
id,
|
id,
|
||||||
label: commandLabel,
|
label: commandLabel,
|
||||||
category: category,
|
category: category,
|
||||||
run: () => {
|
originalValue: title,
|
||||||
if (!workspace) {
|
run: run,
|
||||||
console.error('current workspace not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (blockId) {
|
|
||||||
return navigationHelper.jumpToPageBlock(workspace.id, doc.id, blockId);
|
|
||||||
}
|
|
||||||
return navigationHelper.jumpToPage(workspace.id, doc.id);
|
|
||||||
},
|
|
||||||
icon: icon,
|
icon: icon,
|
||||||
timestamp: doc.meta?.updatedDate,
|
timestamp: doc.meta?.updatedDate,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const usePageCommands = () => {
|
function useSearchedDocCommands(
|
||||||
const recentDocs = useRecentDocs();
|
onSelect: (opts: { docId: string; blockId?: string }) => void
|
||||||
const docs = useLiveData(useService(DocsService).list.docs$);
|
) {
|
||||||
|
const quickSearch = useService(QuickSearchService).quickSearch;
|
||||||
|
const recentPages = useService(RecentPagesService);
|
||||||
|
const query = useLiveData(quickSearch.query$);
|
||||||
const workspace = useService(WorkspaceService).workspace;
|
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 getPageTitle = useGetDocCollectionPageTitle(workspace.docCollection);
|
||||||
const { isPageJournal } = useJournalHelper(workspace.docCollection);
|
const { isPageJournal } = useJournalHelper(workspace.docCollection);
|
||||||
|
const t = useAFFiNEI18N();
|
||||||
|
|
||||||
const [searchTime, setSearchTime] = useState<number>(0);
|
const [searchTime, setSearchTime] = useState<number>(0);
|
||||||
|
|
||||||
@ -170,134 +140,229 @@ export const usePageCommands = () => {
|
|||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
searchTime; // hack to make the searchTime as a dependency
|
searchTime; // hack to make the searchTime as a dependency
|
||||||
|
|
||||||
let results: CMDKCommand[] = [];
|
if (query.trim().length === 0) {
|
||||||
if (query.trim() === '') {
|
return recentPages.getRecentDocs().map(doc => {
|
||||||
results = recentDocs.map(doc => {
|
|
||||||
return docToCommand(
|
return docToCommand(
|
||||||
'affine:recent',
|
'affine:recent',
|
||||||
doc,
|
doc,
|
||||||
navigationHelper,
|
() => onSelect({ docId: doc.id }),
|
||||||
getPageTitle,
|
getPageTitle,
|
||||||
isPageJournal,
|
isPageJournal,
|
||||||
t,
|
t
|
||||||
workspace
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// queried pages that has matched contents
|
return quickSearch
|
||||||
// TODO: we shall have a debounce for global search here
|
.getSearchedDocs(query)
|
||||||
const searchResults = workspace.docCollection.search({
|
.map(({ blockId, content, doc, source }) => {
|
||||||
query,
|
const category = 'affine:pages';
|
||||||
}) as unknown as Map<string, SearchResultsValue>;
|
|
||||||
const resultValues = Array.from(searchResults.values());
|
|
||||||
|
|
||||||
const reverseMapping: Map<string, string> = new Map();
|
const command = docToCommand(
|
||||||
searchResults.forEach((value, key) => {
|
category,
|
||||||
reverseMapping.set(value.space, key);
|
doc,
|
||||||
});
|
() =>
|
||||||
|
onSelect({
|
||||||
|
docId: doc.id,
|
||||||
|
blockId,
|
||||||
|
}),
|
||||||
|
getPageTitle,
|
||||||
|
isPageJournal,
|
||||||
|
t,
|
||||||
|
content
|
||||||
|
);
|
||||||
|
|
||||||
results = docs.map(doc => {
|
if (source === 'link-ref') {
|
||||||
const category = 'affine:pages';
|
command.alwaysShow = true;
|
||||||
|
command.originalValue = query;
|
||||||
|
}
|
||||||
|
|
||||||
const subTitle = resultValues.find(
|
return command;
|
||||||
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 />,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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,
|
searchTime,
|
||||||
query,
|
query,
|
||||||
recentDocs,
|
recentPages,
|
||||||
navigationHelper,
|
|
||||||
getPageTitle,
|
getPageTitle,
|
||||||
isPageJournal,
|
isPageJournal,
|
||||||
t,
|
t,
|
||||||
workspace,
|
onSelect,
|
||||||
docs,
|
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,
|
journalHelper,
|
||||||
|
navigationHelper,
|
||||||
pageHelper,
|
pageHelper,
|
||||||
pageMetaHelper,
|
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 = (
|
export const collectionToCommand = (
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
navigationHelper: ReturnType<typeof useNavigateHelper>,
|
navigationHelper: ReturnType<typeof useNavigateHelper>,
|
||||||
@ -323,7 +388,8 @@ export const useCollectionsCommands = () => {
|
|||||||
// todo: considering collections for searching pages
|
// todo: considering collections for searching pages
|
||||||
const collectionService = useService(CollectionService);
|
const collectionService = useService(CollectionService);
|
||||||
const collections = useLiveData(collectionService.collections$);
|
const collections = useLiveData(collectionService.collections$);
|
||||||
const query = useAtomValue(cmdkQueryAtom);
|
const quickSearch = useService(QuickSearchService).quickSearch;
|
||||||
|
const query = useLiveData(quickSearch.query$);
|
||||||
const navigationHelper = useNavigateHelper();
|
const navigationHelper = useNavigateHelper();
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const workspace = useService(WorkspaceService).workspace;
|
const workspace = useService(WorkspaceService).workspace;
|
||||||
@ -365,7 +431,8 @@ export const useCMDKCommandGroups = () => {
|
|||||||
docMode: currentDocMode,
|
docMode: currentDocMode,
|
||||||
});
|
});
|
||||||
}, [currentDocMode]);
|
}, [currentDocMode]);
|
||||||
const query = useAtomValue(cmdkQueryAtom).trim();
|
const quickSearch = useService(QuickSearchService).quickSearch;
|
||||||
|
const query = useLiveData(quickSearch.query$).trim();
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const commands = [
|
const commands = [
|
||||||
@ -376,3 +443,15 @@ export const useCMDKCommandGroups = () => {
|
|||||||
return filterSortAndGroupCommands(commands, query);
|
return filterSortAndGroupCommands(commands, query);
|
||||||
}, [affineCommands, collectionCommands, pageCommands, 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,
|
gap: 12,
|
||||||
borderBottom: `1px solid ${cssVar('borderColor')}`,
|
borderBottom: `1px solid ${cssVar('borderColor')}`,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
selectors: {
|
|
||||||
'&.inEditor': {
|
|
||||||
paddingTop: '12px',
|
|
||||||
paddingBottom: '18px',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const hasInputLabel = style([
|
||||||
|
searchInputContainer,
|
||||||
|
{
|
||||||
|
paddingTop: '12px',
|
||||||
|
paddingBottom: '18px',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
export const searchInput = style({
|
export const searchInput = style({
|
||||||
color: cssVar('textPrimaryColor'),
|
color: cssVar('textPrimaryColor'),
|
||||||
fontSize: cssVar('fontH5'),
|
fontSize: cssVar('fontH5'),
|
||||||
@ -109,7 +112,7 @@ globalStyle(`${root} [cmdk-group][hidden]`, {
|
|||||||
});
|
});
|
||||||
globalStyle(`${root} [cmdk-list]`, {
|
globalStyle(`${root} [cmdk-list]`, {
|
||||||
maxHeight: 400,
|
maxHeight: 400,
|
||||||
minHeight: 120,
|
minHeight: 80,
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
overscrollBehavior: 'contain',
|
overscrollBehavior: 'contain',
|
||||||
height: 'min(330px, calc(var(--cmdk-list-height) + 8px))',
|
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 { formatDate } from '@affine/core/components/page-list';
|
||||||
import { useDocEngineStatus } from '@affine/core/hooks/affine/use-doc-engine-status';
|
import { useDocEngineStatus } from '@affine/core/hooks/affine/use-doc-engine-status';
|
||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
|
import { QuickSearchService } from '@affine/core/modules/cmdk';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import type { DocMeta } from '@blocksuite/store';
|
import type { DocMeta } from '@blocksuite/store';
|
||||||
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { Command } from 'cmdk';
|
import { Command } from 'cmdk';
|
||||||
import { useDebouncedValue } from 'foxact/use-debounced-value';
|
import { useDebouncedValue } from 'foxact/use-debounced-value';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import { Suspense, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
import {
|
||||||
|
type ReactNode,
|
||||||
|
Suspense,
|
||||||
|
useLayoutEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
cmdkQueryAtom,
|
|
||||||
cmdkValueAtom,
|
cmdkValueAtom,
|
||||||
useCMDKCommandGroups,
|
useCMDKCommandGroups,
|
||||||
|
useSearchCallbackCommandGroups,
|
||||||
} from './data-hooks';
|
} from './data-hooks';
|
||||||
import { HighlightLabel } from './highlight';
|
import { HighlightLabel } from './highlight';
|
||||||
import * as styles from './main.css';
|
import * as styles from './main.css';
|
||||||
@ -59,18 +68,18 @@ const QuickSearchGroup = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const i18nKey = categoryToI18nKey[category];
|
const i18nKey = categoryToI18nKey[category];
|
||||||
const [query, setQuery] = useAtom(cmdkQueryAtom);
|
const quickSearch = useService(QuickSearchService).quickSearch;
|
||||||
|
const query = useLiveData(quickSearch.query$);
|
||||||
|
|
||||||
const onCommendSelect = useAsyncCallback(
|
const onCommendSelect = useAsyncCallback(
|
||||||
async (command: CMDKCommand) => {
|
async (command: CMDKCommand) => {
|
||||||
try {
|
try {
|
||||||
await command.run();
|
await command.run();
|
||||||
} finally {
|
} finally {
|
||||||
setQuery('');
|
|
||||||
onOpenChange?.(false);
|
onOpenChange?.(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setQuery, onOpenChange]
|
[onOpenChange]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -149,20 +158,19 @@ export const CMDKContainer = ({
|
|||||||
onQueryChange,
|
onQueryChange,
|
||||||
query,
|
query,
|
||||||
children,
|
children,
|
||||||
pageMeta,
|
inputLabel,
|
||||||
open,
|
open,
|
||||||
...rest
|
...rest
|
||||||
}: React.PropsWithChildren<{
|
}: React.PropsWithChildren<{
|
||||||
open: boolean;
|
open: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
query: string;
|
query: string;
|
||||||
pageMeta?: Partial<DocMeta>;
|
inputLabel?: ReactNode;
|
||||||
groups: ReturnType<typeof useCMDKCommandGroups>;
|
groups: ReturnType<typeof useCMDKCommandGroups>;
|
||||||
onQueryChange: (query: string) => void;
|
onQueryChange: (query: string) => void;
|
||||||
}>) => {
|
}>) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const [value, setValue] = useAtom(cmdkValueAtom);
|
const [value, setValue] = useAtom(cmdkValueAtom);
|
||||||
const isInEditor = pageMeta !== undefined;
|
|
||||||
const [opening, setOpening] = useState(open);
|
const [opening, setOpening] = useState(open);
|
||||||
const { syncing, progress } = useDocEngineStatus();
|
const { syncing, progress } = useDocEngineStatus();
|
||||||
const showLoading = useDebouncedValue(syncing, 500);
|
const showLoading = useDebouncedValue(syncing, 500);
|
||||||
@ -197,16 +205,14 @@ export const CMDKContainer = ({
|
|||||||
loop
|
loop
|
||||||
>
|
>
|
||||||
{/* todo: add page context here */}
|
{/* todo: add page context here */}
|
||||||
{isInEditor ? (
|
{inputLabel ? (
|
||||||
<div className={styles.pageTitleWrapper}>
|
<div className={styles.pageTitleWrapper}>
|
||||||
<span className={styles.pageTitle}>
|
<span className={styles.pageTitle}>{inputLabel}</span>
|
||||||
{pageMeta.title ? pageMeta.title : t['Untitled']()}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div
|
<div
|
||||||
className={clsx(className, styles.searchInputContainer, {
|
className={clsx(className, styles.searchInputContainer, {
|
||||||
inEditor: isInEditor,
|
[styles.hasInputLabel]: inputLabel,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{showLoading ? (
|
{showLoading ? (
|
||||||
@ -239,20 +245,41 @@ const CMDKQuickSearchModalInner = ({
|
|||||||
open,
|
open,
|
||||||
...props
|
...props
|
||||||
}: CMDKModalProps & { pageMeta?: Partial<DocMeta> }) => {
|
}: CMDKModalProps & { pageMeta?: Partial<DocMeta> }) => {
|
||||||
const [query, setQuery] = useAtom(cmdkQueryAtom);
|
const quickSearch = useService(QuickSearchService).quickSearch;
|
||||||
useLayoutEffect(() => {
|
const query = useLiveData(quickSearch.query$);
|
||||||
if (open) {
|
|
||||||
setQuery('');
|
|
||||||
}
|
|
||||||
}, [open, setQuery]);
|
|
||||||
const groups = useCMDKCommandGroups();
|
const groups = useCMDKCommandGroups();
|
||||||
|
const t = useAFFiNEI18N();
|
||||||
return (
|
return (
|
||||||
<CMDKContainer
|
<CMDKContainer
|
||||||
className={styles.root}
|
className={styles.root}
|
||||||
query={query}
|
query={query}
|
||||||
groups={groups}
|
groups={groups}
|
||||||
onQueryChange={setQuery}
|
onQueryChange={quickSearch.setQuery}
|
||||||
pageMeta={pageMeta}
|
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}
|
open={open}
|
||||||
>
|
>
|
||||||
<QuickSearchCommands groups={groups} onOpenChange={props.onOpenChange} />
|
<QuickSearchCommands groups={groups} onOpenChange={props.onOpenChange} />
|
||||||
@ -265,10 +292,17 @@ export const CMDKQuickSearchModal = ({
|
|||||||
open,
|
open,
|
||||||
...props
|
...props
|
||||||
}: CMDKModalProps & { pageMeta?: Partial<DocMeta> }) => {
|
}: CMDKModalProps & { pageMeta?: Partial<DocMeta> }) => {
|
||||||
|
const quickSearch = useService(QuickSearchService).quickSearch;
|
||||||
|
const mode = useLiveData(quickSearch.mode$);
|
||||||
|
const InnerComp =
|
||||||
|
mode === 'commands'
|
||||||
|
? CMDKQuickSearchModalInner
|
||||||
|
: CMDKQuickSearchCallbackModalInner;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CMDKModal open={open} {...props}>
|
<CMDKModal open={open} {...props}>
|
||||||
<Suspense fallback={<Command.Loading />}>
|
<Suspense fallback={<Command.Loading />}>
|
||||||
<CMDKQuickSearchModalInner
|
<InnerComp
|
||||||
pageMeta={pageMeta}
|
pageMeta={pageMeta}
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={props.onOpenChange}
|
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 { SearchIcon } from '@blocksuite/icons';
|
||||||
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useCommandState } from 'cmdk';
|
import { useCommandState } from 'cmdk';
|
||||||
import { useAtomValue } from 'jotai';
|
|
||||||
|
|
||||||
import { cmdkQueryAtom } from './data-hooks';
|
|
||||||
import * as styles from './not-found.css';
|
import * as styles from './not-found.css';
|
||||||
|
|
||||||
export const NotFoundGroup = () => {
|
export const NotFoundGroup = () => {
|
||||||
const query = useAtomValue(cmdkQueryAtom);
|
const quickSearch = useService(QuickSearchService).quickSearch;
|
||||||
// hack: we know that the filtered count is 3 when there is no result (create page & edgeless & append to journal)
|
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 =
|
const renderNoResult =
|
||||||
useCommandState(state => state.filtered.count === 3) || false;
|
useCommandState(state => state.filtered.count === 3) && mode === 'commands';
|
||||||
|
|
||||||
|
const t = useAFFiNEI18N();
|
||||||
|
|
||||||
if (!renderNoResult) {
|
if (!renderNoResult) {
|
||||||
return null;
|
return null;
|
||||||
@ -24,7 +29,9 @@ export const NotFoundGroup = () => {
|
|||||||
<div className={styles.notFoundIcon}>
|
<div className={styles.notFoundIcon}>
|
||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.notFoundText}>No results found</div>
|
<div className={styles.notFoundText}>
|
||||||
|
{t['com.affine.cmdk.no-results']()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
@ -2,6 +2,7 @@ import { configureQuotaModule } from '@affine/core/modules/quota';
|
|||||||
import { configureInfraModules, type Framework } from '@toeverything/infra';
|
import { configureInfraModules, type Framework } from '@toeverything/infra';
|
||||||
|
|
||||||
import { configureCloudModule } from './cloud';
|
import { configureCloudModule } from './cloud';
|
||||||
|
import { configureQuickSearchModule } from './cmdk';
|
||||||
import { configureCollectionModule } from './collection';
|
import { configureCollectionModule } from './collection';
|
||||||
import { configureFindInPageModule } from './find-in-page';
|
import { configureFindInPageModule } from './find-in-page';
|
||||||
import { configureNavigationModule } from './navigation';
|
import { configureNavigationModule } from './navigation';
|
||||||
@ -30,6 +31,7 @@ export function configureCommonModules(framework: Framework) {
|
|||||||
configureTelemetryModule(framework);
|
configureTelemetryModule(framework);
|
||||||
configureFindInPageModule(framework);
|
configureFindInPageModule(framework);
|
||||||
configurePeekViewModule(framework);
|
configurePeekViewModule(framework);
|
||||||
|
configureQuickSearchModule(framework);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function configureImpls(framework: 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 { Navigator } from './entities/navigator';
|
||||||
|
export { resolveLinkToDoc } from './utils';
|
||||||
export { NavigationButtons } from './view/navigation-buttons';
|
export { NavigationButtons } from './view/navigation-buttons';
|
||||||
|
|
||||||
import { type Framework, WorkspaceScope } from '@toeverything/infra';
|
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 type { BlockModel } from '@blocksuite/store';
|
||||||
|
|
||||||
|
import { resolveLinkToDoc } from '../../navigation';
|
||||||
|
|
||||||
const EMBED_DOC_FLAVOURS = [
|
const EMBED_DOC_FLAVOURS = [
|
||||||
'affine:embed-linked-doc',
|
'affine:embed-linked-doc',
|
||||||
'affine:embed-synced-doc',
|
'affine:embed-synced-doc',
|
||||||
@ -46,25 +48,6 @@ const isSurfaceRefModel = (
|
|||||||
return blockModel.flavour === 'affine:surface-ref';
|
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(
|
function resolvePeekInfoFromPeekTarget(
|
||||||
peekTarget?: PeekViewTarget
|
peekTarget?: PeekViewTarget
|
||||||
): DocPeekViewInfo | null {
|
): DocPeekViewInfo | null {
|
||||||
|
@ -2,6 +2,7 @@ import { Scrollable } from '@affine/component';
|
|||||||
import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton';
|
import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton';
|
||||||
import { PageAIOnboarding } from '@affine/core/components/affine/ai-onboarding';
|
import { PageAIOnboarding } from '@affine/core/components/affine/ai-onboarding';
|
||||||
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
|
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 type { PageRootService } from '@blocksuite/blocks';
|
||||||
import {
|
import {
|
||||||
BookmarkBlockService,
|
BookmarkBlockService,
|
||||||
@ -28,13 +29,11 @@ import {
|
|||||||
WorkspaceService,
|
WorkspaceService,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useSetAtom } from 'jotai';
|
|
||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
import { memo, useCallback, useEffect, useLayoutEffect, useState } from 'react';
|
import { memo, useCallback, useEffect, useLayoutEffect, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import type { Map as YMap } from 'yjs';
|
import type { Map as YMap } from 'yjs';
|
||||||
|
|
||||||
import { recentPageIdsBaseAtom } from '../../../atoms';
|
|
||||||
import { AffineErrorBoundary } from '../../../components/affine/affine-error-boundary';
|
import { AffineErrorBoundary } from '../../../components/affine/affine-error-boundary';
|
||||||
import { GlobalPageHistoryModal } from '../../../components/affine/page-history-modal';
|
import { GlobalPageHistoryModal } from '../../../components/affine/page-history-modal';
|
||||||
import { ImagePreviewModal } from '../../../components/image-preview';
|
import { ImagePreviewModal } from '../../../components/image-preview';
|
||||||
@ -369,19 +368,16 @@ export const Component = () => {
|
|||||||
performanceRenderLogger.info('DetailPage');
|
performanceRenderLogger.info('DetailPage');
|
||||||
|
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const setRecentPageIds = useSetAtom(recentPageIdsBaseAtom);
|
const recentPages = useService(RecentPagesService);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (params.pageId) {
|
if (params.pageId) {
|
||||||
const pageId = params.pageId;
|
const pageId = params.pageId;
|
||||||
localStorage.setItem('last_page_id', pageId);
|
localStorage.setItem('last_page_id', pageId);
|
||||||
|
|
||||||
setRecentPageIds(ids => {
|
recentPages.addRecentDoc(pageId);
|
||||||
// pick 3 recent page ids
|
|
||||||
return [...new Set([pageId, ...ids]).values()].slice(0, 3);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [params, setRecentPageIds]);
|
}, [params, recentPages]);
|
||||||
|
|
||||||
const pageId = params.pageId;
|
const pageId = params.pageId;
|
||||||
|
|
||||||
|
@ -555,6 +555,7 @@
|
|||||||
"com.affine.cmdk.affine.contact-us": "Contact Us",
|
"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-edgeless-as": "New \"{{keyWord}}\" Edgeless",
|
||||||
"com.affine.cmdk.affine.create-new-page-as": "New \"{{keyWord}}\" Page",
|
"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.display-language.to": "Change Display Language to",
|
||||||
"com.affine.cmdk.affine.editor.add-to-favourites": "Add to Favourites",
|
"com.affine.cmdk.affine.editor.add-to-favourites": "Add to Favourites",
|
||||||
"com.affine.cmdk.affine.editor.edgeless.presentation-start": "Start Presentation",
|
"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.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.affine.whats-new": "What's New",
|
||||||
"com.affine.cmdk.placeholder": "Type a command or search anything...",
|
"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.delete": "Delete",
|
||||||
"com.affine.collection-bar.action.tooltip.edit": "Edit",
|
"com.affine.collection-bar.action.tooltip.edit": "Edit",
|
||||||
"com.affine.collection-bar.action.tooltip.pin": "Pin to Sidebar",
|
"com.affine.collection-bar.action.tooltip.pin": "Pin to Sidebar",
|
||||||
|
Loading…
Reference in New Issue
Block a user