From c78eb965079a66185533a71115b42afb5b549d36 Mon Sep 17 00:00:00 2001 From: JimmFly <447268514@qq.com> Date: Wed, 29 Nov 2023 02:35:12 +0000 Subject: [PATCH] feat(core): show searched result with results group and add duplicate commands (#5073) [TOV-65](https://linear.app/affine-design/issue/TOV-65/should-show-searched-result-without-categories) https://github.com/toeverything/AFFiNE/assets/102217452/50fba70b-7efa-4e47-ba8a-de21e400166c --- .../operation-menu.tsx | 48 +++--------------- .../block-suite-page-list/utils.tsx | 6 +++ .../core/src/components/pure/cmdk/main.css.ts | 16 +++--- .../core/src/components/pure/cmdk/main.tsx | 50 ++++++++++++++----- .../affine/use-block-suite-meta-helper.ts | 44 +++++++++++++++- ...se-register-blocksuite-editor-commands.tsx | 16 +++++- packages/frontend/i18n/src/resources/en.json | 1 + 7 files changed, 119 insertions(+), 62 deletions(-) diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-header-title/operation-menu.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-header-title/operation-menu.tsx index 5d19c820da..2fa9a63ad9 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-header-title/operation-menu.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-header-title/operation-menu.tsx @@ -20,23 +20,15 @@ import { MenuItem, MenuSeparator, } from '@toeverything/components/menu'; -import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks'; -import { - useBlockSuitePageMeta, - usePageMetaHelper, -} from '@toeverything/hooks/use-block-suite-page-meta'; -import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper'; -import { useAtomValue, useSetAtom } from 'jotai'; +import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta'; +import { useAtomValue } from 'jotai'; import { useCallback, useRef, useState } from 'react'; -import { applyUpdate, encodeStateAsUpdate } from 'yjs'; -import { setPageModeAtom } from '../../../atoms'; import { currentModeAtom } from '../../../atoms/mode'; import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper'; import { useExportPage } from '../../../hooks/affine/use-export-page'; import { useTrashModalHelper } from '../../../hooks/affine/use-trash-modal-helper'; import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace'; -import { useNavigateHelper } from '../../../hooks/use-navigate-helper'; import { toast } from '../../../utils'; import { PageHistoryModal } from '../../affine/page-history-modal/history-modal'; import { HeaderDropDownButton } from '../../pure/header-drop-down-button'; @@ -50,7 +42,6 @@ type PageMenuProps = { export const PageMenu = ({ rename, pageId }: PageMenuProps) => { const t = useAFFiNEI18N(); const ref = useRef(null); - const { openPage } = useNavigateHelper(); // fixme(himself65): remove these hooks ASAP const [workspace] = useCurrentWorkspace(); @@ -64,11 +55,9 @@ export const PageMenu = ({ rename, pageId }: PageMenuProps) => { const currentMode = useAtomValue(currentModeAtom); const favorite = pageMeta.favorite ?? false; - const { setPageMeta, setPageTitle } = usePageMetaHelper(blockSuiteWorkspace); - const { togglePageMode, toggleFavorite } = + const { togglePageMode, toggleFavorite, duplicate } = useBlockSuiteMetaHelper(blockSuiteWorkspace); const { importFile } = usePageHelper(blockSuiteWorkspace); - const { createPage } = useBlockSuiteWorkspaceHelper(blockSuiteWorkspace); const { setTrashModal } = useTrashModalHelper(blockSuiteWorkspace); const [historyModalOpen, setHistoryModalOpen] = useState(false); @@ -107,34 +96,11 @@ export const PageMenu = ({ rename, pageId }: PageMenuProps) => { }; const exportHandler = useExportPage(currentPage); - const setPageMode = useSetAtom(setPageModeAtom); - const duplicate = useAsyncCallback(async () => { - const currentPageMeta = currentPage.meta; - const newPage = createPage(); - await newPage.waitForLoaded(); + const handleDuplicate = useCallback(() => { + duplicate(pageId); + }, [duplicate, pageId]); - const update = encodeStateAsUpdate(currentPage.spaceDoc); - applyUpdate(newPage.spaceDoc, update); - - setPageMeta(newPage.id, { - tags: currentPageMeta.tags, - favorite: currentPageMeta.favorite, - }); - setPageMode(newPage.id, currentMode); - setPageTitle(newPage.id, `${currentPageMeta.title}(1)`); - openPage(blockSuiteWorkspace.id, newPage.id); - }, [ - blockSuiteWorkspace.id, - createPage, - currentMode, - currentPage.meta, - currentPage.spaceDoc, - openPage, - setPageMeta, - setPageMode, - setPageTitle, - ]); const EditMenu = ( <> { } data-testid="editor-option-menu-duplicate" - onSelect={duplicate} + onSelect={handleDuplicate} style={menuItemStyle} > {t['com.affine.header.option.duplicate']()} diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-page-list/utils.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-page-list/utils.tsx index ea0287e157..d0f3f7b467 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-page-list/utils.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-page-list/utils.tsx @@ -14,11 +14,14 @@ export const usePageHelper = (blockSuiteWorkspace: BlockSuiteWorkspace) => { const { openPage, jumpToSubPath } = useNavigateHelper(); const { createPage } = useBlockSuiteWorkspaceHelper(blockSuiteWorkspace); const pageSettings = useAtomValue(pageSettingsAtom); + const isPreferredEdgeless = useCallback( (pageId: string) => pageSettings[pageId]?.mode === 'edgeless', [pageSettings] ); + const setPageMode = useSetAtom(setPageModeAtom); + const createPageAndOpen = useCallback( (mode?: 'page' | 'edgeless') => { const page = createPage(); @@ -31,9 +34,11 @@ export const usePageHelper = (blockSuiteWorkspace: BlockSuiteWorkspace) => { }, [blockSuiteWorkspace.id, createPage, openPage, setPageMode] ); + const createEdgelessAndOpen = useCallback(() => { return createPageAndOpen('edgeless'); }, [createPageAndOpen]); + const importFileAndOpen = useAsyncCallback(async () => { const { showImportModal } = await import('@blocksuite/blocks'); const onSuccess = (pageIds: string[], isWorkspaceFile: boolean) => { @@ -55,6 +60,7 @@ export const usePageHelper = (blockSuiteWorkspace: BlockSuiteWorkspace) => { }; showImportModal({ workspace: blockSuiteWorkspace, onSuccess }); }, [blockSuiteWorkspace, openPage, jumpToSubPath]); + return useMemo(() => { return { createPage: createPageAndOpen, diff --git a/packages/frontend/core/src/components/pure/cmdk/main.css.ts b/packages/frontend/core/src/components/pure/cmdk/main.css.ts index fc23684cb1..a0d9264b37 100644 --- a/packages/frontend/core/src/components/pure/cmdk/main.css.ts +++ b/packages/frontend/core/src/components/pure/cmdk/main.css.ts @@ -12,6 +12,7 @@ export const searchInput = style({ color: 'var(--affine-text-primary-color)', fontSize: 'var(--affine-font-h-5)', padding: '21px 24px', + marginBottom: '8px', width: '100%', borderBottom: '1px solid var(--affine-border-color)', flexShrink: 0, @@ -114,13 +115,6 @@ globalStyle(`${root} [cmdk-group][hidden]`, { display: 'none', }); -globalStyle( - `${root} [cmdk-group]:not([hidden]):first-of-type [cmdk-group-heading]`, - { - paddingTop: 16, - } -); - globalStyle(`${root} [cmdk-list]`, { maxHeight: 400, minHeight: 120, @@ -187,3 +181,11 @@ globalStyle( color: 'var(--affine-error-color)', } ); + +export const resultGroupHeader = style({ + padding: '8px', + color: 'var(--affine-text-secondary-color)', + fontSize: 'var(--affine-font-xs)', + fontWeight: 600, + lineHeight: '1.67', +}); diff --git a/packages/frontend/core/src/components/pure/cmdk/main.tsx b/packages/frontend/core/src/components/pure/cmdk/main.tsx index 4252265658..918b78e539 100644 --- a/packages/frontend/core/src/components/pure/cmdk/main.tsx +++ b/packages/frontend/core/src/components/pure/cmdk/main.tsx @@ -1,11 +1,12 @@ import { Command } from '@affine/cmdk'; +import { useCommandState } from '@affine/cmdk'; import { formatDate } from '@affine/component/page-list'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import type { PageMeta } from '@blocksuite/store'; import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks'; import type { CommandCategory } from '@toeverything/infra/command'; import clsx from 'clsx'; -import { useAtom } from 'jotai'; +import { useAtom, useAtomValue } from 'jotai'; import { Suspense, useLayoutEffect, useMemo, useState } from 'react'; import { @@ -71,7 +72,7 @@ const QuickSearchGroup = ({ ); return ( - + {commands.map(command => { const label = typeof command.label === 'string' @@ -129,18 +130,43 @@ const QuickSearchCommands = ({ }: { onOpenChange?: (open: boolean) => void; }) => { + const t = useAFFiNEI18N(); const groups = useCMDKCommandGroups(); - return groups.map(([category, commands]) => { - return ( - - ); - }); + const query = useAtomValue(cmdkQueryAtom); + const resultCount = useCommandState(state => state.filtered.count); + const resultGroupHeader = useMemo(() => { + if (query) { + return ( +
+ { + // hack: use resultCount to determine if it is creation or results + // because the creation(as 2 results) is always shown at the top when there is no result + resultCount === 2 + ? t['com.affine.cmdk.affine.category.affine.creation']() + : t['com.affine.cmdk.affine.category.results']() + } +
+ ); + } + return null; + }, [query, resultCount, t]); + + return ( + <> + {resultGroupHeader} + {groups.map(([category, commands]) => { + return ( + + ); + })} + + ); }; export const CMDKContainer = ({ diff --git a/packages/frontend/core/src/hooks/affine/use-block-suite-meta-helper.ts b/packages/frontend/core/src/hooks/affine/use-block-suite-meta-helper.ts index 53dd028296..2380b239fb 100644 --- a/packages/frontend/core/src/hooks/affine/use-block-suite-meta-helper.ts +++ b/packages/frontend/core/src/hooks/affine/use-block-suite-meta-helper.ts @@ -1,25 +1,31 @@ +import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks'; import { useBlockSuitePageMeta, usePageMetaHelper, } from '@toeverything/hooks/use-block-suite-page-meta'; +import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper'; import { useAtomValue, useSetAtom } from 'jotai'; import { useCallback } from 'react'; +import { applyUpdate, encodeStateAsUpdate } from 'yjs'; import { setPageModeAtom } from '../../atoms'; import { currentModeAtom } from '../../atoms/mode'; import type { BlockSuiteWorkspace } from '../../shared'; import { getWorkspaceSetting } from '../../utils/workspace-setting'; +import { useNavigateHelper } from '../use-navigate-helper'; import { useReferenceLinkHelper } from './use-reference-link-helper'; export function useBlockSuiteMetaHelper( blockSuiteWorkspace: BlockSuiteWorkspace ) { - const { setPageMeta, getPageMeta, setPageReadonly } = + const { setPageMeta, getPageMeta, setPageReadonly, setPageTitle } = usePageMetaHelper(blockSuiteWorkspace); const { addReferenceLink } = useReferenceLinkHelper(blockSuiteWorkspace); const metas = useBlockSuitePageMeta(blockSuiteWorkspace); const setPageMode = useSetAtom(setPageModeAtom); const currentMode = useAtomValue(currentModeAtom); + const { createPage } = useBlockSuiteWorkspaceHelper(blockSuiteWorkspace); + const { openPage } = useNavigateHelper(); const switchToPageMode = useCallback( (pageId: string) => { @@ -140,6 +146,40 @@ export function useBlockSuiteMetaHelper( [setPageMeta] ); + const duplicate = useAsyncCallback( + async (pageId: string) => { + const currentPageMeta = getPageMeta(pageId); + const newPage = createPage(); + const currentPage = blockSuiteWorkspace.getPage(pageId); + + await newPage.waitForLoaded(); + if (!currentPageMeta || !currentPage) { + return; + } + + const update = encodeStateAsUpdate(currentPage.spaceDoc); + applyUpdate(newPage.spaceDoc, update); + + setPageMeta(newPage.id, { + tags: currentPageMeta.tags, + favorite: currentPageMeta.favorite, + }); + setPageMode(newPage.id, currentMode); + setPageTitle(newPage.id, `${currentPageMeta.title}(1)`); + openPage(blockSuiteWorkspace.id, newPage.id); + }, + [ + blockSuiteWorkspace, + createPage, + currentMode, + getPageMeta, + openPage, + setPageMeta, + setPageMode, + setPageTitle, + ] + ); + return { switchToPageMode, switchToEdgelessMode, @@ -155,5 +195,7 @@ export function useBlockSuiteMetaHelper( removeToTrash, restoreFromTrash, permanentlyDeletePage, + + duplicate, }; } diff --git a/packages/frontend/core/src/hooks/affine/use-register-blocksuite-editor-commands.tsx b/packages/frontend/core/src/hooks/affine/use-register-blocksuite-editor-commands.tsx index 62ddb8b48b..73a8b53e62 100644 --- a/packages/frontend/core/src/hooks/affine/use-register-blocksuite-editor-commands.tsx +++ b/packages/frontend/core/src/hooks/affine/use-register-blocksuite-editor-commands.tsx @@ -41,7 +41,7 @@ export function useRegisterBlocksuiteEditorCommands( })); }, [pageId, setPageHistoryModalState]); - const { togglePageMode, toggleFavorite, restoreFromTrash } = + const { togglePageMode, toggleFavorite, restoreFromTrash, duplicate } = useBlockSuiteMetaHelper(blockSuiteWorkspace); const exportHandler = useExportPage(currentPage); const { setTrashModal } = useTrashModalHelper(blockSuiteWorkspace); @@ -125,6 +125,19 @@ export function useRegisterBlocksuiteEditorCommands( }) ); + unsubs.push( + registerAffineCommand({ + id: `editor:${mode}-duplicate`, + preconditionStrategy, + category: `editor:${mode}`, + icon: mode === 'page' ? : , + label: t['com.affine.header.option.duplicate'](), + run() { + duplicate(pageId); + }, + }) + ); + unsubs.push( registerAffineCommand({ id: `editor:${mode}-export-to-pdf`, @@ -234,5 +247,6 @@ export function useRegisterBlocksuiteEditorCommands( trash, isCloudWorkspace, openHistoryModal, + duplicate, ]); } diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 440de930e6..0af0b9fc35 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -486,6 +486,7 @@ "com.affine.cmdk.affine.category.editor.edgeless": "Edgeless Commands", "com.affine.cmdk.affine.category.editor.insert-object": "Insert Object", "com.affine.cmdk.affine.category.editor.page": "Page Commands", + "com.affine.cmdk.affine.category.results": "Results", "com.affine.cmdk.affine.client-border-style.to": "Change Client Border Style to", "com.affine.cmdk.affine.color-mode.to": "Change Colour Mode to", "com.affine.cmdk.affine.color-scheme.to": "Change Colour Scheme to",