From d1c4e6141a167d6dd9120508cc61e70547a8ec0c Mon Sep 17 00:00:00 2001 From: Peng Xiao Date: Fri, 16 Feb 2024 13:20:24 +0000 Subject: [PATCH] refactor(component): cmdk ordering (#5722) Replace internal CMDK command filtering/sorting logic. The new implementation includes the following for command scoring: - categories weights - highlighted fragments - original command score value The new logic should be much cleaner and remove some hacks in the original implementation. Not sure if this is optimal yet. Could be changed later. fix https://github.com/toeverything/AFFiNE/issues/5699 --- packages/common/infra/src/command/command.ts | 3 +- .../cmdk/__tests__/filter-commands.spec.ts | 121 +++++++++++++++ .../cmdk}/__tests__/use-highlight.spec.ts | 2 +- .../pure/cmdk/{data.tsx => data-hooks.tsx} | 139 ++---------------- .../components/pure/cmdk/filter-commands.ts | 102 +++++++++++++ .../src/components/pure/cmdk/highlight.tsx | 7 +- .../core/src/components/pure/cmdk/main.tsx | 95 ++++++------ .../src/components/pure/cmdk/not-found.tsx | 2 +- .../core/src/components/pure/cmdk/types.ts | 1 + .../pure/cmdk}/use-highlight.ts | 3 +- ...se-register-blocksuite-editor-commands.tsx | 1 + tests/affine-local/e2e/quick-search.spec.ts | 8 +- .../quick-search-modal.stories.tsx | 9 +- 13 files changed, 304 insertions(+), 189 deletions(-) create mode 100644 packages/frontend/core/src/components/pure/cmdk/__tests__/filter-commands.spec.ts rename packages/frontend/core/src/{hooks => components/pure/cmdk}/__tests__/use-highlight.spec.ts (93%) rename packages/frontend/core/src/components/pure/cmdk/{data.tsx => data-hooks.tsx} (70%) create mode 100644 packages/frontend/core/src/components/pure/cmdk/filter-commands.ts rename packages/frontend/core/src/{hooks/affine => components/pure/cmdk}/use-highlight.ts (93%) 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 ( - + ); };