refactor: move command registry to frontend/core (#7135)

move command registeration related logic out of infra module.
This commit is contained in:
pengx17
2024-06-05 14:09:20 +00:00
parent fa4e4c738a
commit de81527e29
24 changed files with 31 additions and 32 deletions

View File

@@ -1,10 +1,10 @@
import type { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ImportIcon, PlusIcon } from '@blocksuite/icons';
import { registerAffineCommand } from '@toeverything/infra';
import type { createStore } from 'jotai';
import { openCreateWorkspaceModalAtom } from '../atoms';
import type { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils';
import { registerAffineCommand } from './registry';
export function registerAffineCreationCommands({
store,

View File

@@ -1,10 +1,10 @@
import type { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ContactWithUsIcon, NewIcon } from '@blocksuite/icons';
import { registerAffineCommand } from '@toeverything/infra';
import type { createStore } from 'jotai';
import { openSettingModalAtom } from '../atoms';
import { popupWindow } from '../utils';
import { registerAffineCommand } from './registry';
export function registerAffineHelpCommands({
t,

View File

@@ -1,9 +1,9 @@
import type { useAFFiNEI18N } from '@affine/i18n/hooks';
import { SidebarIcon } from '@blocksuite/icons';
import { registerAffineCommand } from '@toeverything/infra';
import type { createStore } from 'jotai';
import { appSidebarOpenAtom } from '../components/app-sidebar';
import { registerAffineCommand } from './registry';
export function registerAffineLayoutCommands({
t,

View File

@@ -2,12 +2,12 @@ import { WorkspaceSubPath } from '@affine/core/shared';
import type { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowRightBigIcon } from '@blocksuite/icons';
import type { DocCollection } from '@blocksuite/store';
import { registerAffineCommand } from '@toeverything/infra';
import type { createStore } from 'jotai';
import { openSettingModalAtom, openWorkspaceListModalAtom } from '../atoms';
import type { useNavigateHelper } from '../hooks/use-navigate-helper';
import { mixpanel } from '../utils/mixpanel';
import { registerAffineCommand } from './registry';
export function registerAffineNavigationCommands({
t,

View File

@@ -1,17 +1,14 @@
import type { useAFFiNEI18N } from '@affine/i18n/hooks';
import { SettingsIcon } from '@blocksuite/icons';
import type { AffineEditorContainer } from '@blocksuite/presets';
import {
appSettingAtom,
PreconditionStrategy,
registerAffineCommand,
} from '@toeverything/infra';
import { appSettingAtom } from '@toeverything/infra';
import type { createStore } from 'jotai';
import type { useTheme } from 'next-themes';
import { openQuickSearchModalAtom } from '../atoms';
import type { useLanguageHelper } from '../hooks/affine/use-language-helper';
import { mixpanel } from '../utils';
import { PreconditionStrategy, registerAffineCommand } from './registry';
export function registerAffineSettingsCommands({
t,

View File

@@ -2,9 +2,10 @@ import { updateReadyAtom } from '@affine/core/hooks/use-app-updater';
import { apis } from '@affine/electron-api';
import type { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ResetIcon } from '@blocksuite/icons';
import { registerAffineCommand } from '@toeverything/infra';
import type { createStore } from 'jotai';
import { registerAffineCommand } from './registry';
export function registerAffineUpdatesCommands({
t,
store,

View File

@@ -4,3 +4,4 @@ export * from './affine-layout';
export * from './affine-navigation';
export * from './affine-settings';
export * from './affine-updates';
export * from './registry';

View File

@@ -0,0 +1,5 @@
# AFFiNE Command Abstractions
This package contains the command abstractions for the AFFiNE framework to be used for CMD-K.
The implementation is highly inspired by the [VSCode Command Abstractions](https://github.com/microsoft/vscode)

View File

@@ -0,0 +1,103 @@
import type { ReactNode } from 'react';
// TODO: need better way for composing different precondition strategies
export enum PreconditionStrategy {
Always,
InPaperOrEdgeless,
InPaper,
InEdgeless,
InEdgelessPresentationMode,
NoSearchResult,
Never,
}
export type CommandCategory =
| 'editor:insert-object'
| 'editor:page'
| 'editor:edgeless'
| 'affine:recent'
| 'affine:pages'
| 'affine:edgeless'
| 'affine:collections'
| 'affine:navigation'
| 'affine:creation'
| 'affine:settings'
| 'affine:layout'
| 'affine:updates'
| 'affine:help'
| 'affine:general'
| 'affine:results';
export interface KeybindingOptions {
binding: string;
// some keybindings are already registered in blocksuite
// we can skip the registration of these keybindings __FOR NOW__
skipRegister?: boolean;
}
export interface AffineCommandOptions {
id: string;
// a set of predefined precondition strategies, but also allow user to customize their own
preconditionStrategy?: PreconditionStrategy | (() => boolean);
// main text on the left..
// make text a function so that we can do i18n and interpolation when we need to
label:
| string
| (() => string)
| {
title: string;
subTitle?: string;
}
| (() => {
title: string;
subTitle?: string;
});
icon: ReactNode; // todo: need a mapping from string -> React element/SVG
category?: CommandCategory;
// we use https://github.com/jamiebuilds/tinykeys so that we can use the same keybinding definition
// for both mac and windows
// todo: render keybinding in command palette
keyBinding?: KeybindingOptions | string;
run: () => void | Promise<void>;
}
export interface AffineCommand {
readonly id: string;
readonly preconditionStrategy: PreconditionStrategy | (() => boolean);
readonly label: {
title: string;
subTitle?: string;
};
readonly icon?: ReactNode; // icon name
readonly category: CommandCategory;
readonly keyBinding?: KeybindingOptions;
run(): void | Promise<void>;
}
export function createAffineCommand(
options: AffineCommandOptions
): AffineCommand {
return {
id: options.id,
run: options.run,
icon: options.icon,
preconditionStrategy:
options.preconditionStrategy ?? PreconditionStrategy.Always,
category: options.category ?? 'affine:general',
get label() {
let label = options.label;
label = typeof label === 'function' ? label?.() : label;
label =
typeof label === 'string'
? {
title: label,
}
: label;
return label;
},
keyBinding:
typeof options.keyBinding === 'string'
? { binding: options.keyBinding }
: options.keyBinding,
};
}

View File

@@ -0,0 +1,2 @@
export * from './command';
export * from './registry';

View File

@@ -0,0 +1,64 @@
import { DebugLogger } from '@affine/debug';
// @ts-expect-error upstream type is wrong
import { tinykeys } from 'tinykeys';
import type { AffineCommand, AffineCommandOptions } from './command';
import { createAffineCommand } from './command';
const commandLogger = new DebugLogger('command:registry');
export const AffineCommandRegistry = new (class {
readonly commands: Map<string, AffineCommand> = new Map();
register(options: AffineCommandOptions) {
if (this.commands.has(options.id)) {
commandLogger.warn(`Command ${options.id} already registered.`);
return () => {};
}
const command = createAffineCommand(options);
this.commands.set(command.id, command);
let unsubKb: (() => void) | undefined;
if (
command.keyBinding &&
!command.keyBinding.skipRegister &&
typeof window !== 'undefined'
) {
const { binding: keybinding } = command.keyBinding;
unsubKb = tinykeys(window, {
[keybinding]: async (e: Event) => {
e.preventDefault();
try {
await command.run();
} catch (e) {
console.error(`Failed to invoke keybinding [${keybinding}]`, e);
}
},
});
}
commandLogger.debug(`Registered command ${command.id}`);
return () => {
unsubKb?.();
this.commands.delete(command.id);
commandLogger.debug(`Unregistered command ${command.id}`);
};
}
get(id: string): AffineCommand | undefined {
if (!this.commands.has(id)) {
commandLogger.warn(`Command ${id} not registered.`);
return undefined;
}
return this.commands.get(id);
}
getAll(): AffineCommand[] {
return Array.from(this.commands.values());
}
})();
export function registerAffineCommand(options: AffineCommandOptions) {
return AffineCommandRegistry.register(options);
}

View File

@@ -1,3 +1,9 @@
import {
type AffineCommand,
AffineCommandRegistry,
type CommandCategory,
PreconditionStrategy,
} from '@affine/core/commands';
import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
import { useGetDocCollectionPageTitle } from '@affine/core/hooks/use-block-suite-workspace-page-title';
import { useJournalHelper } from '@affine/core/hooks/use-journal';
@@ -12,17 +18,10 @@ import {
TodayIcon,
ViewLayersIcon,
} from '@blocksuite/icons';
import type {
AffineCommand,
CommandCategory,
DocRecord,
Workspace,
} from '@toeverything/infra';
import type { DocRecord, Workspace } from '@toeverything/infra';
import {
AffineCommandRegistry,
DocsService,
GlobalContextService,
PreconditionStrategy,
useLiveData,
useService,
WorkspaceService,

View File

@@ -1,4 +1,4 @@
import type { CommandCategory } from '@toeverything/infra';
import type { CommandCategory } from '@affine/core/commands';
import { groupBy } from 'lodash-es';
import { commandScore } from './command-score';

View File

@@ -1,10 +1,10 @@
import { Loading } from '@affine/component/ui/loading';
import type { CommandCategory } from '@affine/core/commands';
import { formatDate } from '@affine/core/components/page-list';
import { useDocEngineStatus } from '@affine/core/hooks/affine/use-doc-engine-status';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { DocMeta } from '@blocksuite/store';
import type { CommandCategory } from '@toeverything/infra';
import clsx from 'clsx';
import { Command } from 'cmdk';
import { useDebouncedValue } from 'foxact/use-debounced-value';

View File

@@ -1,4 +1,5 @@
import type { CommandCategory, DocMode } from '@toeverything/infra';
import type { CommandCategory } from '@affine/core/commands';
import type { DocMode } from '@toeverything/infra';
export interface CommandContext {
docMode: DocMode | undefined;

View File

@@ -1,4 +1,8 @@
import { toast } from '@affine/component';
import {
PreconditionStrategy,
registerAffineCommand,
} from '@affine/core/commands';
import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
import { mixpanel } from '@affine/core/utils';
@@ -8,8 +12,6 @@ import { assertExists } from '@blocksuite/global/utils';
import { EdgelessIcon, HistoryIcon, PageIcon } from '@blocksuite/icons';
import {
DocService,
PreconditionStrategy,
registerAffineCommand,
useLiveData,
useService,
WorkspaceService,

View File

@@ -1,5 +1,5 @@
import { registerAffineCommand } from '@affine/core/commands';
import { useSharingUrl } from '@affine/core/hooks/affine/use-share-url';
import { registerAffineCommand } from '@toeverything/infra';
import { useEffect } from 'react';
export function useRegisterCopyLinkCommands({

View File

@@ -1,5 +1,6 @@
import { registerAffineCommand } from '@affine/core/commands';
import { FindInPageService } from '@affine/core/modules/find-in-page/services/find-in-page';
import { registerAffineCommand, useService } from '@toeverything/infra';
import { useService } from '@toeverything/infra';
import { useCallback, useEffect } from 'react';
export function useRegisterFindInPageCommands() {

View File

@@ -1,8 +1,8 @@
import {
PreconditionStrategy,
registerAffineCommand,
useService,
} from '@toeverything/infra';
} from '@affine/core/commands';
import { useService } from '@toeverything/infra';
import { useEffect } from 'react';
import { NavigatorService } from '../services/navigator';