Skip to content

Commit

Permalink
refactor(component): cmdk ordering
Browse files Browse the repository at this point in the history
  • Loading branch information
pengx17 committed Jan 26, 2024
1 parent fdffe90 commit a18a5d7
Show file tree
Hide file tree
Showing 12 changed files with 291 additions and 177 deletions.
3 changes: 2 additions & 1 deletion packages/common/infra/src/command/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ export type CommandCategory =
| 'affine:layout'
| 'affine:updates'
| 'affine:help'
| 'affine:general';
| 'affine:general'
| 'affine:results';

export interface KeybindingOptions {
binding: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -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'],
],
]);
});
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,15 @@ import { WorkspaceSubPath } from '@affine/core/shared';
import type { Collection } from '@affine/env/filter';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { EdgelessIcon, PageIcon, ViewLayersIcon } from '@blocksuite/icons';
import type { Page, PageMeta } from '@blocksuite/store';
import { type Page, type PageMeta } from '@blocksuite/store';
import { getCurrentStore } from '@toeverything/infra/atom';
import {
type AffineCommand,
AffineCommandRegistry,
type CommandCategory,
PreconditionStrategy,
} from '@toeverything/infra/command';
import { commandScore } from 'cmdk';
import { atom, useAtomValue } from 'jotai';
import { groupBy } from 'lodash-es';
import { useCallback, useEffect, useMemo, useState } from 'react';

import {
Expand All @@ -33,17 +31,14 @@ import { collectionsCRUDAtom } from '../../../atoms/collections';
import { currentPageIdAtom } from '../../../atoms/mode';
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
import { filterSortAndGroupCommands } from './filter-commands';
import type { CMDKCommand, CommandContext } from './types';

interface SearchResultsValue {
space: string;
content: string;
}

export function removeDoubleQuotes(str?: string): string | undefined {
return str?.replace(/"/g, '');
}

export const cmdkQueryAtom = atom('');
export const cmdkValueAtom = atom('');

Expand All @@ -67,7 +62,7 @@ const safeCurrentPageAtom = atom<Promise<Page | undefined>>(async get => {
}

if (!page.loaded) {
await page.waitForLoaded();
await page.load();
}
return page;
});
Expand Down Expand Up @@ -144,9 +139,6 @@ const useRecentPages = () => {
}, [recentPageIds, pages]);
};

const valueWrapperStart = '__>>>';
const valueWrapperEnd = '<<<__';

export const pageToCommand = (
category: CommandCategory,
page: PageMeta,
Expand All @@ -169,21 +161,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 (!currentWorkspace) {
Expand All @@ -204,10 +186,6 @@ export const pageToCommand = (
};
};

const contentMatchedMagicString = '__$$content_matched$$__';
const contentMatchedWithoutSubtitle =
'__$$content_matched_without_subtitle$$__';

export const usePageCommands = () => {
const recentPages = useRecentPages();
const pages = useWorkspacePages();
Expand Down Expand Up @@ -251,13 +229,6 @@ export const usePageCommands = () => {
}) as unknown as Map<string, SearchResultsValue>;
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<string, string> = new Map();
searchResults.forEach((value, key) => {
reverseMapping.set(value.space, key);
Expand Down Expand Up @@ -286,16 +257,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;
});

Expand All @@ -306,11 +267,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: <PageIcon />,
Expand All @@ -321,11 +282,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: <EdgelessIcon />,
Expand Down Expand Up @@ -360,16 +321,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: () => {
if (!currentWorkspace) {
Expand Down Expand Up @@ -421,70 +372,14 @@ export const useCMDKCommandGroups = () => {
const pageCommands = usePageCommands();
const collectionCommands = useCollectionsCommands();
const affineCommands = useAtomValue(filteredAffineCommands);
const query = useAtomValue(cmdkQueryAtom).trim();

return useMemo(() => {
const commands = [
...collectionCommands,
...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<CommandCategory, number>;
}, [groups, query]);
};

function getCommandFilteredCount(commands: CMDKCommand[], query: string) {
return commands.filter(command => {
return command.value && customCommandFilter(command.value, query) > 0;
}).length;
}
Loading

0 comments on commit a18a5d7

Please sign in to comment.