From 9307acf0dee296dd5c4bf7efb40ad4d4525ca729 Mon Sep 17 00:00:00 2001 From: pengx17 Date: Mon, 5 Aug 2024 03:11:47 +0000 Subject: [PATCH] fix(core): ctrl/cmd + click on add page button opens in new tab (#7701) fix PD-1521 --- .../app-sidebar/add-page-button/index.tsx | 3 +- .../block-suite-header/menu/index.tsx | 50 ++++++++++++--- .../src/components/root-app-sidebar/index.tsx | 43 +++++++------ .../modules/workbench/entities/workbench.ts | 62 +++++++++++++------ .../core/src/modules/workbench/index.ts | 11 +++- .../services/workbench-new-tab-handler.ts | 38 ++++++++++++ .../modules/workbench/view/workbench-link.tsx | 32 ++++------ packages/frontend/core/src/utils/popup.ts | 2 - packages/frontend/i18n/src/resources/en.json | 1 + 9 files changed, 168 insertions(+), 74 deletions(-) create mode 100644 packages/frontend/core/src/modules/workbench/services/workbench-new-tab-handler.ts diff --git a/packages/frontend/core/src/components/app-sidebar/add-page-button/index.tsx b/packages/frontend/core/src/components/app-sidebar/add-page-button/index.tsx index c8da78a66b..369c623d0f 100644 --- a/packages/frontend/core/src/components/app-sidebar/add-page-button/index.tsx +++ b/packages/frontend/core/src/components/app-sidebar/add-page-button/index.tsx @@ -3,11 +3,12 @@ import { useI18n } from '@affine/i18n'; import { PlusIcon } from '@blocksuite/icons/rc'; import clsx from 'clsx'; import type React from 'react'; +import type { MouseEventHandler } from 'react'; import * as styles from './index.css'; interface AddPageButtonProps { - onClick?: () => void; + onClick?: MouseEventHandler; className?: string; style?: React.CSSProperties; } diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx index 8188140d1e..511a0dcfe3 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx @@ -19,6 +19,7 @@ import { useExportPage } from '@affine/core/hooks/affine/use-export-page'; import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { mixpanel } from '@affine/core/mixpanel'; +import { WorkbenchService } from '@affine/core/modules/workbench'; import { useDetailPageHeaderResponsive } from '@affine/core/pages/workspace/detail-page/use-header-responsive'; import { WorkspaceFlavour } from '@affine/env/workspace'; import { useI18n } from '@affine/i18n'; @@ -31,8 +32,10 @@ import { HistoryIcon, ImportIcon, InformationIcon, + OpenInNewIcon, PageIcon, ShareIcon, + SplitViewIcon, } from '@blocksuite/icons/rc'; import type { Doc } from '@blocksuite/store'; import { @@ -73,6 +76,8 @@ export const PageHeaderMenuButton = ({ const isInTrash = useLiveData(doc.meta$.map(m => m.trash)); const currentMode = useLiveData(doc.mode$); + const workbench = useService(WorkbenchService).workbench; + const { favorite, toggleFavorite } = useFavorite(pageId); const { duplicate } = useBlockSuiteMetaHelper(docCollection); @@ -94,6 +99,18 @@ export const PageHeaderMenuButton = ({ setOpenInfoModal(true); }; + const handleOpenInNewTab = useCallback(() => { + workbench.openDoc(pageId, { + at: 'new-tab', + }); + }, [pageId, workbench]); + + const handleOpenInSplitView = useCallback(() => { + workbench.openDoc(pageId, { + at: 'tail', + }); + }, [pageId, workbench]); + const handleOpenTrashModal = useCallback(() => { setTrashModal({ open: true, @@ -237,16 +254,35 @@ export const PageHeaderMenuButton = ({ ? t['com.affine.favoritePageOperation.remove']() : t['com.affine.favoritePageOperation.add']()} - {/* {TODO(@Peng): add tag function support} */} - {/* } - data-testid="editor-option-menu-add-tag" - onClick={() => {}} + + + + + } + data-testid="editor-option-menu-open-in-new-tab" + onSelect={handleOpenInNewTab} style={menuItemStyle} > - {t['com.affine.header.option.add-tag']()} - */} + {t['com.affine.workbench.tab.page-menu-open']()} + + + + + + } + data-testid="editor-option-menu-open-in-split-new" + onSelect={handleOpenInSplitView} + style={menuItemStyle} + > + {t['com.affine.workbench.split-view.page-menu-open']()} + + + {runtimeConfig.enableInfoModal && ( { const currentWorkspace = useService(WorkspaceService).workspace; const currentWorkspaceId = currentWorkspace.id; - const { openPage } = useNavigateHelper(); const { appSettings } = useAppSettingHelper(); const docCollection = currentWorkspace.docCollection; const t = useI18n(); + const workbench = useService(WorkbenchService).workbench; const currentPath = useLiveData( - useService(WorkbenchService).workbench.location$.map( - location => location.pathname - ) + workbench.location$.map(location => location.pathname) ); const cmdkQuickSearchService = useService(CMDKQuickSearchService); const onOpenQuickSearchModal = useCallback(() => { @@ -93,27 +90,29 @@ export const RootAppSidebar = (): ReactElement => { const allPageActive = currentPath === '/all'; const pageHelper = usePageHelper(currentWorkspace.docCollection); - const createPage = useCallback(() => { - return pageHelper.createPage(); - }, [pageHelper]); - const onClickNewPage = useAsyncCallback(async () => { - const page = createPage(); - page.load(); - openPage(currentWorkspaceId, page.id); - mixpanel.track('DocCreated', { - segment: 'navigation panel', - module: 'bottom button', - control: 'new doc button', - category: 'page', - type: 'doc', - }); - }, [createPage, currentWorkspaceId, openPage]); + const onClickNewPage = useAsyncCallback( + async (e?: MouseEvent) => { + const page = pageHelper.createPage('page', false); + page.load(); + mixpanel.track('DocCreated', { + segment: 'navigation panel', + module: 'bottom button', + control: 'new doc button', + category: 'page', + type: 'doc', + }); + workbench.openDoc(page.id, { + at: e?.ctrlKey || e?.metaKey ? 'new-tab' : 'active', + }); + }, + [pageHelper, workbench] + ); // Listen to the "New Page" action from the menu useEffect(() => { if (environment.isDesktop) { - return events?.applicationMenu.onNewPageAction(onClickNewPage); + return events?.applicationMenu.onNewPageAction(() => onClickNewPage()); } return; }, [onClickNewPage]); diff --git a/packages/frontend/core/src/modules/workbench/entities/workbench.ts b/packages/frontend/core/src/modules/workbench/entities/workbench.ts index 24dc2d3b11..4dd600cf2c 100644 --- a/packages/frontend/core/src/modules/workbench/entities/workbench.ts +++ b/packages/frontend/core/src/modules/workbench/entities/workbench.ts @@ -1,20 +1,25 @@ import { Unreachable } from '@affine/env/constant'; import { Entity, LiveData } from '@toeverything/infra'; -import type { To } from 'history'; +import { type To } from 'history'; import { nanoid } from 'nanoid'; +import type { WorkbenchNewTabHandler } from '../services/workbench-new-tab-handler'; import type { WorkbenchDefaultState } from '../services/workbench-view-state'; import { View } from './view'; export type WorkbenchPosition = 'beside' | 'active' | 'head' | 'tail' | number; -interface WorkbenchOpenOptions { - at?: WorkbenchPosition; +type WorkbenchOpenOptions = { + at?: WorkbenchPosition | 'new-tab'; replaceHistory?: boolean; -} + show?: boolean; // only for new tab +}; export class Workbench extends Entity { - constructor(private readonly defaultState: WorkbenchDefaultState) { + constructor( + private readonly defaultState: WorkbenchDefaultState, + private readonly newTabHandler: WorkbenchNewTabHandler + ) { super(); } @@ -74,26 +79,45 @@ export class Workbench extends Entity { this.sidebarOpen$.next(!this.sidebarOpen$.value); } - open( - to: To, - { at = 'active', replaceHistory = false }: WorkbenchOpenOptions = {} - ) { - let view = this.viewAt(at); - if (!view) { - const newIndex = this.createView(at, to); - view = this.viewAt(newIndex); - if (!view) { - throw new Unreachable(); - } + open(to: To, option: WorkbenchOpenOptions = {}) { + if (option.at === 'new-tab') { + this.newTab(to, { + show: option.show, + }); } else { - if (replaceHistory) { - view.history.replace(to); + const { at = 'active', replaceHistory = false } = option; + let view = this.viewAt(at); + if (!view) { + const newIndex = this.createView(at, to); + view = this.viewAt(newIndex); + if (!view) { + throw new Unreachable(); + } } else { - view.history.push(to); + if (replaceHistory) { + view.history.replace(to); + } else { + view.history.push(to); + } } } } + newTab( + to: To, + { + show, + }: { + show?: boolean; + } = {} + ) { + this.newTabHandler({ + basename: this.basename$.value, + to, + show: show ?? true, + }); + } + openDoc( id: string | { docId: string; blockId?: string }, options?: WorkbenchOpenOptions diff --git a/packages/frontend/core/src/modules/workbench/index.ts b/packages/frontend/core/src/modules/workbench/index.ts index 5b13b0d997..3bc078d059 100644 --- a/packages/frontend/core/src/modules/workbench/index.ts +++ b/packages/frontend/core/src/modules/workbench/index.ts @@ -20,6 +20,11 @@ import { ViewScope } from './scopes/view'; import { DesktopStateSynchronizer } from './services/desktop-state-synchronizer'; import { ViewService } from './services/view'; import { WorkbenchService } from './services/workbench'; +import { + BrowserWorkbenchNewTabHandler, + DesktopWorkbenchNewTabHandler, + WorkbenchNewTabHandler, +} from './services/workbench-new-tab-handler'; import { DesktopWorkbenchDefaultState, InMemoryWorkbenchDefaultState, @@ -30,7 +35,7 @@ export function configureWorkbenchCommonModule(services: Framework) { services .scope(WorkspaceScope) .service(WorkbenchService) - .entity(Workbench, [WorkbenchDefaultState]) + .entity(Workbench, [WorkbenchDefaultState, WorkbenchNewTabHandler]) .entity(View) .scope(ViewScope) .service(ViewService, [ViewScope]) @@ -41,7 +46,8 @@ export function configureBrowserWorkbenchModule(services: Framework) { configureWorkbenchCommonModule(services); services .scope(WorkspaceScope) - .impl(WorkbenchDefaultState, InMemoryWorkbenchDefaultState); + .impl(WorkbenchDefaultState, InMemoryWorkbenchDefaultState) + .impl(WorkbenchNewTabHandler, () => BrowserWorkbenchNewTabHandler); } export function configureDesktopWorkbenchModule(services: Framework) { @@ -51,5 +57,6 @@ export function configureDesktopWorkbenchModule(services: Framework) { .impl(WorkbenchDefaultState, DesktopWorkbenchDefaultState, [ GlobalStateService, ]) + .impl(WorkbenchNewTabHandler, () => DesktopWorkbenchNewTabHandler) .service(DesktopStateSynchronizer, [WorkbenchService]); } diff --git a/packages/frontend/core/src/modules/workbench/services/workbench-new-tab-handler.ts b/packages/frontend/core/src/modules/workbench/services/workbench-new-tab-handler.ts new file mode 100644 index 0000000000..50da46f289 --- /dev/null +++ b/packages/frontend/core/src/modules/workbench/services/workbench-new-tab-handler.ts @@ -0,0 +1,38 @@ +import { popupWindow } from '@affine/core/utils'; +import { apis } from '@affine/electron-api'; +import { createIdentifier } from '@toeverything/infra'; +import { parsePath, type To } from 'history'; + +export type WorkbenchNewTabHandler = (option: { + basename: string; + to: To; + show: boolean; +}) => void; + +export const WorkbenchNewTabHandler = createIdentifier( + 'WorkbenchNewTabHandler' +); + +export const BrowserWorkbenchNewTabHandler: WorkbenchNewTabHandler = ({ + basename, + to, +}) => { + const link = + basename + + (typeof to === 'string' ? to : `${to.pathname}${to.search}${to.hash}`); + popupWindow(link); +}; + +export const DesktopWorkbenchNewTabHandler: WorkbenchNewTabHandler = ({ + basename, + to, +}) => { + const path = typeof to === 'string' ? parsePath(to) : to; + apis?.ui + .addTab({ + basename, + view: { path }, + show: false, + }) + .catch(console.error); +}; diff --git a/packages/frontend/core/src/modules/workbench/view/workbench-link.tsx b/packages/frontend/core/src/modules/workbench/view/workbench-link.tsx index 9caf953881..75d26cc73e 100644 --- a/packages/frontend/core/src/modules/workbench/view/workbench-link.tsx +++ b/packages/frontend/core/src/modules/workbench/view/workbench-link.tsx @@ -1,9 +1,7 @@ import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; -import { popupWindow } from '@affine/core/utils'; -import { apis } from '@affine/electron-api'; import { useLiveData, useService } from '@toeverything/infra'; -import { parsePath, type To } from 'history'; +import { type To } from 'history'; import { forwardRef, type MouseEvent } from 'react'; import { WorkbenchService } from '../services/workbench'; @@ -31,26 +29,18 @@ export const WorkbenchLink = forwardRef< return; } - if (event.ctrlKey || event.metaKey) { - if (environment.isDesktop) { - if (event.altKey && appSettings.enableMultiView) { - workbench.open(to, { at: 'tail' }); - } else { - const path = typeof to === 'string' ? parsePath(to) : to; - await apis?.ui.addTab({ - basename, - view: { path }, - show: false, - }); - } - } else if (!environment.isDesktop) { - popupWindow(link); + const at = (() => { + if (event.ctrlKey || event.metaKey) { + return event.altKey && appSettings.enableMultiView + ? 'tail' + : 'new-tab'; } - } else { - workbench.open(to); - } + return 'active'; + })(); + + workbench.open(to, { at }); }, - [appSettings.enableMultiView, basename, link, onClick, to, workbench] + [appSettings.enableMultiView, onClick, to, workbench] ); // eslint suspicious runtime error diff --git a/packages/frontend/core/src/utils/popup.ts b/packages/frontend/core/src/utils/popup.ts index 9b1675bb55..b870932b5c 100644 --- a/packages/frontend/core/src/utils/popup.ts +++ b/packages/frontend/core/src/utils/popup.ts @@ -4,7 +4,5 @@ export function popupWindow(target: string) { ? target : runtimeConfig.serverUrlPrefix + target; url.searchParams.set('redirect_uri', target); - - console.log(url.href); return window.open(url, '_blank', `noreferrer noopener`); } diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 2c09a7298e..72966dae39 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1349,6 +1349,7 @@ "com.affine.workbench.split-view-menu.move-left": "Move Left", "com.affine.workbench.split-view-menu.move-right": "Move Right", "com.affine.workbench.split-view.page-menu-open": "Open in split view", + "com.affine.workbench.tab.page-menu-open": "Open in new tab", "com.affine.workspace.cannot-delete": "You cannot delete the last workspace", "com.affine.workspace.cloud": "Cloud Workspaces", "com.affine.workspace.cloud.account.logout": "Sign Out",