mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-06 03:49:56 +08:00
feat: new CMD-K (#4408)
This commit is contained in:
@@ -12,7 +12,8 @@
|
|||||||
"./app": "./src/app.tsx",
|
"./app": "./src/app.tsx",
|
||||||
"./router": "./src/router.ts",
|
"./router": "./src/router.ts",
|
||||||
"./bootstrap/setup": "./src/bootstrap/setup.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": {
|
"dependencies": {
|
||||||
"@affine-test/fixtures": "workspace:*",
|
"@affine-test/fixtures": "workspace:*",
|
||||||
@@ -41,7 +42,6 @@
|
|||||||
"@react-hookz/web": "^23.1.0",
|
"@react-hookz/web": "^23.1.0",
|
||||||
"@toeverything/components": "^0.0.43",
|
"@toeverything/components": "^0.0.43",
|
||||||
"async-call-rpc": "^6.3.1",
|
"async-call-rpc": "^6.3.1",
|
||||||
"cmdk": "^0.2.0",
|
|
||||||
"css-spring": "^4.1.0",
|
"css-spring": "^4.1.0",
|
||||||
"cssnano": "^6.0.1",
|
"cssnano": "^6.0.1",
|
||||||
"graphql": "^16.8.0",
|
"graphql": "^16.8.0",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { describe, expect, test } from 'vitest';
|
|||||||
import {
|
import {
|
||||||
pageSettingFamily,
|
pageSettingFamily,
|
||||||
pageSettingsAtom,
|
pageSettingsAtom,
|
||||||
recentPageSettingsAtom,
|
recentPageIdsBaseAtom,
|
||||||
} from '../index';
|
} from '../index';
|
||||||
|
|
||||||
describe('page mode atom', () => {
|
describe('page mode atom', () => {
|
||||||
@@ -26,20 +26,12 @@ describe('page mode atom', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(store.get(recentPageSettingsAtom)).toEqual([
|
expect(store.get(recentPageIdsBaseAtom)).toEqual(['page0']);
|
||||||
{
|
|
||||||
id: 'page0',
|
|
||||||
mode: 'page',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const page1SettingAtom = pageSettingFamily('page1');
|
const page1SettingAtom = pageSettingFamily('page1');
|
||||||
store.set(page1SettingAtom, {
|
store.set(page1SettingAtom, {
|
||||||
mode: 'edgeless',
|
mode: 'edgeless',
|
||||||
});
|
});
|
||||||
expect(store.get(recentPageSettingsAtom)).toEqual([
|
expect(store.get(recentPageIdsBaseAtom)).toEqual(['page1', 'page0']);
|
||||||
{ id: 'page1', mode: 'edgeless' },
|
|
||||||
{ id: 'page0', mode: 'page' },
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -43,10 +43,6 @@ type PageLocalSetting = {
|
|||||||
mode: PageMode;
|
mode: PageMode;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PartialPageLocalSettingWithPageId = Partial<PageLocalSetting> & {
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const pageSettingsBaseAtom = atomWithStorage(
|
const pageSettingsBaseAtom = atomWithStorage(
|
||||||
'pageSettings',
|
'pageSettings',
|
||||||
{} as Record<string, PageLocalSetting>
|
{} as Record<string, PageLocalSetting>
|
||||||
@@ -55,22 +51,11 @@ const pageSettingsBaseAtom = atomWithStorage(
|
|||||||
// readonly atom by design
|
// readonly atom by design
|
||||||
export const pageSettingsAtom = atom(get => get(pageSettingsBaseAtom));
|
export const pageSettingsAtom = atom(get => get(pageSettingsBaseAtom));
|
||||||
|
|
||||||
const recentPageSettingsBaseAtom = atomWithStorage<string[]>(
|
export const recentPageIdsBaseAtom = atomWithStorage<string[]>(
|
||||||
'recentPageSettings',
|
'recentPageSettings',
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const recentPageSettingsAtom = atom<PartialPageLocalSettingWithPageId[]>(
|
|
||||||
get => {
|
|
||||||
const recentPageIDs = get(recentPageSettingsBaseAtom);
|
|
||||||
const pageSettings = get(pageSettingsAtom);
|
|
||||||
return recentPageIDs.map(id => ({
|
|
||||||
...pageSettings[id],
|
|
||||||
id,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const defaultPageSetting = {
|
const defaultPageSetting = {
|
||||||
mode: 'page',
|
mode: 'page',
|
||||||
} satisfies PageLocalSetting;
|
} satisfies PageLocalSetting;
|
||||||
@@ -85,7 +70,9 @@ export const pageSettingFamily: AtomFamily<
|
|||||||
...defaultPageSetting,
|
...defaultPageSetting,
|
||||||
},
|
},
|
||||||
(get, set, patch) => {
|
(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
|
// pick 3 recent page ids
|
||||||
return [...new Set([pageId, ...ids]).values()].slice(0, 3);
|
return [...new Set([pageId, ...ids]).values()].slice(0, 3);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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());
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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 { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
|
||||||
import { initEmptyPage } from '@toeverything/infra/blocksuite';
|
import { initEmptyPage } from '@toeverything/infra/blocksuite';
|
||||||
import { useAtomValue, useSetAtom } from 'jotai';
|
import { useAtomValue, useSetAtom } from 'jotai';
|
||||||
import { useCallback } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { pageSettingsAtom, setPageModeAtom } from '../../../atoms';
|
import { pageSettingsAtom, setPageModeAtom } from '../../../atoms';
|
||||||
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
||||||
@@ -57,10 +57,17 @@ export const usePageHelper = (blockSuiteWorkspace: BlockSuiteWorkspace) => {
|
|||||||
};
|
};
|
||||||
showImportModal({ workspace: blockSuiteWorkspace, onSuccess });
|
showImportModal({ workspace: blockSuiteWorkspace, onSuccess });
|
||||||
}, [blockSuiteWorkspace, openPage, jumpToSubPath]);
|
}, [blockSuiteWorkspace, openPage, jumpToSubPath]);
|
||||||
return {
|
return useMemo(() => {
|
||||||
createPage: createPageAndOpen,
|
return {
|
||||||
createEdgeless: createEdgelessAndOpen,
|
createPage: createPageAndOpen,
|
||||||
importFile: importFileAndOpen,
|
createEdgeless: createEdgelessAndOpen,
|
||||||
isPreferredEdgeless: isPreferredEdgeless,
|
importFile: importFileAndOpen,
|
||||||
};
|
isPreferredEdgeless: isPreferredEdgeless,
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
createEdgelessAndOpen,
|
||||||
|
createPageAndOpen,
|
||||||
|
importFileAndOpen,
|
||||||
|
isPreferredEdgeless,
|
||||||
|
]);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './main';
|
||||||
|
export * from './modal';
|
||||||
@@ -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)',
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 type { Page } from '@blocksuite/store';
|
||||||
import { useDroppable } from '@dnd-kit/core';
|
import { useDroppable } from '@dnd-kit/core';
|
||||||
import { Menu } from '@toeverything/components/menu';
|
import { Menu } from '@toeverything/components/menu';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import type { HTMLAttributes, ReactElement } from 'react';
|
import type { HTMLAttributes, ReactElement } from 'react';
|
||||||
import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
|
import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ export const RootAppSidebar = ({
|
|||||||
return;
|
return;
|
||||||
}, [onClickNewPage]);
|
}, [onClickNewPage]);
|
||||||
|
|
||||||
const [sidebarOpen, setSidebarOpen] = useAtom(appSidebarOpenAtom);
|
const sidebarOpen = useAtomValue(appSidebarOpenAtom);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (environment.isDesktop) {
|
if (environment.isDesktop) {
|
||||||
window.apis?.ui.handleSidebarVisibilityChange(sidebarOpen).catch(err => {
|
window.apis?.ui.handleSidebarVisibilityChange(sidebarOpen).catch(err => {
|
||||||
@@ -124,17 +124,6 @@ export const RootAppSidebar = ({
|
|||||||
}
|
}
|
||||||
}, [sidebarOpen]);
|
}, [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 [history, setHistory] = useHistoryAtom();
|
||||||
const router = useMemo(() => {
|
const router = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -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 { useBlockSuiteMetaHelper } from '../hooks/affine/use-block-suite-meta-helper';
|
||||||
import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
|
import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
|
||||||
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||||
|
import { useRegisterWorkspaceCommands } from '../hooks/use-register-workspace-commands';
|
||||||
import {
|
import {
|
||||||
AllWorkspaceModals,
|
AllWorkspaceModals,
|
||||||
CurrentWorkspaceModals,
|
CurrentWorkspaceModals,
|
||||||
@@ -61,9 +62,9 @@ import {
|
|||||||
import { pathGenerator } from '../shared';
|
import { pathGenerator } from '../shared';
|
||||||
import { toast } from '../utils';
|
import { toast } from '../utils';
|
||||||
|
|
||||||
const QuickSearchModal = lazy(() =>
|
const CMDKQuickSearchModal = lazy(() =>
|
||||||
import('../components/pure/quick-search-modal').then(module => ({
|
import('../components/pure/cmdk').then(module => ({
|
||||||
default: module.QuickSearchModal,
|
default: module.CMDKQuickSearchModal,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -79,10 +80,9 @@ export const QuickSearch = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QuickSearchModal
|
<CMDKQuickSearchModal
|
||||||
workspace={currentWorkspace}
|
|
||||||
open={openQuickSearchModal}
|
open={openQuickSearchModal}
|
||||||
setOpen={setOpenQuickSearchModalAtom}
|
onOpenChange={setOpenQuickSearchModalAtom}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -141,6 +141,10 @@ export const WorkspaceLayoutInner = ({
|
|||||||
}: PropsWithChildren<WorkspaceLayoutProps>) => {
|
}: PropsWithChildren<WorkspaceLayoutProps>) => {
|
||||||
const [currentWorkspace] = useCurrentWorkspace();
|
const [currentWorkspace] = useCurrentWorkspace();
|
||||||
const { openPage } = useNavigateHelper();
|
const { openPage } = useNavigateHelper();
|
||||||
|
const pageHelper = usePageHelper(currentWorkspace.blockSuiteWorkspace);
|
||||||
|
const t = useAFFiNEI18N();
|
||||||
|
|
||||||
|
useRegisterWorkspaceCommands();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// hotfix for blockVersions
|
// hotfix for blockVersions
|
||||||
@@ -164,15 +168,13 @@ export const WorkspaceLayoutInner = ({
|
|||||||
|
|
||||||
usePassiveWorkspaceEffect(currentWorkspace.blockSuiteWorkspace);
|
usePassiveWorkspaceEffect(currentWorkspace.blockSuiteWorkspace);
|
||||||
|
|
||||||
const helper = usePageHelper(currentWorkspace.blockSuiteWorkspace);
|
|
||||||
|
|
||||||
const handleCreatePage = useCallback(() => {
|
const handleCreatePage = useCallback(() => {
|
||||||
const id = nanoid();
|
const id = nanoid();
|
||||||
helper.createPage(id);
|
pageHelper.createPage(id);
|
||||||
const page = currentWorkspace.blockSuiteWorkspace.getPage(id);
|
const page = currentWorkspace.blockSuiteWorkspace.getPage(id);
|
||||||
assertExists(page);
|
assertExists(page);
|
||||||
return page;
|
return page;
|
||||||
}, [currentWorkspace.blockSuiteWorkspace, helper]);
|
}, [currentWorkspace.blockSuiteWorkspace, pageHelper]);
|
||||||
|
|
||||||
const [, setOpenQuickSearchModalAtom] = useAtom(openQuickSearchModalAtom);
|
const [, setOpenQuickSearchModalAtom] = useAtom(openQuickSearchModalAtom);
|
||||||
const handleOpenQuickSearchModal = useCallback(() => {
|
const handleOpenQuickSearchModal = useCallback(() => {
|
||||||
@@ -205,7 +207,6 @@ export const WorkspaceLayoutInner = ({
|
|||||||
const { removeToTrash: moveToTrash } = useBlockSuiteMetaHelper(
|
const { removeToTrash: moveToTrash } = useBlockSuiteMetaHelper(
|
||||||
currentWorkspace.blockSuiteWorkspace
|
currentWorkspace.blockSuiteWorkspace
|
||||||
);
|
);
|
||||||
const t = useAFFiNEI18N();
|
|
||||||
|
|
||||||
const handleDragEnd = useCallback(
|
const handleDragEnd = useCallback(
|
||||||
(e: DragEndEvent) => {
|
(e: DragEndEvent) => {
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
"jotai": "^2.4.1",
|
"jotai": "^2.4.1",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
|
"tinykeys": "^2.1.0",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"undici": "^5.23.0",
|
"undici": "^5.23.0",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export const config = () => {
|
|||||||
'@toeverything/plugin-infra',
|
'@toeverything/plugin-infra',
|
||||||
'yjs',
|
'yjs',
|
||||||
'semver',
|
'semver',
|
||||||
|
'tinykeys',
|
||||||
],
|
],
|
||||||
define: define,
|
define: define,
|
||||||
format: 'cjs',
|
format: 'cjs',
|
||||||
|
|||||||
@@ -36,6 +36,13 @@ export default {
|
|||||||
async viteFinal(config, _options) {
|
async viteFinal(config, _options) {
|
||||||
return mergeConfig(config, {
|
return mergeConfig(config, {
|
||||||
assetsInclude: ['**/*.md'],
|
assetsInclude: ['**/*.md'],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@toeverything/infra': fileURLToPath(
|
||||||
|
new URL('../../../packages/infra/src', import.meta.url)
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
vanillaExtractPlugin(),
|
vanillaExtractPlugin(),
|
||||||
tsconfigPaths({
|
tsconfigPaths({
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Empty } from '@affine/component';
|
import { Empty, toast } from '@affine/component';
|
||||||
import { toast } from '@affine/component';
|
|
||||||
import type { OperationCellProps } from '@affine/component/page-list';
|
import type { OperationCellProps } from '@affine/component/page-list';
|
||||||
import { PageListTrashView } from '@affine/component/page-list';
|
import {
|
||||||
import { PageList } from '@affine/component/page-list';
|
NewPageButton,
|
||||||
import { NewPageButton } from '@affine/component/page-list';
|
OperationCell,
|
||||||
import { OperationCell } from '@affine/component/page-list';
|
PageList,
|
||||||
|
PageListTrashView,
|
||||||
|
} from '@affine/component/page-list';
|
||||||
import { PageIcon } from '@blocksuite/icons';
|
import { PageIcon } from '@blocksuite/icons';
|
||||||
import { expect } from '@storybook/jest';
|
import { expect } from '@storybook/jest';
|
||||||
import type { Meta, StoryFn } from '@storybook/react';
|
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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# copied directly from https://github.com/pacocoursey/cmdk
|
||||||
|
|
||||||
|
will remove after a new CMDK version is published to npm
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"include": ["./src"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"noEmit": false,
|
||||||
|
"outDir": "lib"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
blockSuiteEditorHeaderStyle,
|
blockSuiteEditorHeaderStyle,
|
||||||
blockSuiteEditorStyle,
|
blockSuiteEditorStyle,
|
||||||
} from './index.css';
|
} from './index.css';
|
||||||
|
import { useRegisterBlocksuiteEditorCommands } from './use-register-blocksuite-editor-commands';
|
||||||
|
|
||||||
export type EditorProps = {
|
export type EditorProps = {
|
||||||
page: Page;
|
page: Page;
|
||||||
@@ -164,6 +165,7 @@ export const BlockSuiteFallback = memo(function BlockSuiteFallback() {
|
|||||||
export const BlockSuiteEditor = memo(function BlockSuiteEditor(
|
export const BlockSuiteEditor = memo(function BlockSuiteEditor(
|
||||||
props: EditorProps & ErrorBoundaryProps
|
props: EditorProps & ErrorBoundaryProps
|
||||||
): ReactElement {
|
): ReactElement {
|
||||||
|
useRegisterBlocksuiteEditorCommands();
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary
|
<ErrorBoundary
|
||||||
fallbackRender={useCallback(
|
fallbackRender={useCallback(
|
||||||
|
|||||||
+35
@@ -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 './styles';
|
||||||
export * from './type';
|
export * from './type';
|
||||||
export * from './use-collection-manager';
|
export * from './use-collection-manager';
|
||||||
|
export * from './utils';
|
||||||
export * from './view';
|
export * from './view';
|
||||||
|
|||||||
@@ -584,5 +584,30 @@
|
|||||||
"com.affine.workspaceList.addWorkspace.create": "Create Workspace",
|
"com.affine.workspaceList.addWorkspace.create": "Create Workspace",
|
||||||
"com.affine.workspaceList.workspaceListType.local": "Local Storage",
|
"com.affine.workspaceList.workspaceListType.local": "Local Storage",
|
||||||
"com.affine.workspaceList.workspaceListType.cloud": "Cloud Sync",
|
"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",
|
"import": "./dist/blocksuite.js",
|
||||||
"require": "./dist/blocksuite.cjs"
|
"require": "./dist/blocksuite.cjs"
|
||||||
},
|
},
|
||||||
|
"./command": {
|
||||||
|
"types": "./dist/src/command/index.d.ts",
|
||||||
|
"import": "./dist/command.js",
|
||||||
|
"require": "./dist/command.cjs"
|
||||||
|
},
|
||||||
"./core/*": {
|
"./core/*": {
|
||||||
"types": "./dist/src/core/*.d.ts",
|
"types": "./dist/src/core/*.d.ts",
|
||||||
"import": "./dist/core/*.js",
|
"import": "./dist/core/*.js",
|
||||||
@@ -54,6 +59,7 @@
|
|||||||
"@blocksuite/global": "0.0.0-20230921103931-38d8f07a-nightly",
|
"@blocksuite/global": "0.0.0-20230921103931-38d8f07a-nightly",
|
||||||
"@blocksuite/store": "0.0.0-20230921103931-38d8f07a-nightly",
|
"@blocksuite/store": "0.0.0-20230921103931-38d8f07a-nightly",
|
||||||
"jotai": "^2.4.1",
|
"jotai": "^2.4.1",
|
||||||
|
"tinykeys": "^2.1.0",
|
||||||
"zod": "^3.22.2"
|
"zod": "^3.22.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './command';
|
||||||
|
export * from './registry';
|
||||||
@@ -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'),
|
blocksuite: resolve(root, 'src/blocksuite/index.ts'),
|
||||||
index: resolve(root, 'src/index.ts'),
|
index: resolve(root, 'src/index.ts'),
|
||||||
atom: resolve(root, 'src/atom.ts'),
|
atom: resolve(root, 'src/atom.ts'),
|
||||||
|
command: resolve(root, 'src/command/index.ts'),
|
||||||
type: resolve(root, 'src/type.ts'),
|
type: resolve(root, 'src/type.ts'),
|
||||||
'core/event-emitter': resolve(root, 'src/core/event-emitter.ts'),
|
'core/event-emitter': resolve(root, 'src/core/event-emitter.ts'),
|
||||||
'preload/electron': resolve(root, 'src/preload/electron.ts'),
|
'preload/electron': resolve(root, 'src/preload/electron.ts'),
|
||||||
|
|||||||
@@ -13,6 +13,20 @@ const openQuickSearchByShortcut = async (page: Page) => {
|
|||||||
await page.waitForTimeout(500);
|
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) {
|
async function assertTitle(page: Page, text: string) {
|
||||||
const edgeless = page.locator('affine-edgeless-page');
|
const edgeless = page.locator('affine-edgeless-page');
|
||||||
if (!edgeless) {
|
if (!edgeless) {
|
||||||
@@ -23,7 +37,9 @@ async function assertTitle(page: Page, text: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function assertResultList(page: Page, texts: 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);
|
expect(actual).toEqual(texts);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,29 +60,21 @@ test('Click slider bar button', async ({ page }) => {
|
|||||||
'[data-testid=slider-bar-quick-search-button]'
|
'[data-testid=slider-bar-quick-search-button]'
|
||||||
);
|
);
|
||||||
await quickSearchButton.click();
|
await quickSearchButton.click();
|
||||||
const quickSearch = page.locator('[data-testid=quickSearch]');
|
const quickSearch = page.locator('[data-testid=cmdk-quick-search]');
|
||||||
await expect(quickSearch).toBeVisible();
|
await expect(quickSearch).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Click arrowDown icon after title', async ({ page }) => {
|
test('Press the shortcut key cmd+k and close with esc', 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 }) => {
|
|
||||||
await openHomePage(page);
|
await openHomePage(page);
|
||||||
await waitForEditorLoad(page);
|
await waitForEditorLoad(page);
|
||||||
await clickNewPageButton(page);
|
await clickNewPageButton(page);
|
||||||
await openQuickSearchByShortcut(page);
|
await openQuickSearchByShortcut(page);
|
||||||
const quickSearch = page.locator('[data-testid=quickSearch]');
|
const quickSearch = page.locator('[data-testid=cmdk-quick-search]');
|
||||||
await expect(quickSearch).toBeVisible();
|
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 }) => {
|
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 waitForEditorLoad(page);
|
||||||
await clickNewPageButton(page);
|
await clickNewPageButton(page);
|
||||||
await openQuickSearchByShortcut(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 addNewPage.click();
|
||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(300);
|
||||||
await assertTitle(page, '');
|
await assertTitle(page, '');
|
||||||
@@ -86,7 +94,9 @@ test('Create a new page with keyword', async ({ page }) => {
|
|||||||
await clickNewPageButton(page);
|
await clickNewPageButton(page);
|
||||||
await openQuickSearchByShortcut(page);
|
await openQuickSearchByShortcut(page);
|
||||||
await page.keyboard.insertText('test123456');
|
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 addNewPage.click();
|
||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(300);
|
||||||
await assertTitle(page, 'test123456');
|
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
|
// input title and create new page
|
||||||
await page.keyboard.insertText('test123456');
|
await page.keyboard.insertText('test123456');
|
||||||
await page.waitForTimeout(300);
|
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 addNewPage.click();
|
||||||
|
|
||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(300);
|
||||||
@@ -137,10 +149,10 @@ test('Navigate to the 404 page and try to open quick search', async ({
|
|||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
await page.goto('http://localhost:8080/404');
|
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 expect(notFoundTip).toBeVisible();
|
||||||
await openQuickSearchByShortcut(page);
|
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 });
|
await expect(quickSearch).toBeVisible({ visible: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -177,18 +189,18 @@ test('Focus title after creating a new page', async ({ page }) => {
|
|||||||
await waitForEditorLoad(page);
|
await waitForEditorLoad(page);
|
||||||
await clickNewPageButton(page);
|
await clickNewPageButton(page);
|
||||||
await openQuickSearchByShortcut(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 addNewPage.click();
|
||||||
await titleIsFocused(page);
|
await titleIsFocused(page);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Not show navigation path if page is not a subpage or current page is not in editor', async ({
|
test('can use keyboard down to select goto setting', async ({ page }) => {
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
await openHomePage(page);
|
await openHomePage(page);
|
||||||
await waitForEditorLoad(page);
|
await waitForEditorLoad(page);
|
||||||
await openQuickSearchByShortcut(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 ({
|
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
|
// create second page
|
||||||
await openQuickSearchByShortcut(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 addNewPage.click();
|
||||||
{
|
{
|
||||||
const title = getBlockSuiteEditorTitle(page);
|
const title = getBlockSuiteEditorTitle(page);
|
||||||
@@ -234,7 +246,9 @@ test('assert the recent browse pages are on the recent list', async ({
|
|||||||
await page.waitForTimeout(200);
|
await page.waitForTimeout(200);
|
||||||
{
|
{
|
||||||
// check does all 3 pages exists on recent page list
|
// 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(0).textContent()).toBe('battlekot');
|
||||||
expect(await quickSearchItems.nth(1).textContent()).toBe('theliquidhorse');
|
expect(await quickSearchItems.nth(1).textContent()).toBe('theliquidhorse');
|
||||||
expect(await quickSearchItems.nth(2).textContent()).toBe('sgtokidoki');
|
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 waitForEditorLoad(page);
|
||||||
await openQuickSearchByShortcut(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 addNewPage.click();
|
||||||
}
|
}
|
||||||
await page.waitForTimeout(200);
|
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 page.waitForTimeout(1000);
|
||||||
await openQuickSearchByShortcut(page);
|
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(
|
expect(await quickSearchItems.nth(0).textContent()).toBe(
|
||||||
'affine is the best'
|
'affine is the best'
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -159,6 +159,9 @@
|
|||||||
{
|
{
|
||||||
"path": "./packages/y-indexeddb"
|
"path": "./packages/y-indexeddb"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "./packages/cmdk"
|
||||||
|
},
|
||||||
// Tests
|
// Tests
|
||||||
{
|
{
|
||||||
"path": "./tests/kit"
|
"path": "./tests/kit"
|
||||||
|
|||||||
@@ -184,6 +184,15 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
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":
|
"@affine/component@workspace:*, @affine/component@workspace:packages/component":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@affine/component@workspace:packages/component"
|
resolution: "@affine/component@workspace:packages/component"
|
||||||
@@ -310,7 +319,6 @@ __metadata:
|
|||||||
"@types/lodash-es": ^4.17.9
|
"@types/lodash-es": ^4.17.9
|
||||||
"@types/webpack-env": ^1.18.1
|
"@types/webpack-env": ^1.18.1
|
||||||
async-call-rpc: ^6.3.1
|
async-call-rpc: ^6.3.1
|
||||||
cmdk: ^0.2.0
|
|
||||||
copy-webpack-plugin: ^11.0.0
|
copy-webpack-plugin: ^11.0.0
|
||||||
css-loader: ^6.8.1
|
css-loader: ^6.8.1
|
||||||
css-spring: ^4.1.0
|
css-spring: ^4.1.0
|
||||||
@@ -428,6 +436,7 @@ __metadata:
|
|||||||
lodash-es: ^4.17.21
|
lodash-es: ^4.17.21
|
||||||
nanoid: ^4.0.2
|
nanoid: ^4.0.2
|
||||||
rxjs: ^7.8.1
|
rxjs: ^7.8.1
|
||||||
|
tinykeys: ^2.1.0
|
||||||
ts-node: ^10.9.1
|
ts-node: ^10.9.1
|
||||||
undici: ^5.23.0
|
undici: ^5.23.0
|
||||||
uuid: ^9.0.0
|
uuid: ^9.0.0
|
||||||
@@ -9101,32 +9110,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@radix-ui/react-dialog@npm:^1.0.4":
|
||||||
version: 1.0.4
|
version: 1.0.4
|
||||||
resolution: "@radix-ui/react-dialog@npm:1.0.4"
|
resolution: "@radix-ui/react-dialog@npm:1.0.4"
|
||||||
@@ -9186,23 +9169,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@radix-ui/react-dismissable-layer@npm:1.0.4":
|
||||||
version: 1.0.4
|
version: 1.0.4
|
||||||
resolution: "@radix-ui/react-dismissable-layer@npm:1.0.4"
|
resolution: "@radix-ui/react-dismissable-layer@npm:1.0.4"
|
||||||
@@ -9253,17 +9219,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@radix-ui/react-focus-guards@npm:1.0.1":
|
||||||
version: 1.0.1
|
version: 1.0.1
|
||||||
resolution: "@radix-ui/react-focus-guards@npm:1.0.1"
|
resolution: "@radix-ui/react-focus-guards@npm:1.0.1"
|
||||||
@@ -9279,21 +9234,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@radix-ui/react-focus-scope@npm:1.0.3":
|
||||||
version: 1.0.3
|
version: 1.0.3
|
||||||
resolution: "@radix-ui/react-focus-scope@npm:1.0.3"
|
resolution: "@radix-ui/react-focus-scope@npm:1.0.3"
|
||||||
@@ -9316,18 +9256,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@radix-ui/react-id@npm:1.0.1":
|
||||||
version: 1.0.1
|
version: 1.0.1
|
||||||
resolution: "@radix-ui/react-id@npm:1.0.1"
|
resolution: "@radix-ui/react-id@npm:1.0.1"
|
||||||
@@ -9444,19 +9372,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@radix-ui/react-portal@npm:1.0.3":
|
||||||
version: 1.0.3
|
version: 1.0.3
|
||||||
resolution: "@radix-ui/react-portal@npm:1.0.3"
|
resolution: "@radix-ui/react-portal@npm:1.0.3"
|
||||||
@@ -9512,19 +9427,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@radix-ui/react-primitive@npm:1.0.1":
|
||||||
version: 1.0.1
|
version: 1.0.1
|
||||||
resolution: "@radix-ui/react-primitive@npm:1.0.1"
|
resolution: "@radix-ui/react-primitive@npm:1.0.1"
|
||||||
@@ -9724,18 +9626,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@radix-ui/react-slot@npm:1.0.1":
|
||||||
version: 1.0.1
|
version: 1.0.1
|
||||||
resolution: "@radix-ui/react-slot@npm:1.0.1"
|
resolution: "@radix-ui/react-slot@npm:1.0.1"
|
||||||
@@ -9926,18 +9816,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@radix-ui/react-use-controllable-state@npm:1.0.1":
|
||||||
version: 1.0.1
|
version: 1.0.1
|
||||||
resolution: "@radix-ui/react-use-controllable-state@npm:1.0.1"
|
resolution: "@radix-ui/react-use-controllable-state@npm:1.0.1"
|
||||||
@@ -9954,18 +9832,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@radix-ui/react-use-escape-keydown@npm:1.0.3":
|
||||||
version: 1.0.3
|
version: 1.0.3
|
||||||
resolution: "@radix-ui/react-use-escape-keydown@npm: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"
|
electron: "link:../../apps/electron/node_modules/electron"
|
||||||
jotai: ^2.4.1
|
jotai: ^2.4.1
|
||||||
react: ^18.2.0
|
react: ^18.2.0
|
||||||
|
tinykeys: ^2.1.0
|
||||||
vite: ^4.4.9
|
vite: ^4.4.9
|
||||||
vite-plugin-dts: 3.5.3
|
vite-plugin-dts: 3.5.3
|
||||||
yjs: ^13.6.8
|
yjs: ^13.6.8
|
||||||
@@ -17113,19 +16980,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"co@npm:^4.6.0":
|
||||||
version: 4.6.0
|
version: 4.6.0
|
||||||
resolution: "co@npm:4.6.0"
|
resolution: "co@npm:4.6.0"
|
||||||
@@ -17247,13 +17101,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"commander@npm:11.0.0":
|
||||||
version: 11.0.0
|
version: 11.0.0
|
||||||
resolution: "commander@npm:11.0.0"
|
resolution: "commander@npm:11.0.0"
|
||||||
@@ -29806,25 +29653,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"react-remove-scroll@npm:2.5.5":
|
||||||
version: 2.5.5
|
version: 2.5.5
|
||||||
resolution: "react-remove-scroll@npm:2.5.5"
|
resolution: "react-remove-scroll@npm:2.5.5"
|
||||||
@@ -32829,6 +32657,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"tinypool@npm:^0.6.0":
|
||||||
version: 0.6.0
|
version: 0.6.0
|
||||||
resolution: "tinypool@npm:0.6.0"
|
resolution: "tinypool@npm:0.6.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user