mirror of
https://github.com/toeverything/AFFiNE.git
synced 2025-01-03 08:11:34 +03:00
feat: new CMD-K (#4408)
This commit is contained in:
parent
27e4599c94
commit
e0063ebc9b
@ -12,7 +12,8 @@
|
||||
"./app": "./src/app.tsx",
|
||||
"./router": "./src/router.ts",
|
||||
"./bootstrap/setup": "./src/bootstrap/setup.ts",
|
||||
"./bootstrap/register-plugins": "./src/bootstrap/register-plugins.ts"
|
||||
"./bootstrap/register-plugins": "./src/bootstrap/register-plugins.ts",
|
||||
"./components/pure/*": "./src/components/pure/*/index.tsx"
|
||||
},
|
||||
"dependencies": {
|
||||
"@affine-test/fixtures": "workspace:*",
|
||||
@ -41,7 +42,6 @@
|
||||
"@react-hookz/web": "^23.1.0",
|
||||
"@toeverything/components": "^0.0.43",
|
||||
"async-call-rpc": "^6.3.1",
|
||||
"cmdk": "^0.2.0",
|
||||
"css-spring": "^4.1.0",
|
||||
"cssnano": "^6.0.1",
|
||||
"graphql": "^16.8.0",
|
||||
|
@ -9,7 +9,7 @@ import { describe, expect, test } from 'vitest';
|
||||
import {
|
||||
pageSettingFamily,
|
||||
pageSettingsAtom,
|
||||
recentPageSettingsAtom,
|
||||
recentPageIdsBaseAtom,
|
||||
} from '../index';
|
||||
|
||||
describe('page mode atom', () => {
|
||||
@ -26,20 +26,12 @@ describe('page mode atom', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(store.get(recentPageSettingsAtom)).toEqual([
|
||||
{
|
||||
id: 'page0',
|
||||
mode: 'page',
|
||||
},
|
||||
]);
|
||||
expect(store.get(recentPageIdsBaseAtom)).toEqual(['page0']);
|
||||
|
||||
const page1SettingAtom = pageSettingFamily('page1');
|
||||
store.set(page1SettingAtom, {
|
||||
mode: 'edgeless',
|
||||
});
|
||||
expect(store.get(recentPageSettingsAtom)).toEqual([
|
||||
{ id: 'page1', mode: 'edgeless' },
|
||||
{ id: 'page0', mode: 'page' },
|
||||
]);
|
||||
expect(store.get(recentPageIdsBaseAtom)).toEqual(['page1', 'page0']);
|
||||
});
|
||||
});
|
||||
|
@ -43,10 +43,6 @@ type PageLocalSetting = {
|
||||
mode: PageMode;
|
||||
};
|
||||
|
||||
type PartialPageLocalSettingWithPageId = Partial<PageLocalSetting> & {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const pageSettingsBaseAtom = atomWithStorage(
|
||||
'pageSettings',
|
||||
{} as Record<string, PageLocalSetting>
|
||||
@ -55,22 +51,11 @@ const pageSettingsBaseAtom = atomWithStorage(
|
||||
// readonly atom by design
|
||||
export const pageSettingsAtom = atom(get => get(pageSettingsBaseAtom));
|
||||
|
||||
const recentPageSettingsBaseAtom = atomWithStorage<string[]>(
|
||||
export const recentPageIdsBaseAtom = atomWithStorage<string[]>(
|
||||
'recentPageSettings',
|
||||
[]
|
||||
);
|
||||
|
||||
export const recentPageSettingsAtom = atom<PartialPageLocalSettingWithPageId[]>(
|
||||
get => {
|
||||
const recentPageIDs = get(recentPageSettingsBaseAtom);
|
||||
const pageSettings = get(pageSettingsAtom);
|
||||
return recentPageIDs.map(id => ({
|
||||
...pageSettings[id],
|
||||
id,
|
||||
}));
|
||||
}
|
||||
);
|
||||
|
||||
const defaultPageSetting = {
|
||||
mode: 'page',
|
||||
} satisfies PageLocalSetting;
|
||||
@ -85,7 +70,9 @@ export const pageSettingFamily: AtomFamily<
|
||||
...defaultPageSetting,
|
||||
},
|
||||
(get, set, patch) => {
|
||||
set(recentPageSettingsBaseAtom, ids => {
|
||||
// fixme: this does not work when page reload,
|
||||
// since atomWithStorage is async
|
||||
set(recentPageIdsBaseAtom, ids => {
|
||||
// pick 3 recent page ids
|
||||
return [...new Set([pageId, ...ids]).values()].slice(0, 3);
|
||||
});
|
||||
|
64
apps/core/src/commands/affine-creation.tsx
Normal file
64
apps/core/src/commands/affine-creation.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import type { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { PlusIcon } from '@blocksuite/icons';
|
||||
import { registerAffineCommand } from '@toeverything/infra/command';
|
||||
import type { createStore } from 'jotai';
|
||||
|
||||
import { openCreateWorkspaceModalAtom } from '../atoms';
|
||||
import type { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils';
|
||||
|
||||
export function registerAffineCreationCommands({
|
||||
store,
|
||||
pageHelper,
|
||||
t,
|
||||
}: {
|
||||
t: ReturnType<typeof useAFFiNEI18N>;
|
||||
store: ReturnType<typeof createStore>;
|
||||
pageHelper: ReturnType<typeof usePageHelper>;
|
||||
}) {
|
||||
const unsubs: Array<() => void> = [];
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: 'affine:new-page',
|
||||
category: 'affine:creation',
|
||||
label: t['com.affine.cmdk.affine.new-page'],
|
||||
icon: <PlusIcon />,
|
||||
keyBinding: environment.isDesktop
|
||||
? {
|
||||
binding: '$mod+N',
|
||||
skipRegister: true,
|
||||
}
|
||||
: undefined,
|
||||
run() {
|
||||
pageHelper.createPage();
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: 'affine:new-edgeless-page',
|
||||
category: 'affine:creation',
|
||||
icon: <PlusIcon />,
|
||||
label: t['com.affine.cmdk.affine.new-edgeless-page'],
|
||||
run() {
|
||||
pageHelper.createEdgeless();
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: 'affine:new-workspace',
|
||||
category: 'affine:creation',
|
||||
icon: <PlusIcon />,
|
||||
label: t['com.affine.cmdk.affine.new-workspace'],
|
||||
run() {
|
||||
store.set(openCreateWorkspaceModalAtom, 'new');
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubs.forEach(unsub => unsub());
|
||||
};
|
||||
}
|
40
apps/core/src/commands/affine-layout.tsx
Normal file
40
apps/core/src/commands/affine-layout.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { appSidebarOpenAtom } from '@affine/component/app-sidebar';
|
||||
import type { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { SidebarIcon } from '@blocksuite/icons';
|
||||
import { registerAffineCommand } from '@toeverything/infra/command';
|
||||
import type { createStore } from 'jotai';
|
||||
|
||||
export function registerAffineLayoutCommands({
|
||||
t,
|
||||
store,
|
||||
}: {
|
||||
t: ReturnType<typeof useAFFiNEI18N>;
|
||||
store: ReturnType<typeof createStore>;
|
||||
}) {
|
||||
const unsubs: Array<() => void> = [];
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: 'affine:toggle-left-sidebar',
|
||||
category: 'affine:layout',
|
||||
icon: <SidebarIcon />,
|
||||
label: () => {
|
||||
const open = store.get(appSidebarOpenAtom);
|
||||
return t[
|
||||
open
|
||||
? 'com.affine.cmdk.affine.left-sidebar.collapse'
|
||||
: 'com.affine.cmdk.affine.left-sidebar.expand'
|
||||
]();
|
||||
},
|
||||
keyBinding: {
|
||||
binding: '$mod+/',
|
||||
},
|
||||
run() {
|
||||
store.set(appSidebarOpenAtom, v => !v);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubs.forEach(unsub => unsub());
|
||||
};
|
||||
}
|
66
apps/core/src/commands/affine-navigation.tsx
Normal file
66
apps/core/src/commands/affine-navigation.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import type { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ArrowRightBigIcon } from '@blocksuite/icons';
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import { registerAffineCommand } from '@toeverything/infra/command';
|
||||
import type { createStore } from 'jotai';
|
||||
|
||||
import { openSettingModalAtom } from '../atoms';
|
||||
import type { useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||
import { WorkspaceSubPath } from '../shared';
|
||||
|
||||
export function registerAffineNavigationCommands({
|
||||
t,
|
||||
store,
|
||||
workspace,
|
||||
navigationHelper,
|
||||
}: {
|
||||
t: ReturnType<typeof useAFFiNEI18N>;
|
||||
store: ReturnType<typeof createStore>;
|
||||
navigationHelper: ReturnType<typeof useNavigateHelper>;
|
||||
workspace: Workspace;
|
||||
}) {
|
||||
const unsubs: Array<() => void> = [];
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: 'affine:goto-all-pages',
|
||||
category: 'affine:navigation',
|
||||
icon: <ArrowRightBigIcon />,
|
||||
label: () => t['com.affine.cmdk.affine.navigation.goto-all-pages'](),
|
||||
run() {
|
||||
navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: 'affine:open-settings',
|
||||
category: 'affine:navigation',
|
||||
icon: <ArrowRightBigIcon />,
|
||||
label: () => t['com.affine.cmdk.affine.navigation.open-settings'](),
|
||||
run() {
|
||||
store.set(openSettingModalAtom, {
|
||||
activeTab: 'appearance',
|
||||
workspaceId: null,
|
||||
open: true,
|
||||
});
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: 'affine:goto-trash',
|
||||
category: 'affine:navigation',
|
||||
icon: <ArrowRightBigIcon />,
|
||||
label: () => t['com.affine.cmdk.affine.navigation.goto-trash'](),
|
||||
run() {
|
||||
navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.TRASH);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubs.forEach(unsub => unsub());
|
||||
};
|
||||
}
|
100
apps/core/src/commands/affine-settings.tsx
Normal file
100
apps/core/src/commands/affine-settings.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
import { Trans } from '@affine/i18n';
|
||||
import type { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { SettingsIcon } from '@blocksuite/icons';
|
||||
import {
|
||||
PreconditionStrategy,
|
||||
registerAffineCommand,
|
||||
} from '@toeverything/infra/command';
|
||||
import type { createStore } from 'jotai';
|
||||
import type { useTheme } from 'next-themes';
|
||||
|
||||
import { openQuickSearchModalAtom } from '../atoms';
|
||||
|
||||
export function registerAffineSettingsCommands({
|
||||
store,
|
||||
theme,
|
||||
}: {
|
||||
t: ReturnType<typeof useAFFiNEI18N>;
|
||||
store: ReturnType<typeof createStore>;
|
||||
theme: ReturnType<typeof useTheme>;
|
||||
}) {
|
||||
const unsubs: Array<() => void> = [];
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: 'affine:show-quick-search',
|
||||
preconditionStrategy: PreconditionStrategy.Never,
|
||||
category: 'affine:general',
|
||||
keyBinding: {
|
||||
binding: '$mod+K',
|
||||
},
|
||||
icon: <SettingsIcon />,
|
||||
run() {
|
||||
store.set(openQuickSearchModalAtom, true);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// color schemes
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: 'affine:change-color-scheme-to-auto',
|
||||
label: (
|
||||
<Trans
|
||||
i18nKey="com.affine.cmdk.affine.color-scheme.to"
|
||||
values={{ colour: 'Auto' }}
|
||||
>
|
||||
Change Colour Scheme to <strong>colour</strong>
|
||||
</Trans>
|
||||
),
|
||||
category: 'affine:settings',
|
||||
icon: <SettingsIcon />,
|
||||
preconditionStrategy: () => theme.theme !== 'system',
|
||||
run() {
|
||||
theme.setTheme('system');
|
||||
},
|
||||
})
|
||||
);
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: 'affine:change-color-scheme-to-dark',
|
||||
label: (
|
||||
<Trans
|
||||
i18nKey="com.affine.cmdk.affine.color-scheme.to"
|
||||
values={{ colour: 'Dark' }}
|
||||
>
|
||||
Change Colour Scheme to <strong>colour</strong>
|
||||
</Trans>
|
||||
),
|
||||
category: 'affine:settings',
|
||||
icon: <SettingsIcon />,
|
||||
preconditionStrategy: () => theme.theme !== 'dark',
|
||||
run() {
|
||||
theme.setTheme('dark');
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: 'affine:change-color-scheme-to-light',
|
||||
label: (
|
||||
<Trans
|
||||
i18nKey="com.affine.cmdk.affine.color-scheme.to"
|
||||
values={{ colour: 'Light' }}
|
||||
>
|
||||
Change Colour Scheme to <strong>colour</strong>
|
||||
</Trans>
|
||||
),
|
||||
category: 'affine:settings',
|
||||
icon: <SettingsIcon />,
|
||||
preconditionStrategy: () => theme.theme !== 'light',
|
||||
run() {
|
||||
theme.setTheme('light');
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubs.forEach(unsub => unsub());
|
||||
};
|
||||
}
|
3
apps/core/src/commands/index.ts
Normal file
3
apps/core/src/commands/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './affine-creation';
|
||||
export * from './affine-layout';
|
||||
export * from './affine-settings';
|
@ -3,7 +3,7 @@ import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
|
||||
import { initEmptyPage } from '@toeverything/infra/blocksuite';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { pageSettingsAtom, setPageModeAtom } from '../../../atoms';
|
||||
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
||||
@ -57,10 +57,17 @@ export const usePageHelper = (blockSuiteWorkspace: BlockSuiteWorkspace) => {
|
||||
};
|
||||
showImportModal({ workspace: blockSuiteWorkspace, onSuccess });
|
||||
}, [blockSuiteWorkspace, openPage, jumpToSubPath]);
|
||||
return useMemo(() => {
|
||||
return {
|
||||
createPage: createPageAndOpen,
|
||||
createEdgeless: createEdgelessAndOpen,
|
||||
importFile: importFileAndOpen,
|
||||
isPreferredEdgeless: isPreferredEdgeless,
|
||||
};
|
||||
}, [
|
||||
createEdgelessAndOpen,
|
||||
createPageAndOpen,
|
||||
importFileAndOpen,
|
||||
isPreferredEdgeless,
|
||||
]);
|
||||
};
|
||||
|
321
apps/core/src/components/pure/cmdk/data.tsx
Normal file
321
apps/core/src/components/pure/cmdk/data.tsx
Normal file
@ -0,0 +1,321 @@
|
||||
import { commandScore } from '@affine/cmdk';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
||||
import type { Page, PageMeta } from '@blocksuite/store';
|
||||
import {
|
||||
useBlockSuitePageMeta,
|
||||
usePageMetaHelper,
|
||||
} from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import {
|
||||
getWorkspace,
|
||||
waitForWorkspace,
|
||||
} from '@toeverything/infra/__internal__/workspace';
|
||||
import {
|
||||
currentPageIdAtom,
|
||||
currentWorkspaceIdAtom,
|
||||
getCurrentStore,
|
||||
} from '@toeverything/infra/atom';
|
||||
import {
|
||||
type AffineCommand,
|
||||
AffineCommandRegistry,
|
||||
type CommandCategory,
|
||||
PreconditionStrategy,
|
||||
} from '@toeverything/infra/command';
|
||||
import { atom, useAtomValue } from 'jotai';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import {
|
||||
openQuickSearchModalAtom,
|
||||
pageSettingsAtom,
|
||||
recentPageIdsBaseAtom,
|
||||
} from '../../../atoms';
|
||||
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
|
||||
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
||||
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
|
||||
import type { CMDKCommand, CommandContext } from './types';
|
||||
|
||||
export const cmdkQueryAtom = atom('');
|
||||
|
||||
// like currentWorkspaceAtom, but not throw error
|
||||
const safeCurrentPageAtom = atom<Promise<Page | undefined>>(async get => {
|
||||
const currentWorkspaceId = get(currentWorkspaceIdAtom);
|
||||
if (!currentWorkspaceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentPageId = get(currentPageIdAtom);
|
||||
|
||||
if (!currentPageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const workspace = getWorkspace(currentWorkspaceId);
|
||||
await waitForWorkspace(workspace);
|
||||
const page = workspace.getPage(currentPageId);
|
||||
|
||||
if (!page) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!page.loaded) {
|
||||
await page.waitForLoaded();
|
||||
}
|
||||
return page;
|
||||
});
|
||||
|
||||
export const commandContextAtom = atom<Promise<CommandContext>>(async get => {
|
||||
const currentPage = await get(safeCurrentPageAtom);
|
||||
const pageSettings = get(pageSettingsAtom);
|
||||
|
||||
return {
|
||||
currentPage,
|
||||
pageMode: currentPage ? pageSettings[currentPage.id]?.mode : undefined,
|
||||
};
|
||||
});
|
||||
|
||||
function filterCommandByContext(
|
||||
command: AffineCommand,
|
||||
context: CommandContext
|
||||
) {
|
||||
if (command.preconditionStrategy === PreconditionStrategy.Always) {
|
||||
return true;
|
||||
}
|
||||
if (command.preconditionStrategy === PreconditionStrategy.InEdgeless) {
|
||||
return context.pageMode === 'edgeless';
|
||||
}
|
||||
if (command.preconditionStrategy === PreconditionStrategy.InPaper) {
|
||||
return context.pageMode === 'page';
|
||||
}
|
||||
if (command.preconditionStrategy === PreconditionStrategy.InPaperOrEdgeless) {
|
||||
return !!context.currentPage;
|
||||
}
|
||||
if (command.preconditionStrategy === PreconditionStrategy.Never) {
|
||||
return false;
|
||||
}
|
||||
if (typeof command.preconditionStrategy === 'function') {
|
||||
return command.preconditionStrategy();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
let quickSearchOpenCounter = 0;
|
||||
const openCountAtom = atom(get => {
|
||||
if (get(openQuickSearchModalAtom)) {
|
||||
quickSearchOpenCounter++;
|
||||
}
|
||||
return quickSearchOpenCounter;
|
||||
});
|
||||
|
||||
export const filteredAffineCommands = atom(async get => {
|
||||
const context = await get(commandContextAtom);
|
||||
// reset when modal open
|
||||
get(openCountAtom);
|
||||
const commands = AffineCommandRegistry.getAll();
|
||||
return commands.filter(command => {
|
||||
return filterCommandByContext(command, context);
|
||||
});
|
||||
});
|
||||
|
||||
const useWorkspacePages = () => {
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const pages = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
|
||||
return pages;
|
||||
};
|
||||
|
||||
const useRecentPages = () => {
|
||||
const pages = useWorkspacePages();
|
||||
const recentPageIds = useAtomValue(recentPageIdsBaseAtom);
|
||||
return useMemo(() => {
|
||||
return recentPageIds
|
||||
.map(pageId => {
|
||||
const page = pages.find(page => page.id === pageId);
|
||||
return page;
|
||||
})
|
||||
.filter((p): p is PageMeta => !!p);
|
||||
}, [recentPageIds, pages]);
|
||||
};
|
||||
|
||||
const valueWrapperStart = '__>>>';
|
||||
const valueWrapperEnd = '<<<__';
|
||||
|
||||
export const pageToCommand = (
|
||||
category: CommandCategory,
|
||||
page: PageMeta,
|
||||
store: ReturnType<typeof getCurrentStore>,
|
||||
navigationHelper: ReturnType<typeof useNavigateHelper>,
|
||||
t: ReturnType<typeof useAFFiNEI18N>
|
||||
): CMDKCommand => {
|
||||
const pageMode = store.get(pageSettingsAtom)?.[page.id]?.mode;
|
||||
const currentWorkspaceId = store.get(currentWorkspaceIdAtom);
|
||||
const label = page.title || t['Untitled']();
|
||||
return {
|
||||
id: page.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 + page.id + '.' + category + valueWrapperEnd,
|
||||
originalValue: label,
|
||||
category: category,
|
||||
run: () => {
|
||||
if (!currentWorkspaceId) {
|
||||
console.error('current workspace not found');
|
||||
return;
|
||||
}
|
||||
navigationHelper.jumpToPage(currentWorkspaceId, page.id);
|
||||
},
|
||||
icon: pageMode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />,
|
||||
timestamp: page.updatedDate,
|
||||
};
|
||||
};
|
||||
|
||||
export const usePageCommands = () => {
|
||||
// todo: considering collections for searching pages
|
||||
// const { savedCollections } = useCollectionManager(currentCollectionsAtom);
|
||||
const recentPages = useRecentPages();
|
||||
const pages = useWorkspacePages();
|
||||
const store = getCurrentStore();
|
||||
const [workspace] = useCurrentWorkspace();
|
||||
const pageHelper = usePageHelper(workspace.blockSuiteWorkspace);
|
||||
const pageMetaHelper = usePageMetaHelper(workspace.blockSuiteWorkspace);
|
||||
const query = useAtomValue(cmdkQueryAtom);
|
||||
const navigationHelper = useNavigateHelper();
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
return useMemo(() => {
|
||||
let results: CMDKCommand[] = [];
|
||||
if (query.trim() === '') {
|
||||
results = recentPages.map(page => {
|
||||
return pageToCommand('affine:recent', page, store, navigationHelper, t);
|
||||
});
|
||||
} else {
|
||||
// queried pages that has matched contents
|
||||
const pageIds = Array.from(
|
||||
workspace.blockSuiteWorkspace.search({ query }).values()
|
||||
).map(id => {
|
||||
if (id.startsWith('space:')) {
|
||||
return id.slice(6);
|
||||
} else {
|
||||
return id;
|
||||
}
|
||||
});
|
||||
|
||||
results = pages.map(page => {
|
||||
const command = pageToCommand(
|
||||
'affine:pages',
|
||||
page,
|
||||
store,
|
||||
navigationHelper,
|
||||
t
|
||||
);
|
||||
|
||||
if (pageIds.includes(page.id)) {
|
||||
// hack to make the page always showing in the search result
|
||||
command.value += query;
|
||||
}
|
||||
|
||||
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:create-page',
|
||||
label: (
|
||||
<Trans
|
||||
i18nKey="com.affine.cmdk.affine.create-new-page-as"
|
||||
values={{ query }}
|
||||
>
|
||||
Create New Page as: <strong>query</strong>
|
||||
</Trans>
|
||||
),
|
||||
value: 'affine::create-page' + query, // hack to make the page always showing in the search result
|
||||
category: 'affine:creation',
|
||||
run: () => {
|
||||
const pageId = pageHelper.createPage();
|
||||
// need to wait for the page to be created
|
||||
setTimeout(() => {
|
||||
pageMetaHelper.setPageTitle(pageId, query);
|
||||
});
|
||||
},
|
||||
icon: <PageIcon />,
|
||||
});
|
||||
|
||||
results.push({
|
||||
id: 'affine:pages:create-edgeless',
|
||||
label: (
|
||||
<Trans
|
||||
values={{ query }}
|
||||
i18nKey="com.affine.cmdk.affine.create-new-edgeless-as"
|
||||
>
|
||||
Create New Edgeless as: <strong>query</strong>
|
||||
</Trans>
|
||||
),
|
||||
value: 'affine::create-edgeless' + query, // hack to make the page always showing in the search result
|
||||
category: 'affine:creation',
|
||||
run: () => {
|
||||
const pageId = pageHelper.createEdgeless();
|
||||
// need to wait for the page to be created
|
||||
setTimeout(() => {
|
||||
pageMetaHelper.setPageTitle(pageId, query);
|
||||
});
|
||||
},
|
||||
icon: <EdgelessIcon />,
|
||||
});
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}, [
|
||||
pageHelper,
|
||||
pageMetaHelper,
|
||||
navigationHelper,
|
||||
pages,
|
||||
query,
|
||||
recentPages,
|
||||
store,
|
||||
t,
|
||||
workspace.blockSuiteWorkspace,
|
||||
]);
|
||||
};
|
||||
|
||||
export const useCMDKCommandGroups = () => {
|
||||
const pageCommands = usePageCommands();
|
||||
const affineCommands = useAtomValue(filteredAffineCommands);
|
||||
|
||||
return useMemo(() => {
|
||||
const commands = [...pageCommands, ...affineCommands];
|
||||
const groups = groupBy(commands, command => command.category);
|
||||
return Object.entries(groups) as [CommandCategory, CMDKCommand[]][];
|
||||
}, [affineCommands, pageCommands]);
|
||||
};
|
||||
|
||||
export const customCommandFilter = (value: string, search: string) => {
|
||||
// strip off the part between __>>> and <<<__
|
||||
const label = value.replace(
|
||||
new RegExp(valueWrapperStart + '.*' + valueWrapperEnd, 'g'),
|
||||
''
|
||||
);
|
||||
return commandScore(label, search);
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
2
apps/core/src/components/pure/cmdk/index.tsx
Normal file
2
apps/core/src/components/pure/cmdk/index.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './main';
|
||||
export * from './modal';
|
131
apps/core/src/components/pure/cmdk/main.css.ts
Normal file
131
apps/core/src/components/pure/cmdk/main.css.ts
Normal file
@ -0,0 +1,131 @@
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const root = style({});
|
||||
|
||||
export const commandsContainer = style({
|
||||
height: 'calc(100% - 65px)',
|
||||
padding: '8px 6px 18px 6px',
|
||||
});
|
||||
|
||||
export const searchInput = style({
|
||||
height: 66,
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
fontSize: 'var(--affine-font-h-5)',
|
||||
padding: '21px 24px',
|
||||
width: '100%',
|
||||
borderBottom: '1px solid var(--affine-border-color)',
|
||||
flexShrink: 0,
|
||||
|
||||
'::placeholder': {
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
},
|
||||
});
|
||||
|
||||
export const panelContainer = style({
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
});
|
||||
|
||||
export const itemIcon = style({
|
||||
fontSize: 20,
|
||||
marginRight: 16,
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: 'var(--affine-icon-secondary)',
|
||||
});
|
||||
|
||||
export const itemLabel = style({
|
||||
fontSize: 14,
|
||||
lineHeight: '1.5',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
flex: 1,
|
||||
});
|
||||
|
||||
export const timestamp = style({
|
||||
display: 'flex',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
});
|
||||
|
||||
export const keybinding = style({
|
||||
display: 'flex',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
columnGap: 2,
|
||||
});
|
||||
|
||||
export const keybindingFragment = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '0 4px',
|
||||
borderRadius: 4,
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
backgroundColor: 'var(--affine-background-tertiary-color)',
|
||||
width: 24,
|
||||
height: 20,
|
||||
});
|
||||
|
||||
globalStyle(`${root} [cmdk-root]`, {
|
||||
height: '100%',
|
||||
});
|
||||
|
||||
globalStyle(`${root} [cmdk-group-heading]`, {
|
||||
padding: '8px',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
fontWeight: 600,
|
||||
lineHeight: '1.67',
|
||||
});
|
||||
|
||||
globalStyle(`${root} [cmdk-group][hidden]`, {
|
||||
display: 'none',
|
||||
});
|
||||
|
||||
globalStyle(`${root} [cmdk-list]`, {
|
||||
maxHeight: 400,
|
||||
overflow: 'auto',
|
||||
overscrollBehavior: 'contain',
|
||||
transition: '.1s ease',
|
||||
transitionProperty: 'height',
|
||||
height: 'min(330px, calc(var(--cmdk-list-height) + 8px))',
|
||||
padding: '0 6px 8px 6px',
|
||||
});
|
||||
|
||||
globalStyle(`${root} [cmdk-list]::-webkit-scrollbar`, {
|
||||
width: 8,
|
||||
height: 8,
|
||||
});
|
||||
|
||||
globalStyle(`${root} [cmdk-list]::-webkit-scrollbar-thumb`, {
|
||||
borderRadius: 4,
|
||||
border: '1px solid transparent',
|
||||
backgroundClip: 'padding-box',
|
||||
});
|
||||
|
||||
globalStyle(`${root} [cmdk-list]:hover::-webkit-scrollbar-thumb`, {
|
||||
backgroundColor: 'var(--affine-divider-color)',
|
||||
});
|
||||
|
||||
globalStyle(`${root} [cmdk-list]:hover::-webkit-scrollbar-thumb:hover`, {
|
||||
backgroundColor: 'var(--affine-icon-color)',
|
||||
});
|
||||
|
||||
globalStyle(`${root} [cmdk-item]`, {
|
||||
display: 'flex',
|
||||
height: 44,
|
||||
padding: '0 12px',
|
||||
alignItems: 'center',
|
||||
cursor: 'default',
|
||||
borderRadius: 4,
|
||||
userSelect: 'none',
|
||||
});
|
||||
|
||||
globalStyle(`${root} [cmdk-item][data-selected=true]`, {
|
||||
background: 'var(--affine-background-secondary-color)',
|
||||
});
|
||||
|
||||
globalStyle(`${root} [cmdk-item][data-selected=true] ${itemIcon}`, {
|
||||
color: 'var(--affine-icon-color)',
|
||||
});
|
215
apps/core/src/components/pure/cmdk/main.tsx
Normal file
215
apps/core/src/components/pure/cmdk/main.tsx
Normal file
@ -0,0 +1,215 @@
|
||||
import { Command } from '@affine/cmdk';
|
||||
import { formatDate } from '@affine/component/page-list';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { CommandCategory } from '@toeverything/infra/command';
|
||||
import clsx from 'clsx';
|
||||
import { useAtom, useSetAtom } from 'jotai';
|
||||
import { Suspense, useEffect, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
cmdkQueryAtom,
|
||||
customCommandFilter,
|
||||
useCMDKCommandGroups,
|
||||
} from './data';
|
||||
import * as styles from './main.css';
|
||||
import { CMDKModal, type CMDKModalProps } from './modal';
|
||||
import type { CMDKCommand } from './types';
|
||||
|
||||
type NoParametersKeys<T> = {
|
||||
[K in keyof T]: T[K] extends () => any ? K : never;
|
||||
}[keyof T];
|
||||
|
||||
type i18nKey = NoParametersKeys<ReturnType<typeof useAFFiNEI18N>>;
|
||||
|
||||
const categoryToI18nKey: Record<CommandCategory, i18nKey> = {
|
||||
'affine:recent': 'com.affine.cmdk.affine.category.affine.recent',
|
||||
'affine:navigation': 'com.affine.cmdk.affine.category.affine.navigation',
|
||||
'affine:creation': 'com.affine.cmdk.affine.category.affine.creation',
|
||||
'affine:general': 'com.affine.cmdk.affine.category.affine.general',
|
||||
'affine:layout': 'com.affine.cmdk.affine.category.affine.layout',
|
||||
'affine:pages': 'com.affine.cmdk.affine.category.affine.pages',
|
||||
'affine:settings': 'com.affine.cmdk.affine.category.affine.settings',
|
||||
'affine:updates': 'com.affine.cmdk.affine.category.affine.updates',
|
||||
'affine:help': 'com.affine.cmdk.affine.category.affine.help',
|
||||
'editor:edgeless': 'com.affine.cmdk.affine.category.editor.edgeless',
|
||||
'editor:insert-object':
|
||||
'com.affine.cmdk.affine.category.editor.insert-object',
|
||||
'editor:page': 'com.affine.cmdk.affine.category.editor.page',
|
||||
};
|
||||
|
||||
const QuickSearchGroup = ({
|
||||
category,
|
||||
commands,
|
||||
onOpenChange,
|
||||
}: {
|
||||
category: CommandCategory;
|
||||
commands: CMDKCommand[];
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const i18nkey = categoryToI18nKey[category];
|
||||
const setQuery = useSetAtom(cmdkQueryAtom);
|
||||
return (
|
||||
<Command.Group key={category} heading={t[i18nkey]()}>
|
||||
{commands.map(command => {
|
||||
return (
|
||||
<Command.Item
|
||||
key={command.id}
|
||||
onSelect={() => {
|
||||
command.run();
|
||||
setQuery('');
|
||||
onOpenChange?.(false);
|
||||
}}
|
||||
value={command.value}
|
||||
>
|
||||
<div className={styles.itemIcon}>{command.icon}</div>
|
||||
<div
|
||||
data-testid="cmdk-label"
|
||||
className={styles.itemLabel}
|
||||
data-value={
|
||||
command.originalValue ? command.originalValue : undefined
|
||||
}
|
||||
>
|
||||
{command.label}
|
||||
</div>
|
||||
{command.timestamp ? (
|
||||
<div className={styles.timestamp}>
|
||||
{formatDate(new Date(command.timestamp))}
|
||||
</div>
|
||||
) : null}
|
||||
{command.keyBinding ? (
|
||||
<CMDKKeyBinding
|
||||
keyBinding={
|
||||
typeof command.keyBinding === 'string'
|
||||
? command.keyBinding
|
||||
: command.keyBinding.binding
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</Command.Item>
|
||||
);
|
||||
})}
|
||||
</Command.Group>
|
||||
);
|
||||
};
|
||||
|
||||
const QuickSearchCommands = ({
|
||||
onOpenChange,
|
||||
}: {
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}) => {
|
||||
const groups = useCMDKCommandGroups();
|
||||
|
||||
return groups.map(([category, commands]) => {
|
||||
return (
|
||||
<QuickSearchGroup
|
||||
key={category}
|
||||
onOpenChange={onOpenChange}
|
||||
category={category}
|
||||
commands={commands}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const CMDKContainer = ({
|
||||
className,
|
||||
onQueryChange,
|
||||
query,
|
||||
children,
|
||||
...rest
|
||||
}: React.PropsWithChildren<{
|
||||
className?: string;
|
||||
query: string;
|
||||
onQueryChange: (query: string) => void;
|
||||
}>) => {
|
||||
const t = useAFFiNEI18N();
|
||||
return (
|
||||
<Command
|
||||
{...rest}
|
||||
data-testid="cmdk-quick-search"
|
||||
filter={customCommandFilter}
|
||||
className={clsx(className, styles.panelContainer)}
|
||||
// Handle KeyboardEvent conflicts with blocksuite
|
||||
onKeyDown={(e: React.KeyboardEvent) => {
|
||||
if (
|
||||
e.key === 'ArrowDown' ||
|
||||
e.key === 'ArrowUp' ||
|
||||
e.key === 'ArrowLeft' ||
|
||||
e.key === 'ArrowRight'
|
||||
) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* todo: add page context here */}
|
||||
<Command.Input
|
||||
placeholder={t['com.affine.cmdk.placeholder']()}
|
||||
autoFocus
|
||||
{...rest}
|
||||
value={query}
|
||||
onValueChange={onQueryChange}
|
||||
className={clsx(className, styles.searchInput)}
|
||||
/>
|
||||
<Command.List>{children}</Command.List>
|
||||
</Command>
|
||||
);
|
||||
};
|
||||
|
||||
export const CMDKQuickSearchModal = (props: CMDKModalProps) => {
|
||||
const [query, setQuery] = useAtom(cmdkQueryAtom);
|
||||
useEffect(() => {
|
||||
if (props.open) {
|
||||
setQuery('');
|
||||
}
|
||||
}, [props.open, setQuery]);
|
||||
return (
|
||||
<CMDKModal {...props}>
|
||||
<CMDKContainer
|
||||
className={styles.root}
|
||||
query={query}
|
||||
onQueryChange={setQuery}
|
||||
>
|
||||
<Suspense fallback={<Command.Loading />}>
|
||||
<QuickSearchCommands onOpenChange={props.onOpenChange} />
|
||||
</Suspense>
|
||||
</CMDKContainer>
|
||||
</CMDKModal>
|
||||
);
|
||||
};
|
||||
|
||||
const CMDKKeyBinding = ({ keyBinding }: { keyBinding: string }) => {
|
||||
const isMacOS = environment.isBrowser && environment.isMacOs;
|
||||
const fragments = useMemo(() => {
|
||||
return keyBinding.split('+').map(fragment => {
|
||||
if (fragment === '$mod') {
|
||||
return isMacOS ? '⌘' : 'Ctrl';
|
||||
}
|
||||
if (fragment === 'ArrowUp') {
|
||||
return '↑';
|
||||
}
|
||||
if (fragment === 'ArrowDown') {
|
||||
return '↓';
|
||||
}
|
||||
if (fragment === 'ArrowLeft') {
|
||||
return '←';
|
||||
}
|
||||
if (fragment === 'ArrowRight') {
|
||||
return '→';
|
||||
}
|
||||
return fragment;
|
||||
});
|
||||
}, [isMacOS, keyBinding]);
|
||||
|
||||
return (
|
||||
<div className={styles.keybinding}>
|
||||
{fragments.map((fragment, index) => {
|
||||
return (
|
||||
<div key={index} className={styles.keybindingFragment}>
|
||||
{fragment}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
55
apps/core/src/components/pure/cmdk/modal.css.ts
Normal file
55
apps/core/src/components/pure/cmdk/modal.css.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { keyframes, style } from '@vanilla-extract/css';
|
||||
|
||||
const contentShow = keyframes({
|
||||
from: { opacity: 0, transform: 'translateY(-2%) scale(0.96)' },
|
||||
to: { opacity: 1, transform: 'translateY(0) scale(1)' },
|
||||
});
|
||||
|
||||
const contentHide = keyframes({
|
||||
to: { opacity: 0, transform: 'translateY(-2%) scale(0.96)' },
|
||||
from: { opacity: 1, transform: 'translateY(0) scale(1)' },
|
||||
});
|
||||
|
||||
export const modalOverlay = style({
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
backgroundColor: 'transparent',
|
||||
zIndex: 'var(--affine-z-index-modal)',
|
||||
});
|
||||
|
||||
export const modalContentWrapper = style({
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'center',
|
||||
zIndex: 'var(--affine-z-index-modal)',
|
||||
padding: '13vh 16px 16px',
|
||||
});
|
||||
|
||||
export const modalContent = style({
|
||||
width: 640,
|
||||
// height: 530,
|
||||
backgroundColor: 'var(--affine-background-overlay-panel-color)',
|
||||
boxShadow: 'var(--affine-cmd-shadow)',
|
||||
borderRadius: '12px',
|
||||
maxWidth: 'calc(100vw - 50px)',
|
||||
minWidth: 480,
|
||||
// minHeight: 420,
|
||||
// :focus-visible will set outline
|
||||
outline: 'none',
|
||||
position: 'relative',
|
||||
zIndex: 'var(--affine-z-index-modal)',
|
||||
willChange: 'transform, opacity',
|
||||
|
||||
selectors: {
|
||||
'&[data-state=entered], &[data-state=entering]': {
|
||||
animation: `${contentShow} 120ms cubic-bezier(0.42, 0, 0.58, 1)`,
|
||||
animationFillMode: 'forwards',
|
||||
},
|
||||
'&[data-state=exited], &[data-state=exiting]': {
|
||||
animation: `${contentHide} 120ms cubic-bezier(0.42, 0, 0.58, 1)`,
|
||||
animationFillMode: 'forwards',
|
||||
},
|
||||
},
|
||||
});
|
67
apps/core/src/components/pure/cmdk/modal.tsx
Normal file
67
apps/core/src/components/pure/cmdk/modal.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
import { useEffect, useReducer } from 'react';
|
||||
|
||||
import * as styles from './modal.css';
|
||||
|
||||
// a CMDK modal that can be used to display a CMDK command
|
||||
// it has a smooth animation and can be closed by clicking outside of the modal
|
||||
|
||||
export interface CMDKModalProps {
|
||||
open: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
type ModalAnimationState = 'entering' | 'entered' | 'exiting' | 'exited';
|
||||
|
||||
function reduceAnimationState(
|
||||
state: ModalAnimationState,
|
||||
action: 'open' | 'close' | 'finish'
|
||||
) {
|
||||
switch (action) {
|
||||
case 'open':
|
||||
return state === 'entered' || state === 'entering' ? state : 'entering';
|
||||
case 'close':
|
||||
return state === 'exited' || state === 'exiting' ? state : 'exiting';
|
||||
case 'finish':
|
||||
return state === 'entering' ? 'entered' : 'exited';
|
||||
}
|
||||
}
|
||||
|
||||
export const CMDKModal = ({
|
||||
onOpenChange,
|
||||
open,
|
||||
children,
|
||||
}: React.PropsWithChildren<CMDKModalProps>) => {
|
||||
const [animationState, dispatch] = useReducer(reduceAnimationState, 'exited');
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(open ? 'open' : 'close');
|
||||
const timeout = setTimeout(() => {
|
||||
dispatch('finish');
|
||||
}, 120);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<Dialog.Root
|
||||
modal
|
||||
open={animationState !== 'exited'}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className={styles.modalOverlay} />
|
||||
<div className={styles.modalContentWrapper}>
|
||||
<Dialog.Content
|
||||
className={styles.modalContent}
|
||||
data-state={animationState}
|
||||
>
|
||||
{children}
|
||||
</Dialog.Content>
|
||||
</div>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
};
|
22
apps/core/src/components/pure/cmdk/types.ts
Normal file
22
apps/core/src/components/pure/cmdk/types.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import type { CommandCategory } from '@toeverything/infra/command';
|
||||
|
||||
export interface CommandContext {
|
||||
currentPage: Page | undefined;
|
||||
pageMode: 'page' | 'edgeless' | undefined;
|
||||
}
|
||||
|
||||
// similar to AffineCommand, but for rendering into the UI
|
||||
// it unifies all possible commands into a single type so that
|
||||
// we can use a single render function to render all different commands
|
||||
export interface CMDKCommand {
|
||||
id: string;
|
||||
label: string | React.ReactNode;
|
||||
icon?: React.ReactNode;
|
||||
category: CommandCategory;
|
||||
keyBinding?: string | { binding: string };
|
||||
timestamp?: number;
|
||||
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<void>;
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
DeleteTemporarilyIcon,
|
||||
FolderIcon,
|
||||
SettingsIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import { useAtom } from 'jotai';
|
||||
import type { ReactElement, SVGProps } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { openSettingModalAtom } from '../../../atoms';
|
||||
|
||||
type IconComponent = (props: SVGProps<SVGSVGElement>) => ReactElement;
|
||||
|
||||
interface ConfigItem {
|
||||
title: string;
|
||||
icon: IconComponent;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
interface ConfigPathItem {
|
||||
title: string;
|
||||
icon: IconComponent;
|
||||
subPath: WorkspaceSubPath;
|
||||
}
|
||||
|
||||
export type Config = ConfigItem | ConfigPathItem;
|
||||
|
||||
export const useSwitchToConfig = (workspaceId: string): Config[] => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [, setOpenSettingModalAtom] = useAtom(openSettingModalAtom);
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
{
|
||||
title: t['com.affine.workspaceSubPath.all'](),
|
||||
subPath: WorkspaceSubPath.ALL,
|
||||
icon: FolderIcon,
|
||||
},
|
||||
{
|
||||
title: t['Workspace Settings'](),
|
||||
onClick: () => {
|
||||
setOpenSettingModalAtom({
|
||||
open: true,
|
||||
activeTab: 'workspace',
|
||||
workspaceId,
|
||||
});
|
||||
},
|
||||
icon: SettingsIcon,
|
||||
},
|
||||
{
|
||||
title: t['com.affine.workspaceSubPath.trash'](),
|
||||
subPath: WorkspaceSubPath.TRASH,
|
||||
icon: DeleteTemporarilyIcon,
|
||||
},
|
||||
],
|
||||
[t, workspaceId, setOpenSettingModalAtom]
|
||||
);
|
||||
};
|
@ -1,59 +0,0 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { assertEquals } from '@blocksuite/global/utils';
|
||||
import { PlusIcon } from '@blocksuite/icons';
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
|
||||
import { initEmptyPage } from '@toeverything/infra/blocksuite';
|
||||
import { Command } from 'cmdk';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
||||
import type { BlockSuiteWorkspace } from '../../../shared';
|
||||
import { StyledModalFooterContent } from './style';
|
||||
|
||||
export interface FooterProps {
|
||||
query: string;
|
||||
onClose: () => void;
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||
}
|
||||
|
||||
export const Footer = ({
|
||||
query,
|
||||
onClose,
|
||||
blockSuiteWorkspace,
|
||||
}: FooterProps) => {
|
||||
const { createPage } = useBlockSuiteWorkspaceHelper(blockSuiteWorkspace);
|
||||
const t = useAFFiNEI18N();
|
||||
const { jumpToPage } = useNavigateHelper();
|
||||
const MAX_QUERY_SHOW_LENGTH = 20;
|
||||
const normalizedQuery =
|
||||
query.length > MAX_QUERY_SHOW_LENGTH
|
||||
? query.slice(0, MAX_QUERY_SHOW_LENGTH) + '...'
|
||||
: query;
|
||||
|
||||
return (
|
||||
<Command.Item
|
||||
data-testid="quick-search-add-new-page"
|
||||
onSelect={useCallback(async () => {
|
||||
const id = nanoid();
|
||||
const page = createPage(id);
|
||||
assertEquals(page.id, id);
|
||||
await initEmptyPage(page, query);
|
||||
blockSuiteWorkspace.setPageMeta(page.id, {
|
||||
title: query,
|
||||
});
|
||||
onClose();
|
||||
jumpToPage(blockSuiteWorkspace.id, page.id);
|
||||
}, [blockSuiteWorkspace, createPage, jumpToPage, onClose, query])}
|
||||
>
|
||||
<StyledModalFooterContent>
|
||||
<PlusIcon />
|
||||
{query ? (
|
||||
<span>{t['New Keyword Page']({ query: normalizedQuery })}</span>
|
||||
) : (
|
||||
<span>{t['New Page']()}</span>
|
||||
)}
|
||||
</StyledModalFooterContent>
|
||||
</Command.Item>
|
||||
);
|
||||
};
|
@ -1,164 +0,0 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Modal } from '@toeverything/components/modal';
|
||||
import { Command } from 'cmdk';
|
||||
import { startTransition, Suspense } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import type { AllWorkspace } from '../../../shared';
|
||||
import { Footer } from './footer';
|
||||
import { Results } from './results';
|
||||
import { SearchInput } from './search-input';
|
||||
import {
|
||||
StyledContent,
|
||||
StyledModalDivider,
|
||||
StyledModalFooter,
|
||||
StyledModalHeader,
|
||||
StyledNotFound,
|
||||
StyledShortcut,
|
||||
} from './style';
|
||||
|
||||
export interface QuickSearchModalProps {
|
||||
workspace: AllWorkspace;
|
||||
open: boolean;
|
||||
setOpen: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export const QuickSearchModal = ({
|
||||
open,
|
||||
setOpen,
|
||||
workspace,
|
||||
}: QuickSearchModalProps) => {
|
||||
const blockSuiteWorkspace = workspace?.blockSuiteWorkspace;
|
||||
const t = useAFFiNEI18N();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [query, _setQuery] = useState('');
|
||||
const setQuery = useCallback((query: string) => {
|
||||
startTransition(() => {
|
||||
_setQuery(query);
|
||||
});
|
||||
}, []);
|
||||
const [showCreatePage, setShowCreatePage] = useState(true);
|
||||
const handleClose = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, [setOpen]);
|
||||
|
||||
// Add ‘⌘+K’ shortcut keys as switches
|
||||
useEffect(() => {
|
||||
const keydown = (e: KeyboardEvent) => {
|
||||
if ((e.key === 'k' && e.metaKey) || (e.key === 'k' && e.ctrlKey)) {
|
||||
const selection = window.getSelection();
|
||||
// prevent search bar focus in firefox
|
||||
e.preventDefault();
|
||||
setQuery('');
|
||||
if (selection?.toString()) {
|
||||
setOpen(false);
|
||||
return;
|
||||
}
|
||||
setOpen(!open);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', keydown, { capture: true });
|
||||
return () =>
|
||||
document.removeEventListener('keydown', keydown, { capture: true });
|
||||
}, [open, setOpen, setQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
// Waiting for DOM rendering
|
||||
requestAnimationFrame(() => {
|
||||
const inputElement = inputRef.current;
|
||||
inputElement?.focus();
|
||||
});
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
width={608}
|
||||
withoutCloseButton
|
||||
contentOptions={{
|
||||
['data-testid' as string]: 'quickSearch',
|
||||
style: {
|
||||
maxHeight: '80vh',
|
||||
minHeight: '412px',
|
||||
top: '80px',
|
||||
overflow: 'hidden',
|
||||
transform: 'translateX(-50%)',
|
||||
padding: 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Command
|
||||
shouldFilter={false}
|
||||
//Handle KeyboardEvent conflicts with blocksuite
|
||||
onKeyDown={(e: React.KeyboardEvent) => {
|
||||
if (
|
||||
e.key === 'ArrowDown' ||
|
||||
e.key === 'ArrowUp' ||
|
||||
e.key === 'ArrowLeft' ||
|
||||
e.key === 'ArrowRight'
|
||||
) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<StyledModalHeader>
|
||||
<SearchInput
|
||||
ref={inputRef}
|
||||
onValueChange={value => {
|
||||
setQuery(value);
|
||||
}}
|
||||
onKeyDown={e => {
|
||||
// Avoid triggering the cmdk onSelect event when the input method is in use
|
||||
if (e.nativeEvent.isComposing) {
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
}}
|
||||
placeholder={t['Quick search placeholder']()}
|
||||
/>
|
||||
<StyledShortcut>
|
||||
{environment.isBrowser && environment.isMacOs
|
||||
? '⌘ + K'
|
||||
: 'Ctrl + K'}
|
||||
</StyledShortcut>
|
||||
</StyledModalHeader>
|
||||
<StyledModalDivider />
|
||||
<Command.List>
|
||||
<StyledContent>
|
||||
<Suspense
|
||||
fallback={
|
||||
<StyledNotFound>
|
||||
<span>{t['com.affine.loading']()}</span>
|
||||
</StyledNotFound>
|
||||
}
|
||||
>
|
||||
<Results
|
||||
query={query}
|
||||
onClose={handleClose}
|
||||
workspace={workspace}
|
||||
setShowCreatePage={setShowCreatePage}
|
||||
/>
|
||||
</Suspense>
|
||||
</StyledContent>
|
||||
{showCreatePage ? (
|
||||
<>
|
||||
<StyledModalDivider />
|
||||
<StyledModalFooter>
|
||||
<Footer
|
||||
query={query}
|
||||
onClose={handleClose}
|
||||
blockSuiteWorkspace={blockSuiteWorkspace}
|
||||
/>
|
||||
</StyledModalFooter>
|
||||
</>
|
||||
) : null}
|
||||
</Command.List>
|
||||
</Command>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuickSearchModal;
|
@ -1,188 +0,0 @@
|
||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
|
||||
import { Command } from 'cmdk';
|
||||
import { type Atom, atom, useAtomValue } from 'jotai';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import { startTransition, useEffect } from 'react';
|
||||
|
||||
import { recentPageSettingsAtom } from '../../../atoms';
|
||||
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
||||
import type { AllWorkspace } from '../../../shared';
|
||||
import { useSwitchToConfig } from './config';
|
||||
import { StyledListItem, StyledNotFound } from './style';
|
||||
|
||||
export interface ResultsProps {
|
||||
workspace: AllWorkspace;
|
||||
query: string;
|
||||
onClose: () => void;
|
||||
setShowCreatePage: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
const loadAllPageWeakMap = new WeakMap<Workspace, Atom<Promise<void>>>();
|
||||
|
||||
function getLoadAllPage(workspace: Workspace) {
|
||||
if (loadAllPageWeakMap.has(workspace)) {
|
||||
return loadAllPageWeakMap.get(workspace) as Atom<Promise<void>>;
|
||||
} else {
|
||||
const aAtom = atom(async () => {
|
||||
// fixme: we have to load all pages here and re-index them
|
||||
// there might have performance issue
|
||||
await Promise.all(
|
||||
[...workspace.pages.values()].map(page =>
|
||||
page.waitForLoaded().then(() => {
|
||||
workspace.indexer.search.refreshPageIndex(page.id, page.spaceDoc);
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
loadAllPageWeakMap.set(workspace, aAtom);
|
||||
return aAtom;
|
||||
}
|
||||
}
|
||||
|
||||
export const Results = ({
|
||||
query,
|
||||
workspace,
|
||||
setShowCreatePage,
|
||||
onClose,
|
||||
}: ResultsProps) => {
|
||||
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
|
||||
useBlockSuiteWorkspaceHelper(blockSuiteWorkspace);
|
||||
const pageList = useBlockSuitePageMeta(blockSuiteWorkspace);
|
||||
assertExists(blockSuiteWorkspace.id);
|
||||
const list = useSwitchToConfig(workspace.id);
|
||||
useAtomValue(getLoadAllPage(blockSuiteWorkspace));
|
||||
|
||||
const recentPageSetting = useAtomValue(recentPageSettingsAtom);
|
||||
const t = useAFFiNEI18N();
|
||||
const { jumpToPage, jumpToSubPath } = useNavigateHelper();
|
||||
const pageIds = [...blockSuiteWorkspace.search({ query }).values()].map(
|
||||
id => {
|
||||
if (id.startsWith('space:')) {
|
||||
return id.slice(6);
|
||||
} else {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const resultsPageMeta = pageList.filter(
|
||||
page => pageIds.indexOf(page.id) > -1 && !page.trash
|
||||
);
|
||||
|
||||
const recentlyViewedItem = recentPageSetting.filter(recent => {
|
||||
const page = pageList.find(page => recent.id === page.id);
|
||||
if (!page) {
|
||||
return false;
|
||||
} else {
|
||||
return page.trash !== true;
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
startTransition(() => {
|
||||
setShowCreatePage(resultsPageMeta.length === 0);
|
||||
});
|
||||
}, [resultsPageMeta.length, setShowCreatePage]);
|
||||
|
||||
if (!query) {
|
||||
return (
|
||||
<>
|
||||
{recentlyViewedItem.length > 0 && (
|
||||
<Command.Group heading={t['Recent']()}>
|
||||
{recentlyViewedItem.map(recent => {
|
||||
const page = pageList.find(page => recent.id === page.id);
|
||||
assertExists(page);
|
||||
return (
|
||||
<Command.Item
|
||||
key={page.id}
|
||||
value={page.id}
|
||||
onSelect={() => {
|
||||
onClose();
|
||||
jumpToPage(blockSuiteWorkspace.id, page.id);
|
||||
}}
|
||||
>
|
||||
<StyledListItem>
|
||||
{recent.mode === 'edgeless' ? (
|
||||
<EdgelessIcon />
|
||||
) : (
|
||||
<PageIcon />
|
||||
)}
|
||||
<span>{page.title || UNTITLED_WORKSPACE_NAME}</span>
|
||||
</StyledListItem>
|
||||
</Command.Item>
|
||||
);
|
||||
})}
|
||||
</Command.Group>
|
||||
)}
|
||||
<Command.Group heading={t['Jump to']()}>
|
||||
{list.map(link => {
|
||||
return (
|
||||
<Command.Item
|
||||
key={link.title}
|
||||
value={link.title}
|
||||
onSelect={() => {
|
||||
onClose();
|
||||
if ('subPath' in link) {
|
||||
jumpToSubPath(blockSuiteWorkspace.id, link.subPath);
|
||||
} else if ('onClick' in link) {
|
||||
link.onClick();
|
||||
} else {
|
||||
throw new Error('Invalid link');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<StyledListItem>
|
||||
<link.icon />
|
||||
<span>{link.title}</span>
|
||||
</StyledListItem>
|
||||
</Command.Item>
|
||||
);
|
||||
})}
|
||||
</Command.Group>
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (!resultsPageMeta.length) {
|
||||
return (
|
||||
<StyledNotFound>
|
||||
<span>{t['Find 0 result']()}</span>
|
||||
<img
|
||||
alt="no result"
|
||||
src="/imgs/no-result.svg"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
</StyledNotFound>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Command.Group
|
||||
heading={t['Find results']({ number: `${resultsPageMeta.length}` })}
|
||||
>
|
||||
{resultsPageMeta.map(result => {
|
||||
return (
|
||||
<Command.Item
|
||||
key={result.id}
|
||||
onSelect={() => {
|
||||
onClose();
|
||||
assertExists(blockSuiteWorkspace.id);
|
||||
jumpToPage(blockSuiteWorkspace.id, result.id);
|
||||
}}
|
||||
value={result.id}
|
||||
>
|
||||
<StyledListItem>
|
||||
{result.mode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />}
|
||||
<span>{result.title || UNTITLED_WORKSPACE_NAME}</span>
|
||||
</StyledListItem>
|
||||
</Command.Item>
|
||||
);
|
||||
})}
|
||||
</Command.Group>
|
||||
);
|
||||
};
|
@ -1,33 +0,0 @@
|
||||
import { SearchIcon } from '@blocksuite/icons';
|
||||
import { Command } from 'cmdk';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import { StyledInputContent, StyledLabel } from './style';
|
||||
|
||||
export const SearchInput = forwardRef<
|
||||
HTMLInputElement,
|
||||
Omit<
|
||||
React.InputHTMLAttributes<HTMLInputElement>,
|
||||
'value' | 'onChange' | 'type'
|
||||
> & {
|
||||
/**
|
||||
* Optional controlled state for the value of the search input.
|
||||
*/
|
||||
value?: string;
|
||||
/**
|
||||
* Event handler called when the search value changes.
|
||||
*/
|
||||
onValueChange?: (search: string) => void;
|
||||
} & React.RefAttributes<HTMLInputElement>
|
||||
>((props, ref) => {
|
||||
return (
|
||||
<StyledInputContent>
|
||||
<StyledLabel htmlFor=":r5:">
|
||||
<SearchIcon />
|
||||
</StyledLabel>
|
||||
<Command.Input ref={ref} {...props} />
|
||||
</StyledInputContent>
|
||||
);
|
||||
});
|
||||
|
||||
SearchInput.displayName = 'SearchInput';
|
@ -1,180 +0,0 @@
|
||||
import { displayFlex, styled, textEllipsis } from '@affine/component';
|
||||
|
||||
export const StyledContent = styled('div')(() => {
|
||||
return {
|
||||
minHeight: '290px',
|
||||
maxHeight: '70vh',
|
||||
width: '100%',
|
||||
overflow: 'auto',
|
||||
marginBottom: '10px',
|
||||
...displayFlex('flex-start', 'flex-start'),
|
||||
flexDirection: 'column',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
transition: 'all 0.15s',
|
||||
letterSpacing: '0.06em',
|
||||
'[cmdk-group]': {
|
||||
width: '100%',
|
||||
},
|
||||
'[cmdk-group-heading]': {
|
||||
...displayFlex('start', 'center'),
|
||||
margin: '0 16px',
|
||||
height: '36px',
|
||||
lineHeight: '22px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
},
|
||||
'[cmdk-item]': {
|
||||
margin: '0 4px',
|
||||
},
|
||||
'[aria-selected="true"]': {
|
||||
transition: 'all 0.15s',
|
||||
borderRadius: '4px',
|
||||
color: 'var(--affine-primary-color)',
|
||||
backgroundColor: 'var(--affine-hover-color)',
|
||||
padding: '0 2px',
|
||||
},
|
||||
};
|
||||
});
|
||||
export const StyledJumpTo = styled('div')(() => {
|
||||
return {
|
||||
...displayFlex('center', 'start'),
|
||||
flexDirection: 'column',
|
||||
padding: '10px 10px 10px 0',
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
strong: {
|
||||
fontWeight: '500',
|
||||
marginBottom: '10px',
|
||||
},
|
||||
};
|
||||
});
|
||||
export const StyledNotFound = styled('div')(() => {
|
||||
return {
|
||||
width: '612px',
|
||||
...displayFlex('center', 'center'),
|
||||
flexDirection: 'column',
|
||||
padding: '0 16px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
lineHeight: '22px',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
span: {
|
||||
...displayFlex('flex-start', 'center'),
|
||||
width: '100%',
|
||||
fontWeight: '400',
|
||||
height: '36px',
|
||||
},
|
||||
|
||||
img: {
|
||||
marginTop: '10px',
|
||||
},
|
||||
};
|
||||
});
|
||||
export const StyledInputContent = styled('div')(() => {
|
||||
return {
|
||||
...displayFlex('space-between', 'center'),
|
||||
input: {
|
||||
width: '492px',
|
||||
height: '22px',
|
||||
padding: '0 12px',
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
...displayFlex('space-between', 'center'),
|
||||
letterSpacing: '0.06em',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
'::placeholder': {
|
||||
color: 'var(--affine-placeholder-color)',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
export const StyledShortcut = styled('div')(() => {
|
||||
return {
|
||||
color: 'var(--affine-placeholder-color)',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledLabel = styled('label')(() => {
|
||||
return {
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
color: 'var(--affine-icon-color)',
|
||||
fontSize: '20px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledModalHeader = styled('div')(() => {
|
||||
return {
|
||||
height: '36px',
|
||||
margin: '12px 16px 0px 16px',
|
||||
...displayFlex('space-between', 'center'),
|
||||
};
|
||||
});
|
||||
export const StyledModalDivider = styled('div')(() => {
|
||||
return {
|
||||
width: 'auto',
|
||||
height: '0',
|
||||
margin: '6px 16px',
|
||||
borderTop: '0.5px solid var(--affine-border-color)',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledModalFooter = styled('div')(() => {
|
||||
return {
|
||||
fontSize: 'inherit',
|
||||
lineHeight: '22px',
|
||||
marginBottom: '8px',
|
||||
textAlign: 'center',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
...displayFlex('center', 'center'),
|
||||
transition: 'all .15s',
|
||||
'[cmdk-item]': {
|
||||
margin: '0 4px',
|
||||
},
|
||||
'[aria-selected="true"]': {
|
||||
transition: 'all 0.15s',
|
||||
borderRadius: '4px',
|
||||
color: 'var(--affine-primary-color)',
|
||||
backgroundColor: 'var(--affine-hover-color)',
|
||||
'span,svg': {
|
||||
transition: 'all 0.15s',
|
||||
transform: 'scale(1.02)',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
export const StyledModalFooterContent = styled('button')(() => {
|
||||
return {
|
||||
width: '600px',
|
||||
height: '32px',
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
lineHeight: '22px',
|
||||
textAlign: 'center',
|
||||
...displayFlex('center', 'center'),
|
||||
color: 'inherit',
|
||||
borderRadius: '4px',
|
||||
transition: 'background .15s, color .15s',
|
||||
'>svg': {
|
||||
fontSize: '20px',
|
||||
marginRight: '12px',
|
||||
},
|
||||
};
|
||||
});
|
||||
export const StyledListItem = styled('button')(() => {
|
||||
return {
|
||||
width: '100%',
|
||||
height: '32px',
|
||||
fontSize: 'inherit',
|
||||
color: 'inherit',
|
||||
padding: '0 12px',
|
||||
borderRadius: '4px',
|
||||
transition: 'all .15s',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
span: {
|
||||
...textEllipsis(1),
|
||||
},
|
||||
'> svg': {
|
||||
fontSize: '20px',
|
||||
marginRight: '12px',
|
||||
},
|
||||
};
|
||||
});
|
@ -20,7 +20,7 @@ import {
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { useDroppable } from '@dnd-kit/core';
|
||||
import { Menu } from '@toeverything/components/menu';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import type { HTMLAttributes, ReactElement } from 'react';
|
||||
import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
@ -115,7 +115,7 @@ export const RootAppSidebar = ({
|
||||
return;
|
||||
}, [onClickNewPage]);
|
||||
|
||||
const [sidebarOpen, setSidebarOpen] = useAtom(appSidebarOpenAtom);
|
||||
const sidebarOpen = useAtomValue(appSidebarOpenAtom);
|
||||
useEffect(() => {
|
||||
if (environment.isDesktop) {
|
||||
window.apis?.ui.handleSidebarVisibilityChange(sidebarOpen).catch(err => {
|
||||
@ -124,17 +124,6 @@ export const RootAppSidebar = ({
|
||||
}
|
||||
}, [sidebarOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
const keydown = (e: KeyboardEvent) => {
|
||||
if ((e.key === '/' && e.metaKey) || (e.key === '/' && e.ctrlKey)) {
|
||||
setSidebarOpen(!sidebarOpen);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', keydown, { capture: true });
|
||||
return () =>
|
||||
document.removeEventListener('keydown', keydown, { capture: true });
|
||||
}, [sidebarOpen, setSidebarOpen]);
|
||||
|
||||
const [history, setHistory] = useHistoryAtom();
|
||||
const router = useMemo(() => {
|
||||
return {
|
||||
|
54
apps/core/src/hooks/use-register-workspace-commands.ts
Normal file
54
apps/core/src/hooks/use-register-workspace-commands.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useStore } from 'jotai';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import {
|
||||
registerAffineCreationCommands,
|
||||
registerAffineLayoutCommands,
|
||||
registerAffineSettingsCommands,
|
||||
} from '../commands';
|
||||
import { registerAffineNavigationCommands } from '../commands/affine-navigation';
|
||||
import { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils';
|
||||
import { useCurrentWorkspace } from './current/use-current-workspace';
|
||||
import { useNavigateHelper } from './use-navigate-helper';
|
||||
|
||||
export function useRegisterWorkspaceCommands() {
|
||||
const store = useStore();
|
||||
const t = useAFFiNEI18N();
|
||||
const theme = useTheme();
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const pageHelper = usePageHelper(currentWorkspace.blockSuiteWorkspace);
|
||||
const navigationHelper = useNavigateHelper();
|
||||
useEffect(() => {
|
||||
const unsubs: Array<() => void> = [];
|
||||
unsubs.push(
|
||||
registerAffineNavigationCommands({
|
||||
store,
|
||||
t,
|
||||
workspace: currentWorkspace.blockSuiteWorkspace,
|
||||
navigationHelper,
|
||||
})
|
||||
);
|
||||
unsubs.push(registerAffineSettingsCommands({ store, t, theme }));
|
||||
unsubs.push(registerAffineLayoutCommands({ store, t }));
|
||||
unsubs.push(
|
||||
registerAffineCreationCommands({
|
||||
store,
|
||||
pageHelper: pageHelper,
|
||||
t,
|
||||
})
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubs.forEach(unsub => unsub());
|
||||
};
|
||||
}, [
|
||||
store,
|
||||
pageHelper,
|
||||
t,
|
||||
theme,
|
||||
currentWorkspace.blockSuiteWorkspace,
|
||||
navigationHelper,
|
||||
]);
|
||||
}
|
@ -54,6 +54,7 @@ import {
|
||||
import { useBlockSuiteMetaHelper } from '../hooks/affine/use-block-suite-meta-helper';
|
||||
import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
|
||||
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||
import { useRegisterWorkspaceCommands } from '../hooks/use-register-workspace-commands';
|
||||
import {
|
||||
AllWorkspaceModals,
|
||||
CurrentWorkspaceModals,
|
||||
@ -61,9 +62,9 @@ import {
|
||||
import { pathGenerator } from '../shared';
|
||||
import { toast } from '../utils';
|
||||
|
||||
const QuickSearchModal = lazy(() =>
|
||||
import('../components/pure/quick-search-modal').then(module => ({
|
||||
default: module.QuickSearchModal,
|
||||
const CMDKQuickSearchModal = lazy(() =>
|
||||
import('../components/pure/cmdk').then(module => ({
|
||||
default: module.CMDKQuickSearchModal,
|
||||
}))
|
||||
);
|
||||
|
||||
@ -79,10 +80,9 @@ export const QuickSearch = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<QuickSearchModal
|
||||
workspace={currentWorkspace}
|
||||
<CMDKQuickSearchModal
|
||||
open={openQuickSearchModal}
|
||||
setOpen={setOpenQuickSearchModalAtom}
|
||||
onOpenChange={setOpenQuickSearchModalAtom}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -141,6 +141,10 @@ export const WorkspaceLayoutInner = ({
|
||||
}: PropsWithChildren<WorkspaceLayoutProps>) => {
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const { openPage } = useNavigateHelper();
|
||||
const pageHelper = usePageHelper(currentWorkspace.blockSuiteWorkspace);
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
useRegisterWorkspaceCommands();
|
||||
|
||||
useEffect(() => {
|
||||
// hotfix for blockVersions
|
||||
@ -164,15 +168,13 @@ export const WorkspaceLayoutInner = ({
|
||||
|
||||
usePassiveWorkspaceEffect(currentWorkspace.blockSuiteWorkspace);
|
||||
|
||||
const helper = usePageHelper(currentWorkspace.blockSuiteWorkspace);
|
||||
|
||||
const handleCreatePage = useCallback(() => {
|
||||
const id = nanoid();
|
||||
helper.createPage(id);
|
||||
pageHelper.createPage(id);
|
||||
const page = currentWorkspace.blockSuiteWorkspace.getPage(id);
|
||||
assertExists(page);
|
||||
return page;
|
||||
}, [currentWorkspace.blockSuiteWorkspace, helper]);
|
||||
}, [currentWorkspace.blockSuiteWorkspace, pageHelper]);
|
||||
|
||||
const [, setOpenQuickSearchModalAtom] = useAtom(openQuickSearchModalAtom);
|
||||
const handleOpenQuickSearchModal = useCallback(() => {
|
||||
@ -205,7 +207,6 @@ export const WorkspaceLayoutInner = ({
|
||||
const { removeToTrash: moveToTrash } = useBlockSuiteMetaHelper(
|
||||
currentWorkspace.blockSuiteWorkspace
|
||||
);
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
(e: DragEndEvent) => {
|
||||
|
@ -55,6 +55,7 @@
|
||||
"jotai": "^2.4.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"rxjs": "^7.8.1",
|
||||
"tinykeys": "^2.1.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"undici": "^5.23.0",
|
||||
"uuid": "^9.0.0",
|
||||
|
@ -45,6 +45,7 @@ export const config = () => {
|
||||
'@toeverything/plugin-infra',
|
||||
'yjs',
|
||||
'semver',
|
||||
'tinykeys',
|
||||
],
|
||||
define: define,
|
||||
format: 'cjs',
|
||||
|
@ -36,6 +36,13 @@ export default {
|
||||
async viteFinal(config, _options) {
|
||||
return mergeConfig(config, {
|
||||
assetsInclude: ['**/*.md'],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@toeverything/infra': fileURLToPath(
|
||||
new URL('../../../packages/infra/src', import.meta.url)
|
||||
),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
vanillaExtractPlugin(),
|
||||
tsconfigPaths({
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { Empty } from '@affine/component';
|
||||
import { toast } from '@affine/component';
|
||||
import { Empty, toast } from '@affine/component';
|
||||
import type { OperationCellProps } from '@affine/component/page-list';
|
||||
import { PageListTrashView } from '@affine/component/page-list';
|
||||
import { PageList } from '@affine/component/page-list';
|
||||
import { NewPageButton } from '@affine/component/page-list';
|
||||
import { OperationCell } from '@affine/component/page-list';
|
||||
import {
|
||||
NewPageButton,
|
||||
OperationCell,
|
||||
PageList,
|
||||
PageListTrashView,
|
||||
} from '@affine/component/page-list';
|
||||
import { PageIcon } from '@blocksuite/icons';
|
||||
import { expect } from '@storybook/jest';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
|
@ -0,0 +1,80 @@
|
||||
import { CMDKQuickSearchModal } from '@affine/core/components/pure/cmdk';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { getOrCreateWorkspace } from '@affine/workspace/manager';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import { currentWorkspaceIdAtom } from '@toeverything/infra/atom';
|
||||
import {
|
||||
registerAffineCreationCommands,
|
||||
registerAffineLayoutCommands,
|
||||
registerAffineSettingsCommands,
|
||||
} from 'apps/core/src/commands';
|
||||
import { useStore } from 'jotai';
|
||||
import { useEffect, useLayoutEffect } from 'react';
|
||||
import { withRouter } from 'storybook-addon-react-router-v6';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/QuickSearch',
|
||||
parameters: {
|
||||
chromatic: { disableSnapshot: true },
|
||||
},
|
||||
} satisfies Meta;
|
||||
|
||||
function useRegisterCommands() {
|
||||
const t = useAFFiNEI18N();
|
||||
const store = useStore();
|
||||
useEffect(() => {
|
||||
const unsubs = [
|
||||
registerAffineSettingsCommands({
|
||||
t,
|
||||
store,
|
||||
theme: {
|
||||
setTheme: () => {},
|
||||
theme: 'auto',
|
||||
themes: ['auto', 'dark', 'light'],
|
||||
},
|
||||
}),
|
||||
registerAffineCreationCommands({
|
||||
t,
|
||||
store,
|
||||
pageHelper: {
|
||||
createEdgeless: () => 'noop',
|
||||
createPage: () => 'noop',
|
||||
importFile: () => Promise.resolve(),
|
||||
isPreferredEdgeless: () => false,
|
||||
},
|
||||
}),
|
||||
registerAffineLayoutCommands({ t, store }),
|
||||
];
|
||||
|
||||
return () => {
|
||||
unsubs.forEach(unsub => unsub());
|
||||
};
|
||||
}, [store, t]);
|
||||
}
|
||||
|
||||
function usePrepareWorkspace() {
|
||||
const store = useStore();
|
||||
useLayoutEffect(() => {
|
||||
const workspaceId = 'test-workspace';
|
||||
getOrCreateWorkspace(workspaceId, WorkspaceFlavour.LOCAL);
|
||||
store.set(rootWorkspacesMetadataAtom, [
|
||||
{
|
||||
id: workspaceId,
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
version: 4,
|
||||
},
|
||||
]);
|
||||
store.set(currentWorkspaceIdAtom, workspaceId);
|
||||
}, [store]);
|
||||
}
|
||||
|
||||
export const CMDKStoryWithCommands: StoryFn = () => {
|
||||
usePrepareWorkspace();
|
||||
useRegisterCommands();
|
||||
|
||||
return <CMDKQuickSearchModal open />;
|
||||
};
|
||||
|
||||
CMDKStoryWithCommands.decorators = [withRouter];
|
@ -0,0 +1,37 @@
|
||||
import { CMDKContainer, CMDKModal } from '@affine/core/components/pure/cmdk';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/QuickSearch',
|
||||
parameters: {
|
||||
chromatic: { disableSnapshot: true },
|
||||
},
|
||||
} satisfies Meta;
|
||||
|
||||
export const CMDKModalStory: StoryFn = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [counter, setCounter] = useState(0);
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => setOpen(true)}>Open Modal</Button>
|
||||
<CMDKModal key={counter} open={open} onOpenChange={setOpen}>
|
||||
<Button onClick={() => setCounter(c => c + 1)}>
|
||||
Trigger new modal
|
||||
</Button>
|
||||
</CMDKModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const CMDKPanelStory: StoryFn = () => {
|
||||
const [query, setQuery] = useState('');
|
||||
return (
|
||||
<>
|
||||
<CMDKModal open>
|
||||
<CMDKContainer query={query} onQueryChange={setQuery} />
|
||||
</CMDKModal>
|
||||
</>
|
||||
);
|
||||
};
|
3
packages/cmdk/README.md
Normal file
3
packages/cmdk/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# copied directly from https://github.com/pacocoursey/cmdk
|
||||
|
||||
will remove after a new CMDK version is published to npm
|
12
packages/cmdk/package.json
Normal file
12
packages/cmdk/package.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@affine/cmdk",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.tsx",
|
||||
"module": "./src/index.tsx",
|
||||
"devDependencies": {
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"version": "0.2.1"
|
||||
}
|
179
packages/cmdk/src/command-score.ts
Normal file
179
packages/cmdk/src/command-score.ts
Normal file
@ -0,0 +1,179 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
// The scores are arranged so that a continuous match of characters will
|
||||
// result in a total score of 1.
|
||||
//
|
||||
// The best case, this character is a match, and either this is the start
|
||||
// of the string, or the previous character was also a match.
|
||||
var SCORE_CONTINUE_MATCH = 1,
|
||||
// A new match at the start of a word scores better than a new match
|
||||
// elsewhere as it's more likely that the user will type the starts
|
||||
// of fragments.
|
||||
// NOTE: We score word jumps between spaces slightly higher than slashes, brackets
|
||||
// hyphens, etc.
|
||||
SCORE_SPACE_WORD_JUMP = 0.9,
|
||||
SCORE_NON_SPACE_WORD_JUMP = 0.8,
|
||||
// Any other match isn't ideal, but we include it for completeness.
|
||||
SCORE_CHARACTER_JUMP = 0.17,
|
||||
// If the user transposed two letters, it should be significantly penalized.
|
||||
//
|
||||
// i.e. "ouch" is more likely than "curtain" when "uc" is typed.
|
||||
SCORE_TRANSPOSITION = 0.1,
|
||||
// The goodness of a match should decay slightly with each missing
|
||||
// character.
|
||||
//
|
||||
// i.e. "bad" is more likely than "bard" when "bd" is typed.
|
||||
//
|
||||
// This will not change the order of suggestions based on SCORE_* until
|
||||
// 100 characters are inserted between matches.
|
||||
PENALTY_SKIPPED = 0.999,
|
||||
// The goodness of an exact-case match should be higher than a
|
||||
// case-insensitive match by a small amount.
|
||||
//
|
||||
// i.e. "HTML" is more likely than "haml" when "HM" is typed.
|
||||
//
|
||||
// This will not change the order of suggestions based on SCORE_* until
|
||||
// 1000 characters are inserted between matches.
|
||||
PENALTY_CASE_MISMATCH = 0.9999,
|
||||
// Match higher for letters closer to the beginning of the word
|
||||
PENALTY_DISTANCE_FROM_START = 0.9,
|
||||
// If the word has more characters than the user typed, it should
|
||||
// be penalised slightly.
|
||||
//
|
||||
// i.e. "html" is more likely than "html5" if I type "html".
|
||||
//
|
||||
// However, it may well be the case that there's a sensible secondary
|
||||
// ordering (like alphabetical) that it makes sense to rely on when
|
||||
// there are many prefix matches, so we don't make the penalty increase
|
||||
// with the number of tokens.
|
||||
PENALTY_NOT_COMPLETE = 0.99;
|
||||
|
||||
var IS_GAP_REGEXP = /[\\\/_+.#"@\[\(\{&]/,
|
||||
COUNT_GAPS_REGEXP = /[\\\/_+.#"@\[\(\{&]/g,
|
||||
IS_SPACE_REGEXP = /[\s-]/,
|
||||
COUNT_SPACE_REGEXP = /[\s-]/g;
|
||||
|
||||
function commandScoreInner(
|
||||
string,
|
||||
abbreviation,
|
||||
lowerString,
|
||||
lowerAbbreviation,
|
||||
stringIndex,
|
||||
abbreviationIndex,
|
||||
memoizedResults
|
||||
) {
|
||||
if (abbreviationIndex === abbreviation.length) {
|
||||
if (stringIndex === string.length) {
|
||||
return SCORE_CONTINUE_MATCH;
|
||||
}
|
||||
return PENALTY_NOT_COMPLETE;
|
||||
}
|
||||
|
||||
var memoizeKey = `${stringIndex},${abbreviationIndex}`;
|
||||
if (memoizedResults[memoizeKey] !== undefined) {
|
||||
return memoizedResults[memoizeKey];
|
||||
}
|
||||
|
||||
var abbreviationChar = lowerAbbreviation.charAt(abbreviationIndex);
|
||||
var index = lowerString.indexOf(abbreviationChar, stringIndex);
|
||||
var highScore = 0;
|
||||
|
||||
var score, transposedScore, wordBreaks, spaceBreaks;
|
||||
|
||||
while (index >= 0) {
|
||||
score = commandScoreInner(
|
||||
string,
|
||||
abbreviation,
|
||||
lowerString,
|
||||
lowerAbbreviation,
|
||||
index + 1,
|
||||
abbreviationIndex + 1,
|
||||
memoizedResults
|
||||
);
|
||||
if (score > highScore) {
|
||||
if (index === stringIndex) {
|
||||
score *= SCORE_CONTINUE_MATCH;
|
||||
} else if (IS_GAP_REGEXP.test(string.charAt(index - 1))) {
|
||||
score *= SCORE_NON_SPACE_WORD_JUMP;
|
||||
wordBreaks = string
|
||||
.slice(stringIndex, index - 1)
|
||||
.match(COUNT_GAPS_REGEXP);
|
||||
if (wordBreaks && stringIndex > 0) {
|
||||
score *= Math.pow(PENALTY_SKIPPED, wordBreaks.length);
|
||||
}
|
||||
} else if (IS_SPACE_REGEXP.test(string.charAt(index - 1))) {
|
||||
score *= SCORE_SPACE_WORD_JUMP;
|
||||
spaceBreaks = string
|
||||
.slice(stringIndex, index - 1)
|
||||
.match(COUNT_SPACE_REGEXP);
|
||||
if (spaceBreaks && stringIndex > 0) {
|
||||
score *= Math.pow(PENALTY_SKIPPED, spaceBreaks.length);
|
||||
}
|
||||
} else {
|
||||
score *= SCORE_CHARACTER_JUMP;
|
||||
if (stringIndex > 0) {
|
||||
score *= Math.pow(PENALTY_SKIPPED, index - stringIndex);
|
||||
}
|
||||
}
|
||||
|
||||
if (string.charAt(index) !== abbreviation.charAt(abbreviationIndex)) {
|
||||
score *= PENALTY_CASE_MISMATCH;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(score < SCORE_TRANSPOSITION &&
|
||||
lowerString.charAt(index - 1) ===
|
||||
lowerAbbreviation.charAt(abbreviationIndex + 1)) ||
|
||||
(lowerAbbreviation.charAt(abbreviationIndex + 1) ===
|
||||
lowerAbbreviation.charAt(abbreviationIndex) && // allow duplicate letters. Ref #7428
|
||||
lowerString.charAt(index - 1) !==
|
||||
lowerAbbreviation.charAt(abbreviationIndex))
|
||||
) {
|
||||
transposedScore = commandScoreInner(
|
||||
string,
|
||||
abbreviation,
|
||||
lowerString,
|
||||
lowerAbbreviation,
|
||||
index + 1,
|
||||
abbreviationIndex + 2,
|
||||
memoizedResults
|
||||
);
|
||||
|
||||
if (transposedScore * SCORE_TRANSPOSITION > score) {
|
||||
score = transposedScore * SCORE_TRANSPOSITION;
|
||||
}
|
||||
}
|
||||
|
||||
if (score > highScore) {
|
||||
highScore = score;
|
||||
}
|
||||
|
||||
index = lowerString.indexOf(abbreviationChar, index + 1);
|
||||
}
|
||||
|
||||
memoizedResults[memoizeKey] = highScore;
|
||||
return highScore;
|
||||
}
|
||||
|
||||
function formatInput(string) {
|
||||
// convert all valid space characters to space so they match each other
|
||||
return string.toLowerCase().replace(COUNT_SPACE_REGEXP, ' ');
|
||||
}
|
||||
|
||||
export function commandScore(string: string, abbreviation: string): number {
|
||||
/* NOTE:
|
||||
* in the original, we used to do the lower-casing on each recursive call, but this meant that toLowerCase()
|
||||
* was the dominating cost in the algorithm, passing both is a little ugly, but considerably faster.
|
||||
*/
|
||||
return commandScoreInner(
|
||||
string,
|
||||
abbreviation,
|
||||
formatInput(string),
|
||||
formatInput(abbreviation),
|
||||
0,
|
||||
0,
|
||||
{}
|
||||
);
|
||||
}
|
1134
packages/cmdk/src/index.tsx
Normal file
1134
packages/cmdk/src/index.tsx
Normal file
File diff suppressed because it is too large
Load Diff
9
packages/cmdk/tsconfig.json
Normal file
9
packages/cmdk/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["./src"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"noEmit": false,
|
||||
"outDir": "lib"
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ import {
|
||||
blockSuiteEditorHeaderStyle,
|
||||
blockSuiteEditorStyle,
|
||||
} from './index.css';
|
||||
import { useRegisterBlocksuiteEditorCommands } from './use-register-blocksuite-editor-commands';
|
||||
|
||||
export type EditorProps = {
|
||||
page: Page;
|
||||
@ -164,6 +165,7 @@ export const BlockSuiteFallback = memo(function BlockSuiteFallback() {
|
||||
export const BlockSuiteEditor = memo(function BlockSuiteEditor(
|
||||
props: EditorProps & ErrorBoundaryProps
|
||||
): ReactElement {
|
||||
useRegisterBlocksuiteEditorCommands();
|
||||
return (
|
||||
<ErrorBoundary
|
||||
fallbackRender={useCallback(
|
||||
|
@ -0,0 +1,35 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { EdgelessIcon } from '@blocksuite/icons';
|
||||
import { registerAffineCommand } from '@toeverything/infra/command';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export function useRegisterBlocksuiteEditorCommands() {
|
||||
const t = useAFFiNEI18N();
|
||||
useEffect(() => {
|
||||
const unsubs: Array<() => void> = [];
|
||||
const getEdgeless = () => {
|
||||
return document.querySelector('affine-edgeless-page');
|
||||
};
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: 'editor:edgeless-presentation-start',
|
||||
preconditionStrategy: () => !!getEdgeless(),
|
||||
category: 'editor:edgeless',
|
||||
icon: <EdgelessIcon />,
|
||||
label: t['com.affine.cmdk.affine.editor.edgeless.presentation-start'](),
|
||||
run() {
|
||||
// this is pretty hack and easy to break. need a better way to communicate with blocksuite editor
|
||||
document
|
||||
.querySelector<HTMLElement>('edgeless-toolbar')
|
||||
?.shadowRoot?.querySelector<HTMLElement>(
|
||||
'.edgeless-toolbar-left-part > edgeless-tool-icon-button:last-child'
|
||||
)
|
||||
?.click();
|
||||
},
|
||||
})
|
||||
);
|
||||
return () => {
|
||||
unsubs.forEach(unsub => unsub());
|
||||
};
|
||||
}, [t]);
|
||||
}
|
@ -8,4 +8,5 @@ export * from './operation-menu-items';
|
||||
export * from './styles';
|
||||
export * from './type';
|
||||
export * from './use-collection-manager';
|
||||
export * from './utils';
|
||||
export * from './view';
|
||||
|
@ -584,5 +584,30 @@
|
||||
"com.affine.workspaceList.addWorkspace.create": "Create Workspace",
|
||||
"com.affine.workspaceList.workspaceListType.local": "Local Storage",
|
||||
"com.affine.workspaceList.workspaceListType.cloud": "Cloud Sync",
|
||||
"Local": "Local"
|
||||
"Local": "Local",
|
||||
"com.affine.cmdk.placeholder": "Type a command or search anything...",
|
||||
"com.affine.cmdk.affine.new-page": "New Page",
|
||||
"com.affine.cmdk.affine.new-edgeless-page": "New Edgeless",
|
||||
"com.affine.cmdk.affine.new-workspace": "New Workspace",
|
||||
"com.affine.cmdk.affine.create-new-page-as": "Create New Page as: <1>{{query}}</1>",
|
||||
"com.affine.cmdk.affine.create-new-edgeless-as": "Create New Edgeless as: <1>{{query}}</1>",
|
||||
"com.affine.cmdk.affine.color-scheme.to": "Change Colour Scheme to <1>{{colour}}</1>",
|
||||
"com.affine.cmdk.affine.left-sidebar.expand": "Expand Left Sidebar",
|
||||
"com.affine.cmdk.affine.left-sidebar.collapse": "Collapse Left Sidebar",
|
||||
"com.affine.cmdk.affine.navigation.goto-all-pages": "Go to All Pages",
|
||||
"com.affine.cmdk.affine.navigation.open-settings": "Go to Settings",
|
||||
"com.affine.cmdk.affine.navigation.goto-trash": "Go to Trash",
|
||||
"com.affine.cmdk.affine.category.affine.recent": "Recent",
|
||||
"com.affine.cmdk.affine.category.affine.navigation": "Navigation",
|
||||
"com.affine.cmdk.affine.category.affine.pages": "Pages",
|
||||
"com.affine.cmdk.affine.category.affine.creation": "Create",
|
||||
"com.affine.cmdk.affine.category.affine.settings": "Settings",
|
||||
"com.affine.cmdk.affine.category.affine.layout": "Layout Controls",
|
||||
"com.affine.cmdk.affine.category.affine.help": "Help",
|
||||
"com.affine.cmdk.affine.category.affine.updates": "Updates",
|
||||
"com.affine.cmdk.affine.category.affine.general": "General",
|
||||
"com.affine.cmdk.affine.category.editor.insert-object": "Insert Object",
|
||||
"com.affine.cmdk.affine.category.editor.page": "Page Commands",
|
||||
"com.affine.cmdk.affine.category.editor.edgeless": "Edgeless Commands",
|
||||
"com.affine.cmdk.affine.editor.edgeless.presentation-start": "Start Presentation"
|
||||
}
|
||||
|
@ -15,6 +15,11 @@
|
||||
"import": "./dist/blocksuite.js",
|
||||
"require": "./dist/blocksuite.cjs"
|
||||
},
|
||||
"./command": {
|
||||
"types": "./dist/src/command/index.d.ts",
|
||||
"import": "./dist/command.js",
|
||||
"require": "./dist/command.cjs"
|
||||
},
|
||||
"./core/*": {
|
||||
"types": "./dist/src/core/*.d.ts",
|
||||
"import": "./dist/core/*.js",
|
||||
@ -54,6 +59,7 @@
|
||||
"@blocksuite/global": "0.0.0-20230921103931-38d8f07a-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230921103931-38d8f07a-nightly",
|
||||
"jotai": "^2.4.1",
|
||||
"tinykeys": "^2.1.0",
|
||||
"zod": "^3.22.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
5
packages/infra/src/command/README.md
Normal file
5
packages/infra/src/command/README.md
Normal 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)
|
80
packages/infra/src/command/command.ts
Normal file
80
packages/infra/src/command/command.ts
Normal file
@ -0,0 +1,80 @@
|
||||
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:navigation'
|
||||
| 'affine:creation'
|
||||
| 'affine:settings'
|
||||
| 'affine:layout'
|
||||
| 'affine:updates'
|
||||
| 'affine:help'
|
||||
| 'affine:general';
|
||||
|
||||
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) | ReactNode | (() => ReactNode);
|
||||
icon: React.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?: ReactNode | string;
|
||||
readonly icon?: React.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() {
|
||||
const label = options.label;
|
||||
return typeof label === 'function' ? label?.() : label;
|
||||
},
|
||||
keyBinding:
|
||||
typeof options.keyBinding === 'string'
|
||||
? { binding: options.keyBinding }
|
||||
: options.keyBinding,
|
||||
};
|
||||
}
|
2
packages/infra/src/command/index.ts
Normal file
2
packages/infra/src/command/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './command';
|
||||
export * from './registry';
|
64
packages/infra/src/command/registry.ts
Normal file
64
packages/infra/src/command/registry.ts
Normal file
@ -0,0 +1,64 @@
|
||||
// @ts-expect-error upstream type is wrong
|
||||
import { tinykeys } from 'tinykeys';
|
||||
|
||||
import {
|
||||
type AffineCommand,
|
||||
type AffineCommandOptions,
|
||||
createAffineCommand,
|
||||
} from './command';
|
||||
|
||||
export const AffineCommandRegistry = new (class {
|
||||
readonly commands: Map<string, AffineCommand> = new Map();
|
||||
|
||||
register(options: AffineCommandOptions) {
|
||||
if (this.commands.has(options.id)) {
|
||||
console.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);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.debug(`Registered command ${command.id}`);
|
||||
return () => {
|
||||
unsubKb?.();
|
||||
this.commands.delete(command.id);
|
||||
console.debug(`Unregistered command ${command.id}`);
|
||||
};
|
||||
}
|
||||
|
||||
get(id: string): AffineCommand | undefined {
|
||||
if (!this.commands.has(id)) {
|
||||
console.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);
|
||||
}
|
@ -14,6 +14,7 @@ export default defineConfig({
|
||||
blocksuite: resolve(root, 'src/blocksuite/index.ts'),
|
||||
index: resolve(root, 'src/index.ts'),
|
||||
atom: resolve(root, 'src/atom.ts'),
|
||||
command: resolve(root, 'src/command/index.ts'),
|
||||
type: resolve(root, 'src/type.ts'),
|
||||
'core/event-emitter': resolve(root, 'src/core/event-emitter.ts'),
|
||||
'preload/electron': resolve(root, 'src/preload/electron.ts'),
|
||||
|
@ -13,6 +13,20 @@ const openQuickSearchByShortcut = async (page: Page) => {
|
||||
await page.waitForTimeout(500);
|
||||
};
|
||||
|
||||
const keyboardDownAndSelect = async (page: Page, label: string) => {
|
||||
await page.keyboard.press('ArrowDown');
|
||||
if (
|
||||
(await page
|
||||
.locator('[cmdk-item][data-selected] [data-testid="cmdk-label"]')
|
||||
.innerText()) !== label
|
||||
) {
|
||||
await keyboardDownAndSelect(page, label);
|
||||
} else {
|
||||
await page.pause();
|
||||
await page.keyboard.press('Enter');
|
||||
}
|
||||
};
|
||||
|
||||
async function assertTitle(page: Page, text: string) {
|
||||
const edgeless = page.locator('affine-edgeless-page');
|
||||
if (!edgeless) {
|
||||
@ -23,7 +37,9 @@ async function assertTitle(page: Page, text: string) {
|
||||
}
|
||||
|
||||
async function assertResultList(page: Page, texts: string[]) {
|
||||
const actual = await page.locator('[cmdk-item]').allInnerTexts();
|
||||
const actual = await page
|
||||
.locator('[cmdk-item] [data-testid=cmdk-label]')
|
||||
.allInnerTexts();
|
||||
expect(actual).toEqual(texts);
|
||||
}
|
||||
|
||||
@ -44,29 +60,21 @@ test('Click slider bar button', async ({ page }) => {
|
||||
'[data-testid=slider-bar-quick-search-button]'
|
||||
);
|
||||
await quickSearchButton.click();
|
||||
const quickSearch = page.locator('[data-testid=quickSearch]');
|
||||
const quickSearch = page.locator('[data-testid=cmdk-quick-search]');
|
||||
await expect(quickSearch).toBeVisible();
|
||||
});
|
||||
|
||||
test('Click arrowDown icon after title', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitForEditorLoad(page);
|
||||
await clickNewPageButton(page);
|
||||
const quickSearchButton = page.locator(
|
||||
'[data-testid=slider-bar-quick-search-button]'
|
||||
);
|
||||
await quickSearchButton.click();
|
||||
const quickSearch = page.locator('[data-testid=quickSearch]');
|
||||
await expect(quickSearch).toBeVisible();
|
||||
});
|
||||
|
||||
test('Press the shortcut key cmd+k', async ({ page }) => {
|
||||
test('Press the shortcut key cmd+k and close with esc', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitForEditorLoad(page);
|
||||
await clickNewPageButton(page);
|
||||
await openQuickSearchByShortcut(page);
|
||||
const quickSearch = page.locator('[data-testid=quickSearch]');
|
||||
const quickSearch = page.locator('[data-testid=cmdk-quick-search]');
|
||||
await expect(quickSearch).toBeVisible();
|
||||
|
||||
// press esc to close quick search
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(quickSearch).toBeVisible({ visible: false });
|
||||
});
|
||||
|
||||
test('Create a new page without keyword', async ({ page }) => {
|
||||
@ -74,7 +82,7 @@ test('Create a new page without keyword', async ({ page }) => {
|
||||
await waitForEditorLoad(page);
|
||||
await clickNewPageButton(page);
|
||||
await openQuickSearchByShortcut(page);
|
||||
const addNewPage = page.locator('[data-testid=quick-search-add-new-page]');
|
||||
const addNewPage = page.locator('[cmdk-item] >> text=New Page');
|
||||
await addNewPage.click();
|
||||
await page.waitForTimeout(300);
|
||||
await assertTitle(page, '');
|
||||
@ -86,7 +94,9 @@ test('Create a new page with keyword', async ({ page }) => {
|
||||
await clickNewPageButton(page);
|
||||
await openQuickSearchByShortcut(page);
|
||||
await page.keyboard.insertText('test123456');
|
||||
const addNewPage = page.locator('[data-testid=quick-search-add-new-page]');
|
||||
const addNewPage = page.locator(
|
||||
'[cmdk-item] >> text=Create New Page as: test123456'
|
||||
);
|
||||
await addNewPage.click();
|
||||
await page.waitForTimeout(300);
|
||||
await assertTitle(page, 'test123456');
|
||||
@ -110,7 +120,9 @@ test('Create a new page and search this page', async ({ page }) => {
|
||||
// input title and create new page
|
||||
await page.keyboard.insertText('test123456');
|
||||
await page.waitForTimeout(300);
|
||||
const addNewPage = page.locator('[data-testid=quick-search-add-new-page]');
|
||||
const addNewPage = page.locator(
|
||||
'[cmdk-item] >> text=Create New Page as: test123456'
|
||||
);
|
||||
await addNewPage.click();
|
||||
|
||||
await page.waitForTimeout(300);
|
||||
@ -137,10 +149,10 @@ test('Navigate to the 404 page and try to open quick search', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('http://localhost:8080/404');
|
||||
const notFoundTip = page.getByTestId('not-found');
|
||||
const notFoundTip = page.locator('button >> text=Back to My Content');
|
||||
await expect(notFoundTip).toBeVisible();
|
||||
await openQuickSearchByShortcut(page);
|
||||
const quickSearch = page.locator('[data-testid=quickSearch]');
|
||||
const quickSearch = page.locator('[data-testid=cmdk-quick-search]');
|
||||
await expect(quickSearch).toBeVisible({ visible: false });
|
||||
});
|
||||
|
||||
@ -177,18 +189,18 @@ test('Focus title after creating a new page', async ({ page }) => {
|
||||
await waitForEditorLoad(page);
|
||||
await clickNewPageButton(page);
|
||||
await openQuickSearchByShortcut(page);
|
||||
const addNewPage = page.locator('[data-testid=quick-search-add-new-page]');
|
||||
const addNewPage = page.locator('[cmdk-item] >> text=New Page');
|
||||
await addNewPage.click();
|
||||
await titleIsFocused(page);
|
||||
});
|
||||
|
||||
test('Not show navigation path if page is not a subpage or current page is not in editor', async ({
|
||||
page,
|
||||
}) => {
|
||||
test('can use keyboard down to select goto setting', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitForEditorLoad(page);
|
||||
await openQuickSearchByShortcut(page);
|
||||
expect(await page.getByTestId('navigation-path').count()).toBe(0);
|
||||
await keyboardDownAndSelect(page, 'Go to Settings');
|
||||
|
||||
await expect(page.getByTestId('setting-modal')).toBeVisible();
|
||||
});
|
||||
|
||||
test('assert the recent browse pages are on the recent list', async ({
|
||||
@ -209,7 +221,7 @@ test('assert the recent browse pages are on the recent list', async ({
|
||||
|
||||
// create second page
|
||||
await openQuickSearchByShortcut(page);
|
||||
const addNewPage = page.getByTestId('quick-search-add-new-page');
|
||||
const addNewPage = page.locator('[cmdk-item] >> text=New Page');
|
||||
await addNewPage.click();
|
||||
{
|
||||
const title = getBlockSuiteEditorTitle(page);
|
||||
@ -234,7 +246,9 @@ test('assert the recent browse pages are on the recent list', async ({
|
||||
await page.waitForTimeout(200);
|
||||
{
|
||||
// check does all 3 pages exists on recent page list
|
||||
const quickSearchItems = page.locator('[cmdk-item]');
|
||||
const quickSearchItems = page.locator(
|
||||
'[cmdk-item] [data-testid="cmdk-label"]'
|
||||
);
|
||||
expect(await quickSearchItems.nth(0).textContent()).toBe('battlekot');
|
||||
expect(await quickSearchItems.nth(1).textContent()).toBe('theliquidhorse');
|
||||
expect(await quickSearchItems.nth(2).textContent()).toBe('sgtokidoki');
|
||||
@ -245,7 +259,7 @@ test('assert the recent browse pages are on the recent list', async ({
|
||||
await waitForEditorLoad(page);
|
||||
await openQuickSearchByShortcut(page);
|
||||
{
|
||||
const addNewPage = page.getByTestId('quick-search-add-new-page');
|
||||
const addNewPage = page.locator('[cmdk-item] >> text=New Page');
|
||||
await addNewPage.click();
|
||||
}
|
||||
await page.waitForTimeout(200);
|
||||
@ -258,7 +272,9 @@ test('assert the recent browse pages are on the recent list', async ({
|
||||
await page.waitForTimeout(1000);
|
||||
await openQuickSearchByShortcut(page);
|
||||
{
|
||||
const quickSearchItems = page.locator('[cmdk-item]');
|
||||
const quickSearchItems = page.locator(
|
||||
'[cmdk-item] [data-testid="cmdk-label"]'
|
||||
);
|
||||
expect(await quickSearchItems.nth(0).textContent()).toBe(
|
||||
'affine is the best'
|
||||
);
|
||||
|
@ -159,6 +159,9 @@
|
||||
{
|
||||
"path": "./packages/y-indexeddb"
|
||||
},
|
||||
{
|
||||
"path": "./packages/cmdk"
|
||||
},
|
||||
// Tests
|
||||
{
|
||||
"path": "./tests/kit"
|
||||
|
201
yarn.lock
201
yarn.lock
@ -184,6 +184,15 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@affine/cmdk@workspace:packages/cmdk":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@affine/cmdk@workspace:packages/cmdk"
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@affine/component@workspace:*, @affine/component@workspace:packages/component":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@affine/component@workspace:packages/component"
|
||||
@ -310,7 +319,6 @@ __metadata:
|
||||
"@types/lodash-es": ^4.17.9
|
||||
"@types/webpack-env": ^1.18.1
|
||||
async-call-rpc: ^6.3.1
|
||||
cmdk: ^0.2.0
|
||||
copy-webpack-plugin: ^11.0.0
|
||||
css-loader: ^6.8.1
|
||||
css-spring: ^4.1.0
|
||||
@ -428,6 +436,7 @@ __metadata:
|
||||
lodash-es: ^4.17.21
|
||||
nanoid: ^4.0.2
|
||||
rxjs: ^7.8.1
|
||||
tinykeys: ^2.1.0
|
||||
ts-node: ^10.9.1
|
||||
undici: ^5.23.0
|
||||
uuid: ^9.0.0
|
||||
@ -9101,32 +9110,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-dialog@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@radix-ui/react-dialog@npm:1.0.0"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.13.10
|
||||
"@radix-ui/primitive": 1.0.0
|
||||
"@radix-ui/react-compose-refs": 1.0.0
|
||||
"@radix-ui/react-context": 1.0.0
|
||||
"@radix-ui/react-dismissable-layer": 1.0.0
|
||||
"@radix-ui/react-focus-guards": 1.0.0
|
||||
"@radix-ui/react-focus-scope": 1.0.0
|
||||
"@radix-ui/react-id": 1.0.0
|
||||
"@radix-ui/react-portal": 1.0.0
|
||||
"@radix-ui/react-presence": 1.0.0
|
||||
"@radix-ui/react-primitive": 1.0.0
|
||||
"@radix-ui/react-slot": 1.0.0
|
||||
"@radix-ui/react-use-controllable-state": 1.0.0
|
||||
aria-hidden: ^1.1.1
|
||||
react-remove-scroll: 2.5.4
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
checksum: 32a1ab36a483ceae40b804f116611577b976e1781f33c8d74093f4132fd230b325eadd0306efd77a628d604b61c3a592d4c720a5b7e69e74dbfb56ce85b2c6fd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-dialog@npm:^1.0.4":
|
||||
version: 1.0.4
|
||||
resolution: "@radix-ui/react-dialog@npm:1.0.4"
|
||||
@ -9186,23 +9169,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-dismissable-layer@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@radix-ui/react-dismissable-layer@npm:1.0.0"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.13.10
|
||||
"@radix-ui/primitive": 1.0.0
|
||||
"@radix-ui/react-compose-refs": 1.0.0
|
||||
"@radix-ui/react-primitive": 1.0.0
|
||||
"@radix-ui/react-use-callback-ref": 1.0.0
|
||||
"@radix-ui/react-use-escape-keydown": 1.0.0
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
checksum: c5af6445ea3f584bad1fb3ed01703c2d4a889d9b99b23bc2c821a2c166fdbb75cfdd1e4870d3f958d9ac78f5e1b8006f762317cba765839592e5c5af1183a7a7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-dismissable-layer@npm:1.0.4":
|
||||
version: 1.0.4
|
||||
resolution: "@radix-ui/react-dismissable-layer@npm:1.0.4"
|
||||
@ -9253,17 +9219,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-focus-guards@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@radix-ui/react-focus-guards@npm:1.0.0"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.13.10
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
checksum: 8c714e8caa6032f5402eecb0323addd7456d3496946dbad1b9ee8ebf5845943876945e7af9bca179e9f8ffe5100e61cb4ba54a185873949125c310c406be5aa4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-focus-guards@npm:1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "@radix-ui/react-focus-guards@npm:1.0.1"
|
||||
@ -9279,21 +9234,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-focus-scope@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@radix-ui/react-focus-scope@npm:1.0.0"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.13.10
|
||||
"@radix-ui/react-compose-refs": 1.0.0
|
||||
"@radix-ui/react-primitive": 1.0.0
|
||||
"@radix-ui/react-use-callback-ref": 1.0.0
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
checksum: 2ee0b9a2d1905aba25d0225c203d20745fd798aa61d65f55c6041169701d47d6b801d4392a57d94968f0d8ef3410eb79fcab19c9c80f03e9b9f6b24f6f997f98
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-focus-scope@npm:1.0.3":
|
||||
version: 1.0.3
|
||||
resolution: "@radix-ui/react-focus-scope@npm:1.0.3"
|
||||
@ -9316,18 +9256,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-id@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@radix-ui/react-id@npm:1.0.0"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.13.10
|
||||
"@radix-ui/react-use-layout-effect": 1.0.0
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
checksum: ba323cedd6a6df6f6e51ed1f7f7747988ce432b47fd94d860f962b14b342dcf049eae33f8ad0b72fd7df6329a7375542921132271fba64ab0a271c93f09c48d1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-id@npm:1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "@radix-ui/react-id@npm:1.0.1"
|
||||
@ -9444,19 +9372,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-portal@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@radix-ui/react-portal@npm:1.0.0"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.13.10
|
||||
"@radix-ui/react-primitive": 1.0.0
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
checksum: dfb194b2df32830db2daf01569176c6e4cf3af2c6e393ece60532543902acf13a6629f9a45003902c99df195c2249bc56d4f4425a5fed2897555df9bdf01efa0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-portal@npm:1.0.3":
|
||||
version: 1.0.3
|
||||
resolution: "@radix-ui/react-portal@npm:1.0.3"
|
||||
@ -9512,19 +9427,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-primitive@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@radix-ui/react-primitive@npm:1.0.0"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.13.10
|
||||
"@radix-ui/react-slot": 1.0.0
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
checksum: fb3fe8c8c5a57995716cce4d7e9039e474c09ba5d714994419ad4940bc954da670f1188813cc931f189b23d9bd5a67adf7087bf44fe1d4272b4a334a3514d38b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-primitive@npm:1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "@radix-ui/react-primitive@npm:1.0.1"
|
||||
@ -9724,18 +9626,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-slot@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@radix-ui/react-slot@npm:1.0.0"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.13.10
|
||||
"@radix-ui/react-compose-refs": 1.0.0
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
checksum: 60c0190ebdca21785b4f8b58a0c52717600c98953fc49da9580870519c60f52d5cf873dffa05446f4bb539066326ccec0827f4ca252b02ec4ff1a4ae203f59d7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-slot@npm:1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "@radix-ui/react-slot@npm:1.0.1"
|
||||
@ -9926,18 +9816,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-use-controllable-state@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@radix-ui/react-use-controllable-state@npm:1.0.0"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.13.10
|
||||
"@radix-ui/react-use-callback-ref": 1.0.0
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
checksum: 35f1e714bbe3fc9f5362a133339dd890fb96edb79b63168a99403c65dd5f2b63910e0c690255838029086719e31360fa92544a55bc902cfed4442bb3b55822e2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-use-controllable-state@npm:1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "@radix-ui/react-use-controllable-state@npm:1.0.1"
|
||||
@ -9954,18 +9832,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-use-escape-keydown@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@radix-ui/react-use-escape-keydown@npm:1.0.0"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.13.10
|
||||
"@radix-ui/react-use-callback-ref": 1.0.0
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
checksum: a6728d40e059fdf2da0703cde9afb10defbcd951d6e1dc48522f33f9399f5aa0514751d9e25847bdcc57328b9d745a3baa36baf9f6af6453a5c894dfcbd40352
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-use-escape-keydown@npm:1.0.3":
|
||||
version: 1.0.3
|
||||
resolution: "@radix-ui/react-use-escape-keydown@npm:1.0.3"
|
||||
@ -12608,6 +12474,7 @@ __metadata:
|
||||
electron: "link:../../apps/electron/node_modules/electron"
|
||||
jotai: ^2.4.1
|
||||
react: ^18.2.0
|
||||
tinykeys: ^2.1.0
|
||||
vite: ^4.4.9
|
||||
vite-plugin-dts: 3.5.3
|
||||
yjs: ^13.6.8
|
||||
@ -17113,19 +16980,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cmdk@npm:^0.2.0":
|
||||
version: 0.2.0
|
||||
resolution: "cmdk@npm:0.2.0"
|
||||
dependencies:
|
||||
"@radix-ui/react-dialog": 1.0.0
|
||||
command-score: 0.1.2
|
||||
peerDependencies:
|
||||
react: ^18.0.0
|
||||
react-dom: ^18.0.0
|
||||
checksum: b81add6daebb192e0ab79bd776aa22c75a9aa5c8ca3c40d6c99cbe3ab4f276bf580eb49d1e568126b6af1e72c0012b53ec260c241a594baf782a80b866a0d17e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"co@npm:^4.6.0":
|
||||
version: 4.6.0
|
||||
resolution: "co@npm:4.6.0"
|
||||
@ -17247,13 +17101,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"command-score@npm:0.1.2":
|
||||
version: 0.1.2
|
||||
resolution: "command-score@npm:0.1.2"
|
||||
checksum: b733fd552d7e569070da3d474b1ed5f54785fdf3dd61670002e0a00b2eff1a547c2b6d3af3683c012f4f39c6455f9e7ee5e9997a79c08048ec37ec2195d3df08
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"commander@npm:11.0.0":
|
||||
version: 11.0.0
|
||||
resolution: "commander@npm:11.0.0"
|
||||
@ -29806,25 +29653,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-remove-scroll@npm:2.5.4":
|
||||
version: 2.5.4
|
||||
resolution: "react-remove-scroll@npm:2.5.4"
|
||||
dependencies:
|
||||
react-remove-scroll-bar: ^2.3.3
|
||||
react-style-singleton: ^2.2.1
|
||||
tslib: ^2.1.0
|
||||
use-callback-ref: ^1.3.0
|
||||
use-sidecar: ^1.1.2
|
||||
peerDependencies:
|
||||
"@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
checksum: 01b0f65542a4c8803ee748b4e6cf2adad66d034e15fb72e8455773b0d7b178ec806b3194d74f412db7064670c45552cc666c04e9fb3b5d466dce5fb48e634825
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-remove-scroll@npm:2.5.5":
|
||||
version: 2.5.5
|
||||
resolution: "react-remove-scroll@npm:2.5.5"
|
||||
@ -32829,6 +32657,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tinykeys@npm:^2.1.0":
|
||||
version: 2.1.0
|
||||
resolution: "tinykeys@npm:2.1.0"
|
||||
checksum: 49e8f7cf09c29372758f434d072429ed5a93cd3148d218af9938ccbbac4e15a2697c93a34dcfe7138a11e569a2145b35709947e5d2b74e48a8e17c575b8beb7f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tinypool@npm:^0.6.0":
|
||||
version: 0.6.0
|
||||
resolution: "tinypool@npm:0.6.0"
|
||||
|
Loading…
Reference in New Issue
Block a user