diff --git a/packages/common/infra/src/command/command.ts b/packages/common/infra/src/command/command.ts index 130d39b1e5..9455ccb078 100644 --- a/packages/common/infra/src/command/command.ts +++ b/packages/common/infra/src/command/command.ts @@ -25,7 +25,8 @@ export type CommandCategory = | 'affine:layout' | 'affine:updates' | 'affine:help' - | 'affine:general'; + | 'affine:general' + | 'affine:results'; export interface KeybindingOptions { binding: string; diff --git a/packages/frontend/core/src/components/pure/cmdk/__tests__/filter-commands.spec.ts b/packages/frontend/core/src/components/pure/cmdk/__tests__/filter-commands.spec.ts new file mode 100644 index 0000000000..87592dc7c6 --- /dev/null +++ b/packages/frontend/core/src/components/pure/cmdk/__tests__/filter-commands.spec.ts @@ -0,0 +1,121 @@ +/** + * @vitest-environment happy-dom + */ +import { describe, expect, test } from 'vitest'; + +import { filterSortAndGroupCommands } from '../filter-commands'; +import type { CMDKCommand } from '../types'; + +const commands: CMDKCommand[] = ( + [ + { + id: 'affine:goto-all-pages', + category: 'affine:navigation', + label: { title: 'Go to All Pages' }, + }, + { + id: 'affine:goto-page-list', + category: 'affine:navigation', + label: { title: 'Go to Page List' }, + }, + { + id: 'affine:new-page', + category: 'affine:creation', + alwaysShow: true, + label: { title: 'New Page' }, + }, + { + id: 'affine:new-edgeless-page', + category: 'affine:creation', + alwaysShow: true, + label: { title: 'New Edgeless' }, + }, + { + id: 'affine:pages.foo', + category: 'affine:pages', + label: { title: 'New Page', subTitle: 'foo' }, + }, + { + id: 'affine:pages.bar', + category: 'affine:pages', + label: { title: 'New Page', subTitle: 'bar' }, + }, + ] as const +).map(c => { + return { + ...c, + run: () => {}, + }; +}); + +describe('filterSortAndGroupCommands', () => { + function defineTest( + name: string, + query: string, + expected: [string, string[]][] + ) { + test(name, () => { + // Call the function + const result = filterSortAndGroupCommands(commands, query); + const sortedIds = result.map(([category, commands]) => { + return [category, commands.map(command => command.id)]; + }); + + console.log(JSON.stringify(sortedIds)); + + // Assert the result + expect(sortedIds).toEqual(expected); + }); + } + + defineTest('without query', '', [ + ['affine:navigation', ['affine:goto-all-pages', 'affine:goto-page-list']], + ['affine:creation', ['affine:new-page', 'affine:new-edgeless-page']], + ['affine:pages', ['affine:pages.foo', 'affine:pages.bar']], + ]); + + defineTest('with query = a', 'a', [ + [ + 'affine:results', + [ + 'affine:goto-all-pages', + 'affine:pages.foo', + 'affine:pages.bar', + 'affine:new-page', + 'affine:new-edgeless-page', + 'affine:goto-page-list', + ], + ], + ]); + + defineTest('with query = nepa', 'nepa', [ + [ + 'affine:results', + [ + 'affine:pages.foo', + 'affine:pages.bar', + 'affine:new-page', + 'affine:new-edgeless-page', + ], + ], + ]); + + defineTest('with query = new', 'new', [ + [ + 'affine:results', + [ + 'affine:pages.foo', + 'affine:pages.bar', + 'affine:new-page', + 'affine:new-edgeless-page', + ], + ], + ]); + + defineTest('with query = foo', 'foo', [ + [ + 'affine:results', + ['affine:pages.foo', 'affine:new-page', 'affine:new-edgeless-page'], + ], + ]); +}); diff --git a/packages/frontend/core/src/hooks/__tests__/use-highlight.spec.ts b/packages/frontend/core/src/components/pure/cmdk/__tests__/use-highlight.spec.ts similarity index 93% rename from packages/frontend/core/src/hooks/__tests__/use-highlight.spec.ts rename to packages/frontend/core/src/components/pure/cmdk/__tests__/use-highlight.spec.ts index 16ef24584c..f2dca1b6e9 100644 --- a/packages/frontend/core/src/hooks/__tests__/use-highlight.spec.ts +++ b/packages/frontend/core/src/components/pure/cmdk/__tests__/use-highlight.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'vitest'; -import { highlightTextFragments } from '../affine/use-highlight'; +import { highlightTextFragments } from '../use-highlight'; describe('highlightTextFragments', () => { test('should correctly highlight full matches', () => { diff --git a/packages/frontend/core/src/components/pure/cmdk/data.tsx b/packages/frontend/core/src/components/pure/cmdk/data-hooks.tsx similarity index 70% rename from packages/frontend/core/src/components/pure/cmdk/data.tsx rename to packages/frontend/core/src/components/pure/cmdk/data-hooks.tsx index fa305d98e4..745850aa56 100644 --- a/packages/frontend/core/src/components/pure/cmdk/data.tsx +++ b/packages/frontend/core/src/components/pure/cmdk/data-hooks.tsx @@ -1,8 +1,12 @@ +import { currentPageIdAtom } from '@affine/core/atoms/mode'; import { useCollectionManager } from '@affine/core/components/page-list'; import { useBlockSuitePageMeta, usePageMetaHelper, } from '@affine/core/hooks/use-block-suite-page-meta'; +import { useJournalHelper } from '@affine/core/hooks/use-journal'; +import { CollectionService } from '@affine/core/modules/collection'; +import { WorkspaceSubPath } from '@affine/core/shared'; import type { Collection } from '@affine/env/filter'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { @@ -11,8 +15,8 @@ import { TodayIcon, ViewLayersIcon, } from '@blocksuite/icons'; -import type { PageMeta } from '@blocksuite/store'; -import { Workspace } from '@toeverything/infra'; +import { type PageMeta } from '@blocksuite/store'; +import { useService, Workspace } from '@toeverything/infra'; import { getCurrentStore } from '@toeverything/infra/atom'; import { type AffineCommand, @@ -20,19 +24,13 @@ import { type CommandCategory, PreconditionStrategy, } from '@toeverything/infra/command'; -import { useService } from '@toeverything/infra/di'; -import { commandScore } from 'cmdk'; import { atom, useAtomValue } from 'jotai'; -import { groupBy } from 'lodash-es'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { pageSettingsAtom, recentPageIdsBaseAtom } from '../../../atoms'; -import { currentPageIdAtom } from '../../../atoms/mode'; -import { useJournalHelper } from '../../../hooks/use-journal'; import { useNavigateHelper } from '../../../hooks/use-navigate-helper'; -import { CollectionService } from '../../../modules/collection'; -import { WorkspaceSubPath } from '../../../shared'; import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils'; +import { filterSortAndGroupCommands } from './filter-commands'; import type { CMDKCommand, CommandContext } from './types'; interface SearchResultsValue { @@ -40,10 +38,6 @@ interface SearchResultsValue { content: string; } -export function removeDoubleQuotes(str?: string): string | undefined { - return str?.replace(/"/g, ''); -} - export const cmdkQueryAtom = atom(''); export const cmdkValueAtom = atom(''); @@ -98,9 +92,6 @@ const useRecentPages = () => { }, [recentPageIds, pages]); }; -const valueWrapperStart = '__>>>'; -const valueWrapperEnd = '<<<__'; - export const pageToCommand = ( category: CommandCategory, page: PageMeta, @@ -123,21 +114,11 @@ export const pageToCommand = ( // hack: when comparing, the part between >>> and <<< will be ignored // adding this patch so that CMDK will not complain about duplicated commands - const id = CSS.escape( - title + - (label?.subTitle || '') + - valueWrapperStart + - page.id + - '.' + - category + - valueWrapperEnd - ); + const id = category + '.' + page.id; return { id, label: commandLabel, - value: id, - originalValue: title, category: category, run: () => { if (!workspace) { @@ -154,10 +135,6 @@ export const pageToCommand = ( }; }; -const contentMatchedMagicString = '__$$content_matched$$__'; -const contentMatchedWithoutSubtitle = - '__$$content_matched_without_subtitle$$__'; - export const usePageCommands = () => { const recentPages = useRecentPages(); const pages = useWorkspacePages(); @@ -209,13 +186,6 @@ export const usePageCommands = () => { }) as unknown as Map; const resultValues = Array.from(searchResults.values()); - const pageIds = resultValues.map(result => { - if (result.space.startsWith('space:')) { - return result.space.slice(6); - } else { - return result.space; - } - }); const reverseMapping: Map = new Map(); searchResults.forEach((value, key) => { reverseMapping.set(value.space, key); @@ -245,16 +215,6 @@ export const usePageCommands = () => { label, blockId ); - - if (pageIds.includes(page.id)) { - // hack to make the page always showing in the search result - command.value += contentMatchedMagicString; - } - if (!subTitle) { - // hack to make the page title result always before the content result - command.value += contentMatchedWithoutSubtitle; - } - return command; }); @@ -263,7 +223,7 @@ export const usePageCommands = () => { results.push({ id: 'affine:pages:append-to-journal', label: t['com.affine.journal.cmdk.append-to-today'](), - value: 'affine::append-journal' + query, // hack to make the page always showing in the search result + alwaysShow: true, category: 'affine:creation', run: async () => { const appendRes = await journalHelper.appendContentToToday(query); @@ -283,11 +243,11 @@ export const usePageCommands = () => { label: t['com.affine.cmdk.affine.create-new-page-as']({ keyWord: query, }), - value: 'affine::create-page' + query, // hack to make the page always showing in the search result + alwaysShow: true, category: 'affine:creation', run: async () => { const page = pageHelper.createPage(); - await page.waitForLoaded(); + await page.load(); pageMetaHelper.setPageTitle(page.id, query); }, icon: , @@ -298,11 +258,11 @@ export const usePageCommands = () => { label: t['com.affine.cmdk.affine.create-new-edgeless-as']({ keyWord: query, }), - value: 'affine::create-edgeless' + query, // hack to make the page always showing in the search result + alwaysShow: true, category: 'affine:creation', run: async () => { const page = pageHelper.createEdgeless(); - await page.waitForLoaded(); + await page.load(); pageMetaHelper.setPageTitle(page.id, query); }, icon: , @@ -337,16 +297,6 @@ export const collectionToCommand = ( return { id: collection.id, label: label, - // hack: when comparing, the part between >>> and <<< will be ignored - // adding this patch so that CMDK will not complain about duplicated commands - value: - label + - valueWrapperStart + - collection.id + - '.' + - category + - valueWrapperEnd, - originalValue: label, category: category, run: () => { navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.ALL); @@ -401,7 +351,6 @@ export const useCollectionsCommands = () => { export const useCMDKCommandGroups = () => { const pageCommands = usePageCommands(); const collectionCommands = useCollectionsCommands(); - const currentPageId = useAtomValue(currentPageIdAtom); const pageSettings = useAtomValue(pageSettingsAtom); const currentPageMode = currentPageId @@ -412,6 +361,7 @@ export const useCMDKCommandGroups = () => { pageMode: currentPageMode, }); }, [currentPageMode]); + const query = useAtomValue(cmdkQueryAtom).trim(); return useMemo(() => { const commands = [ @@ -419,63 +369,6 @@ export const useCMDKCommandGroups = () => { ...pageCommands, ...affineCommands, ]; - const groups = groupBy(commands, command => command.category); - return Object.entries(groups) as [CommandCategory, CMDKCommand[]][]; - }, [affineCommands, collectionCommands, pageCommands]); + return filterSortAndGroupCommands(commands, query); + }, [affineCommands, collectionCommands, pageCommands, query]); }; - -export const customCommandFilter = (value: string, search: string) => { - // strip off the part between __>>> and <<<__ - let label = value.replace( - new RegExp(valueWrapperStart + '.*' + valueWrapperEnd, 'g'), - '' - ); - - const pageContentMatched = label.includes(contentMatchedMagicString); - if (pageContentMatched) { - label = label.replace(contentMatchedMagicString, ''); - } - const pageTitleMatched = label.includes(contentMatchedWithoutSubtitle); - if (pageTitleMatched) { - label = label.replace(contentMatchedWithoutSubtitle, ''); - } - - // use to remove double quotes from a string until this issue is fixed - // https://github.com/pacocoursey/cmdk/issues/189 - const escapedSearch = removeDoubleQuotes(search) || ''; - const originalScore = commandScore(label, escapedSearch); - - // hack to make the page title result always before the content result - // if the command has matched the title but not the subtitle, - // we should give it a higher score - if (originalScore > 0 && pageTitleMatched) { - return 0.999; - } - // if the command has matched the content but not the label, - // we should give it a higher score, but not too high - if (originalScore < 0.01 && pageContentMatched) { - return 0.3; - } - - return originalScore; -}; - -export const useCommandFilteredStatus = ( - groups: [CommandCategory, CMDKCommand[]][] -) => { - // for each of the groups, show the count of commands that has matched the query - const query = useAtomValue(cmdkQueryAtom); - return useMemo(() => { - return Object.fromEntries( - groups.map(([category, commands]) => { - return [category, getCommandFilteredCount(commands, query)] as const; - }) - ) as Record; - }, [groups, query]); -}; - -function getCommandFilteredCount(commands: CMDKCommand[], query: string) { - return commands.filter(command => { - return command.value && customCommandFilter(command.value, query) > 0; - }).length; -} diff --git a/packages/frontend/core/src/components/pure/cmdk/filter-commands.ts b/packages/frontend/core/src/components/pure/cmdk/filter-commands.ts new file mode 100644 index 0000000000..6a7eb5064b --- /dev/null +++ b/packages/frontend/core/src/components/pure/cmdk/filter-commands.ts @@ -0,0 +1,102 @@ +import type { CommandCategory } from '@toeverything/infra/command'; +import { commandScore } from 'cmdk'; +import { groupBy } from 'lodash-es'; + +import type { CMDKCommand } from './types'; +import { highlightTextFragments } from './use-highlight'; + +export function filterSortAndGroupCommands( + commands: CMDKCommand[], + query: string +): [CommandCategory, CMDKCommand[]][] { + const scoredCommands = commands + .map(command => { + // attach value = id to each command + return { + ...command, + value: command.id.toLowerCase(), // required by cmdk library + score: getCommandScore(command, query), + }; + }) + .filter(c => c.score > 0); + + const sorted = scoredCommands.sort((a, b) => { + return b.score - a.score; + }); + + if (query) { + const onlyCreation = sorted.every( + command => command.category === 'affine:creation' + ); + if (onlyCreation) { + return [['affine:creation', sorted]]; + } else { + return [['affine:results', sorted]]; + } + } else { + const groups = groupBy(sorted, command => command.category); + return Object.entries(groups) as [CommandCategory, CMDKCommand[]][]; + } +} + +const highlightScore = (text: string, search: string) => { + if (text.trim().length === 0) { + return 0; + } + const fragments = highlightTextFragments(text, search); + const highlightedFragment = fragments.filter(fragment => fragment.highlight); + // check the longest highlighted fragment + const longestFragment = Math.max( + 0, + ...highlightedFragment.map(fragment => fragment.text.length) + ); + return longestFragment / search.length; +}; + +const getCategoryWeight = (command: CommandCategory) => { + switch (command) { + case 'affine:recent': + return 1; + case 'affine:pages': + case 'affine:edgeless': + case 'affine:collections': + return 0.8; + case 'affine:creation': + return 0.2; + default: + return 0.5; + } +}; + +const subTitleWeight = 0.8; + +export const getCommandScore = (command: CMDKCommand, search: string) => { + if (search.trim() === '') { + return 1; + } + const title = + (typeof command?.label === 'string' + ? command.label + : command?.label.title) || ''; + const subTitle = + (typeof command?.label === 'string' ? '' : command?.label.subTitle) || ''; + + const catWeight = getCategoryWeight(command.category); + + const zeroComScore = Math.max( + commandScore(title, search), + commandScore(subTitle, search) * subTitleWeight + ); + + // if both title and subtitle has matched, we will use the higher score + const hlScore = Math.max( + highlightScore(title, search), + highlightScore(subTitle, search) * subTitleWeight + ); + + const score = Math.max( + zeroComScore * hlScore * catWeight, + command.alwaysShow ? 0.1 : 0 + ); + return score; +}; diff --git a/packages/frontend/core/src/components/pure/cmdk/highlight.tsx b/packages/frontend/core/src/components/pure/cmdk/highlight.tsx index 359f77972d..ab9cda8487 100644 --- a/packages/frontend/core/src/components/pure/cmdk/highlight.tsx +++ b/packages/frontend/core/src/components/pure/cmdk/highlight.tsx @@ -1,7 +1,7 @@ import { memo } from 'react'; -import { useHighlight } from '../../../hooks/affine/use-highlight'; import * as styles from './highlight.css'; +import { useHighlight } from './use-highlight'; type SearchResultLabel = { title: string; @@ -22,10 +22,7 @@ export const Highlight = memo(function Highlight({ text = '', highlight = '', }: HighlightProps) { - // Use regular expression to replace all line breaks and carriage returns in the text - const cleanedText = text.replace(/\r?\n|\r/g, ''); - - const highlights = useHighlight(cleanedText, highlight.toLowerCase()); + const highlights = useHighlight(text, highlight); return (
diff --git a/packages/frontend/core/src/components/pure/cmdk/main.tsx b/packages/frontend/core/src/components/pure/cmdk/main.tsx index b866843265..228eb0022f 100644 --- a/packages/frontend/core/src/components/pure/cmdk/main.tsx +++ b/packages/frontend/core/src/components/pure/cmdk/main.tsx @@ -4,17 +4,15 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks'; import type { PageMeta } from '@blocksuite/store'; import type { CommandCategory } from '@toeverything/infra/command'; import clsx from 'clsx'; -import { Command, useCommandState } from 'cmdk'; -import { useAtom, useAtomValue } from 'jotai'; +import { Command } from 'cmdk'; +import { useAtom } from 'jotai'; import { Suspense, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { cmdkQueryAtom, cmdkValueAtom, - customCommandFilter, - removeDoubleQuotes, useCMDKCommandGroups, -} from './data'; +} from './data-hooks'; import { HighlightLabel } from './highlight'; import * as styles from './main.css'; import { CMDKModal, type CMDKModalProps } from './modal'; @@ -43,6 +41,7 @@ const categoryToI18nKey: Record = { 'editor:insert-object': 'com.affine.cmdk.affine.category.editor.insert-object', 'editor:page': 'com.affine.cmdk.affine.category.editor.page', + 'affine:results': 'com.affine.cmdk.affine.category.results', }; const QuickSearchGroup = ({ @@ -55,7 +54,7 @@ const QuickSearchGroup = ({ onOpenChange?: (open: boolean) => void; }) => { const t = useAFFiNEI18N(); - const i18nkey = categoryToI18nKey[category]; + const i18nKey = categoryToI18nKey[category]; const [query, setQuery] = useAtom(cmdkQueryAtom); const onCommendSelect = useAsyncCallback( @@ -71,7 +70,7 @@ const QuickSearchGroup = ({ ); return ( - + {commands.map(command => { const label = typeof command.label === 'string' @@ -79,15 +78,11 @@ const QuickSearchGroup = ({ title: command.label, } : command.label; - - // use to remove double quotes from a string until this issue is fixed - // https://github.com/pacocoursey/cmdk/issues/189 - const escapeValue = removeDoubleQuotes(command.value); return ( onCommendSelect(command)} - value={escapeValue} + value={command.value} data-is-danger={ command.id === 'editor:page-move-to-trash' || command.id === 'editor:edgeless-move-to-trash' @@ -97,9 +92,7 @@ const QuickSearchGroup = ({
@@ -126,34 +119,13 @@ const QuickSearchGroup = ({ const QuickSearchCommands = ({ onOpenChange, + groups, }: { onOpenChange?: (open: boolean) => void; + groups: ReturnType; }) => { - const t = useAFFiNEI18N(); - const groups = useCMDKCommandGroups(); - - const query = useAtomValue(cmdkQueryAtom); - const resultCount = useCommandState(state => state.filtered.count); - const resultGroupHeader = useMemo(() => { - if (query) { - return ( -
- { - // hack: use resultCount to determine if it is creation or results - // because the creation(as 2 results) is always shown at the top when there is no result - resultCount === 2 - ? t['com.affine.cmdk.affine.category.affine.creation']() - : t['com.affine.cmdk.affine.category.results']() - } -
- ); - } - return null; - }, [query, resultCount, t]); - return ( <> - {resultGroupHeader} {groups.map(([category, commands]) => { return ( ; onQueryChange: (query: string) => void; }>) => { const t = useAFFiNEI18N(); @@ -190,7 +163,7 @@ export const CMDKContainer = ({ const inputRef = useRef(null); - // fix list height animation on openning + // fix list height animation on opening useLayoutEffect(() => { if (open) { setOpening(true); @@ -206,15 +179,15 @@ export const CMDKContainer = ({ } return; }, [open]); - return ( {/* todo: add page context here */} {isInEditor ? ( @@ -242,7 +215,7 @@ export const CMDKContainer = ({ ); }; -export const CMDKQuickSearchModal = ({ +const CMDKQuickSearchModalInner = ({ pageMeta, open, ...props @@ -253,19 +226,35 @@ export const CMDKQuickSearchModal = ({ setQuery(''); } }, [open, setQuery]); + const groups = useCMDKCommandGroups(); + return ( + + + + ); +}; + +export const CMDKQuickSearchModal = ({ + pageMeta, + open, + ...props +}: CMDKModalProps & { pageMeta?: PageMeta }) => { return ( - - }> - - - + }> + + ); }; diff --git a/packages/frontend/core/src/components/pure/cmdk/not-found.tsx b/packages/frontend/core/src/components/pure/cmdk/not-found.tsx index 0d73a9e2a5..70494ab816 100644 --- a/packages/frontend/core/src/components/pure/cmdk/not-found.tsx +++ b/packages/frontend/core/src/components/pure/cmdk/not-found.tsx @@ -2,7 +2,7 @@ import { SearchIcon } from '@blocksuite/icons'; import { useCommandState } from 'cmdk'; import { useAtomValue } from 'jotai'; -import { cmdkQueryAtom } from './data'; +import { cmdkQueryAtom } from './data-hooks'; import * as styles from './not-found.css'; export const NotFoundGroup = () => { diff --git a/packages/frontend/core/src/components/pure/cmdk/types.ts b/packages/frontend/core/src/components/pure/cmdk/types.ts index dddc96abf9..548873258b 100644 --- a/packages/frontend/core/src/components/pure/cmdk/types.ts +++ b/packages/frontend/core/src/components/pure/cmdk/types.ts @@ -19,6 +19,7 @@ export interface CMDKCommand { category: CommandCategory; keyBinding?: string | { binding: string }; timestamp?: number; + alwaysShow?: boolean; value?: string; // this is used for item filtering originalValue?: string; // some values may be transformed, this is the original value run: (e?: Event) => void | Promise; diff --git a/packages/frontend/core/src/hooks/affine/use-highlight.ts b/packages/frontend/core/src/components/pure/cmdk/use-highlight.ts similarity index 93% rename from packages/frontend/core/src/hooks/affine/use-highlight.ts rename to packages/frontend/core/src/components/pure/cmdk/use-highlight.ts index d807065681..2d72d7b151 100644 --- a/packages/frontend/core/src/hooks/affine/use-highlight.ts +++ b/packages/frontend/core/src/components/pure/cmdk/use-highlight.ts @@ -1,7 +1,8 @@ import { useMemo } from 'react'; function* highlightTextFragmentsGenerator(text: string, query: string) { - const lowerCaseText = text.toLowerCase(); + const lowerCaseText = text.replace(/\r?\n|\r/g, '').toLowerCase(); + query = query.toLowerCase(); let startIndex = lowerCaseText.indexOf(query); if (startIndex !== -1) { diff --git a/packages/frontend/core/src/hooks/affine/use-register-blocksuite-editor-commands.tsx b/packages/frontend/core/src/hooks/affine/use-register-blocksuite-editor-commands.tsx index e9a2002cf5..bc23896854 100644 --- a/packages/frontend/core/src/hooks/affine/use-register-blocksuite-editor-commands.tsx +++ b/packages/frontend/core/src/hooks/affine/use-register-blocksuite-editor-commands.tsx @@ -126,6 +126,7 @@ export function useRegisterBlocksuiteEditorCommands( }) ); + // todo: should not show duplicate for journal unsubs.push( registerAffineCommand({ id: `editor:${mode}-duplicate`, diff --git a/tests/affine-local/e2e/quick-search.spec.ts b/tests/affine-local/e2e/quick-search.spec.ts index c7bc71dacb..be0aa0ba98 100644 --- a/tests/affine-local/e2e/quick-search.spec.ts +++ b/tests/affine-local/e2e/quick-search.spec.ts @@ -24,10 +24,12 @@ const insertInputText = async (page: Page, text: string) => { const keyboardDownAndSelect = async (page: Page, label: string) => { await page.keyboard.press('ArrowDown'); + const selectedEl = page.locator( + '[cmdk-item][data-selected] [data-testid="cmdk-label"]' + ); if ( - (await page - .locator('[cmdk-item][data-selected] [data-testid="cmdk-label"]') - .innerText()) !== label + !(await selectedEl.isVisible()) || + (await selectedEl.innerText()) !== label ) { await keyboardDownAndSelect(page, label); } else { diff --git a/tests/storybook/src/stories/quick-search/quick-search-modal.stories.tsx b/tests/storybook/src/stories/quick-search/quick-search-modal.stories.tsx index 368df7e97e..ec6b2257c4 100644 --- a/tests/storybook/src/stories/quick-search/quick-search-modal.stories.tsx +++ b/tests/storybook/src/stories/quick-search/quick-search-modal.stories.tsx @@ -1,5 +1,6 @@ import { Button } from '@affine/component/ui/button'; import { CMDKContainer, CMDKModal } from '@affine/core/components/pure/cmdk'; +import { useCMDKCommandGroups } from '@affine/core/components/pure/cmdk/data-hooks'; import type { Meta, StoryFn } from '@storybook/react'; import { useState } from 'react'; @@ -27,9 +28,15 @@ export const CMDKModalStory: StoryFn = () => { export const CMDKPanelStory: StoryFn = () => { const [query, setQuery] = useState(''); + const groups = useCMDKCommandGroups(); return ( - + ); };