feat: allow opening new tab for some navigation buttons (#7764)

fix AF-1010
This commit is contained in:
pengx17
2024-08-07 06:43:10 +00:00
parent e1087a0c7b
commit ae9381c36d
12 changed files with 111 additions and 111 deletions

View File

@@ -1,8 +1,8 @@
import { WorkbenchLink } from '@affine/core/modules/workbench';
import { ArrowDownSmallIcon } from '@blocksuite/icons/rc'; import { ArrowDownSmallIcon } from '@blocksuite/icons/rc';
import clsx from 'clsx'; import clsx from 'clsx';
import React from 'react'; import React from 'react';
import type { To } from 'react-router-dom'; import type { To } from 'react-router-dom';
import { Link } from 'react-router-dom';
import * as styles from './index.css'; import * as styles from './index.css';
@@ -90,7 +90,7 @@ export const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(
MenuItem.displayName = 'MenuItem'; MenuItem.displayName = 'MenuItem';
export const MenuLinkItem = React.forwardRef<HTMLDivElement, MenuLinkItemProps>( export const MenuLinkItem = React.forwardRef<HTMLDivElement, MenuLinkItemProps>(
({ to, linkComponent: LinkComponent = Link, ...props }, ref) => { ({ to, linkComponent: LinkComponent = WorkbenchLink, ...props }, ref) => {
return ( return (
<LinkComponent to={to} className={styles.linkItemRoot}> <LinkComponent to={to} className={styles.linkItemRoot}>
{/* The <a> element rendered by Link does not generate display box due to `display: contents` style */} {/* The <a> element rendered by Link does not generate display box due to `display: contents` style */}

View File

@@ -1,18 +1,14 @@
import { toast } from '@affine/component'; import { toast } from '@affine/component';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
import { useDocCollectionHelper } from '@affine/core/hooks/use-block-suite-workspace-helper'; import { useDocCollectionHelper } from '@affine/core/hooks/use-block-suite-workspace-helper';
import { WorkspaceSubPath } from '@affine/core/shared'; import { WorkbenchService } from '@affine/core/modules/workbench';
import { DocsService, initEmptyPage, useService } from '@toeverything/infra'; import { DocsService, initEmptyPage, useService } from '@toeverything/infra';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
import type { DocCollection } from '../../../shared'; import type { DocCollection } from '../../../shared';
export const usePageHelper = (docCollection: DocCollection) => { export const usePageHelper = (docCollection: DocCollection) => {
const { openPage, jumpToSubPath } = useNavigateHelper(); const workbench = useService(WorkbenchService).workbench;
const { createDoc } = useDocCollectionHelper(docCollection); const { createDoc } = useDocCollectionHelper(docCollection);
const { setDocMeta } = useDocMetaHelper(docCollection);
const docRecordList = useService(DocsService).list; const docRecordList = useService(DocsService).list;
const isPreferredEdgeless = useCallback( const isPreferredEdgeless = useCallback(
@@ -22,18 +18,21 @@ export const usePageHelper = (docCollection: DocCollection) => {
); );
const createPageAndOpen = useCallback( const createPageAndOpen = useCallback(
(mode?: 'page' | 'edgeless', open?: boolean) => { (mode?: 'page' | 'edgeless', open?: boolean | 'new-tab') => {
const page = createDoc(); const page = createDoc();
initEmptyPage(page); initEmptyPage(page);
docRecordList.doc$(page.id).value?.setMode(mode || 'page'); docRecordList.doc$(page.id).value?.setMode(mode || 'page');
if (open !== false) openPage(docCollection.id, page.id); if (open !== false)
workbench.openDoc(page.id, {
at: open === 'new-tab' ? 'new-tab' : 'active',
});
return page; return page;
}, },
[docCollection.id, createDoc, openPage, docRecordList] [createDoc, docRecordList, workbench]
); );
const createEdgelessAndOpen = useCallback( const createEdgelessAndOpen = useCallback(
(open?: boolean) => { (open?: boolean | 'new-tab') => {
return createPageAndOpen('edgeless', open); return createPageAndOpen('edgeless', open);
}, },
[createPageAndOpen] [createPageAndOpen]
@@ -59,7 +58,7 @@ export const usePageHelper = (docCollection: DocCollection) => {
}.` }.`
); );
if (options.isWorkspaceFile) { if (options.isWorkspaceFile) {
jumpToSubPath(docCollection.id, WorkspaceSubPath.ALL); workbench.openAll();
return; return;
} }
@@ -67,7 +66,7 @@ export const usePageHelper = (docCollection: DocCollection) => {
return; return;
} }
const pageId = pageIds[0]; const pageId = pageIds[0];
openPage(docCollection.id, pageId); workbench.openDoc(pageId);
}; };
showImportModal({ showImportModal({
collection: docCollection, collection: docCollection,
@@ -78,47 +77,20 @@ export const usePageHelper = (docCollection: DocCollection) => {
}); });
return await promise; return await promise;
}, },
[docCollection, openPage, jumpToSubPath] [docCollection, workbench]
);
const createLinkedPageAndOpen = useAsyncCallback(
async (pageId: string) => {
const page = createPageAndOpen();
page.load();
const parentPage = docCollection.getDoc(pageId);
if (parentPage) {
parentPage.load();
const text = parentPage.Text.fromDelta([
{
insert: ' ',
attributes: {
reference: {
type: 'LinkedPage',
pageId: page.id,
},
},
},
]);
const [frame] = parentPage.getBlockByFlavour('affine:note');
frame && parentPage.addBlock('affine:paragraph', { text }, frame.id);
setDocMeta(page.id, {});
}
},
[docCollection, createPageAndOpen, setDocMeta]
); );
return useMemo(() => { return useMemo(() => {
return { return {
isPreferredEdgeless, isPreferredEdgeless,
createPage: createPageAndOpen, createPage: (open?: boolean | 'new-tab') =>
createPageAndOpen('page', open),
createEdgeless: createEdgelessAndOpen, createEdgeless: createEdgelessAndOpen,
importFile: importFileAndOpen, importFile: importFileAndOpen,
createLinkedPage: createLinkedPageAndOpen,
}; };
}, [ }, [
isPreferredEdgeless, isPreferredEdgeless,
createEdgelessAndOpen, createEdgelessAndOpen,
createLinkedPageAndOpen,
createPageAndOpen, createPageAndOpen,
importFileAndOpen, importFileAndOpen,
]); ]);

View File

@@ -3,14 +3,14 @@ import { BlockCard } from '@affine/component/card/block-card';
import { track } from '@affine/core/mixpanel'; import { track } from '@affine/core/mixpanel';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { EdgelessIcon, ImportIcon, PageIcon } from '@blocksuite/icons/rc'; import { EdgelessIcon, ImportIcon, PageIcon } from '@blocksuite/icons/rc';
import type { PropsWithChildren } from 'react'; import type { MouseEvent, PropsWithChildren } from 'react';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { menuContent } from './new-page-button.css'; import { menuContent } from './new-page-button.css';
type NewPageButtonProps = { type NewPageButtonProps = {
createNewPage: () => void; createNewPage: (e?: MouseEvent) => void;
createNewEdgeless: () => void; createNewEdgeless: (e?: MouseEvent) => void;
importFile?: () => void; importFile?: () => void;
size?: 'small' | 'default'; size?: 'small' | 'default';
}; };
@@ -67,19 +67,26 @@ export const NewPageButton = ({
}: PropsWithChildren<NewPageButtonProps>) => { }: PropsWithChildren<NewPageButtonProps>) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const handleCreateNewPage = useCallback(() => { const handleCreateNewPage: NewPageButtonProps['createNewPage'] = useCallback(
createNewPage(); e => {
createNewPage(e);
setOpen(false); setOpen(false);
track.allDocs.header.actions.createDoc(); track.allDocs.header.actions.createDoc();
}, [createNewPage]); },
[createNewPage]
);
const handleCreateNewEdgeless = useCallback(() => { const handleCreateNewEdgeless: NewPageButtonProps['createNewEdgeless'] =
createNewEdgeless(); useCallback(
e => {
createNewEdgeless(e);
setOpen(false); setOpen(false);
track.allDocs.header.actions.createDoc({ track.allDocs.header.actions.createDoc({
mode: 'edgeless', mode: 'edgeless',
}); });
}, [createNewEdgeless]); },
[createNewEdgeless]
);
const handleImportFile = useCallback(() => { const handleImportFile = useCallback(() => {
importFile?.(); importFile?.();

View File

@@ -65,8 +65,13 @@ export const PageListHeader = () => {
<PageListNewPageButton <PageListNewPageButton
size="small" size="small"
testId="new-page-button-trigger" testId="new-page-button-trigger"
onCreateEdgeless={createEdgeless} onCreateEdgeless={e =>
onCreatePage={createPage} // todo: abstract this for ctrl check
createEdgeless(e?.metaKey || e?.ctrlKey ? 'new-tab' : true)
}
onCreatePage={e =>
createPage(e?.metaKey || e?.ctrlKey ? 'new-tab' : true)
}
onImportFile={onImportFile} onImportFile={onImportFile}
> >
<div className={styles.buttonText}>{t['New Page']()}</div> <div className={styles.buttonText}>{t['New Page']()}</div>

View File

@@ -1,4 +1,4 @@
import type { PropsWithChildren } from 'react'; import type { MouseEvent, PropsWithChildren } from 'react';
import { NewPageButton } from '../components/new-page-button'; import { NewPageButton } from '../components/new-page-button';
import * as styles from './page-list-new-page-button.css'; import * as styles from './page-list-new-page-button.css';
@@ -15,9 +15,9 @@ export const PageListNewPageButton = ({
className?: string; className?: string;
size?: 'small' | 'default'; size?: 'small' | 'default';
testId?: string; testId?: string;
onCreatePage: () => void; onCreatePage: (e?: MouseEvent) => void;
onCreateEdgeless: () => void; onCreateEdgeless: (e?: MouseEvent) => void;
onImportFile?: () => void; onImportFile?: (e?: MouseEvent) => void;
}>) => { }>) => {
return ( return (
<div className={className} data-testid={testId}> <div className={className} data-testid={testId}>

View File

@@ -9,7 +9,6 @@ import {
} from '@affine/core/modules/explorer'; } from '@affine/core/modules/explorer';
import { ExplorerTags } from '@affine/core/modules/explorer/views/sections/tags'; import { ExplorerTags } from '@affine/core/modules/explorer/views/sections/tags';
import { CMDKQuickSearchService } from '@affine/core/modules/quicksearch/services/cmdk'; import { CMDKQuickSearchService } from '@affine/core/modules/quicksearch/services/cmdk';
import { pathGenerator } from '@affine/core/shared';
import { apis, events } from '@affine/electron-api'; import { apis, events } from '@affine/electron-api';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { AllDocsIcon, SettingsIcon } from '@blocksuite/icons/rc'; import { AllDocsIcon, SettingsIcon } from '@blocksuite/icons/rc';
@@ -69,7 +68,6 @@ export type RootAppSidebarProps = {
*/ */
export const RootAppSidebar = (): ReactElement => { export const RootAppSidebar = (): ReactElement => {
const currentWorkspace = useService(WorkspaceService).workspace; const currentWorkspace = useService(WorkspaceService).workspace;
const currentWorkspaceId = currentWorkspace.id;
const { appSettings } = useAppSettingHelper(); const { appSettings } = useAppSettingHelper();
const docCollection = currentWorkspace.docCollection; const docCollection = currentWorkspace.docCollection;
const t = useI18n(); const t = useI18n();
@@ -88,15 +86,13 @@ export const RootAppSidebar = (): ReactElement => {
const onClickNewPage = useAsyncCallback( const onClickNewPage = useAsyncCallback(
async (e?: MouseEvent) => { async (e?: MouseEvent) => {
const page = pageHelper.createPage('page', false); const page = pageHelper.createPage(
e?.ctrlKey || e?.metaKey ? 'new-tab' : true
);
page.load(); page.load();
track.$.navigationPanel.$.createDoc(); track.$.navigationPanel.$.createDoc();
workbench.openDoc(page.id, {
at: e?.ctrlKey || e?.metaKey ? 'new-tab' : 'active',
});
}, },
[pageHelper, workbench] [pageHelper]
); );
useEffect(() => { useEffect(() => {
if (environment.isDesktop) { if (environment.isDesktop) {
@@ -145,11 +141,7 @@ export const RootAppSidebar = (): ReactElement => {
/> />
<AddPageButton onClick={onClickNewPage} /> <AddPageButton onClick={onClickNewPage} />
</div> </div>
<MenuLinkItem <MenuLinkItem icon={<AllDocsIcon />} active={allPageActive} to={'/all'}>
icon={<AllDocsIcon />}
active={allPageActive}
to={pathGenerator.all(currentWorkspaceId)}
>
<span data-testid="all-pages"> <span data-testid="all-pages">
{t['com.affine.workspaceSubPath.all']()} {t['com.affine.workspaceSubPath.all']()}
</span> </span>

View File

@@ -7,6 +7,7 @@ import type { DocCollection } from '@affine/core/shared';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { TodayIcon, TomorrowIcon, YesterdayIcon } from '@blocksuite/icons/rc'; import { TodayIcon, TomorrowIcon, YesterdayIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import { type MouseEvent, useCallback } from 'react';
import { MenuItem } from '../app-sidebar'; import { MenuItem } from '../app-sidebar';
@@ -26,6 +27,13 @@ export const AppSidebarJournalButton = ({
location.pathname.split('/')[1] location.pathname.split('/')[1]
); );
const handleOpenToday = useCallback(
(e: MouseEvent) => {
openToday(e.ctrlKey || e.metaKey);
},
[openToday]
);
const Icon = const Icon =
isJournal && journalDate isJournal && journalDate
? journalDate.isBefore(new Date(), 'day') ? journalDate.isBefore(new Date(), 'day')
@@ -39,7 +47,7 @@ export const AppSidebarJournalButton = ({
<MenuItem <MenuItem
data-testid="slider-bar-journals-button" data-testid="slider-bar-journals-button"
active={isJournal} active={isJournal}
onClick={openToday} onClick={handleOpenToday}
icon={<Icon />} icon={<Icon />}
> >
{t['com.affine.journal.app-sidebar-title']()} {t['com.affine.journal.app-sidebar-title']()}

View File

@@ -3,7 +3,6 @@ import {
useConfirmModal, useConfirmModal,
useDropTarget, useDropTarget,
} from '@affine/component'; } from '@affine/component';
import { WorkbenchLink } from '@affine/core/modules/workbench';
import type { AffineDNDData } from '@affine/core/types/dnd'; import type { AffineDNDData } from '@affine/core/types/dnd';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { import {
@@ -62,7 +61,6 @@ export const TrashButton = () => {
ref={dropTargetRef} ref={dropTargetRef}
icon={<AnimatedDeleteIcon closed={draggedOver} />} icon={<AnimatedDeleteIcon closed={draggedOver} />}
active={trashActive || draggedOver} active={trashActive || draggedOver}
linkComponent={WorkbenchLink}
to={'/trash'} to={'/trash'}
> >
<span data-testid="trash-page"> <span data-testid="trash-page">

View File

@@ -1,12 +1,12 @@
import { i18nTime } from '@affine/i18n'; import { i18nTime } from '@affine/i18n';
import { initEmptyPage } from '@toeverything/infra'; import { initEmptyPage, useService } from '@toeverything/infra';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { WorkbenchService } from '../modules/workbench';
import type { DocCollection } from '../shared'; import type { DocCollection } from '../shared';
import { useCurrentWorkspacePropertiesAdapter } from './use-affine-adapter'; import { useCurrentWorkspacePropertiesAdapter } from './use-affine-adapter';
import { useDocCollectionHelper } from './use-block-suite-workspace-helper'; import { useDocCollectionHelper } from './use-block-suite-workspace-helper';
import { useNavigateHelper } from './use-navigate-helper';
type MaybeDate = Date | string | number; type MaybeDate = Date | string | number;
export const JOURNAL_DATE_FORMAT = 'YYYY-MM-DD'; export const JOURNAL_DATE_FORMAT = 'YYYY-MM-DD';
@@ -153,26 +153,32 @@ export const useJournalHelper = (docCollection: DocCollection) => {
// split useJournalRouteHelper since it requires a <Route /> context, which may not work in lit // split useJournalRouteHelper since it requires a <Route /> context, which may not work in lit
export const useJournalRouteHelper = (docCollection: DocCollection) => { export const useJournalRouteHelper = (docCollection: DocCollection) => {
const navigateHelper = useNavigateHelper();
const { getJournalByDate } = useJournalHelper(docCollection); const { getJournalByDate } = useJournalHelper(docCollection);
const workbench = useService(WorkbenchService).workbench;
/** /**
* open journal by date, create one if not exist * open journal by date, create one if not exist
*/ */
const openJournal = useCallback( const openJournal = useCallback(
(maybeDate: MaybeDate) => { (maybeDate: MaybeDate, newTab?: boolean) => {
const page = getJournalByDate(maybeDate); const page = getJournalByDate(maybeDate);
navigateHelper.openPage(docCollection.id, page.id); workbench.openDoc(page.id, {
at: newTab ? 'new-tab' : 'active',
});
return page.id;
}, },
[getJournalByDate, navigateHelper, docCollection.id] [getJournalByDate, workbench]
); );
/** /**
* open today's journal * open today's journal
*/ */
const openToday = useCallback(() => { const openToday = useCallback(
(newTab?: boolean) => {
const date = dayjs().format(JOURNAL_DATE_FORMAT); const date = dayjs().format(JOURNAL_DATE_FORMAT);
openJournal(date); return openJournal(date, newTab);
}, [openJournal]); },
[openJournal]
);
return useMemo( return useMemo(
() => ({ () => ({

View File

@@ -20,7 +20,7 @@ import type { AffineDNDData } from '@affine/core/types/dnd';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { PlusIcon } from '@blocksuite/icons/rc'; import { PlusIcon } from '@blocksuite/icons/rc';
import { DocsService, useLiveData, useServices } from '@toeverything/infra'; import { DocsService, useLiveData, useServices } from '@toeverything/infra';
import { useCallback, useMemo } from 'react'; import { type MouseEventHandler, useCallback, useMemo } from 'react';
import { ExplorerService } from '../../../services/explorer'; import { ExplorerService } from '../../../services/explorer';
import { CollapsibleSection } from '../../layouts/collapsible-section'; import { CollapsibleSection } from '../../layouts/collapsible-section';
@@ -88,21 +88,26 @@ export const ExplorerFavorites = () => {
[] []
); );
const handleCreateNewFavoriteDoc = useCallback(() => { const handleCreateNewFavoriteDoc: MouseEventHandler = useCallback(
e => {
const newDoc = docsService.createDoc(); const newDoc = docsService.createDoc();
favoriteService.favoriteList.add( favoriteService.favoriteList.add(
'doc', 'doc',
newDoc.id, newDoc.id,
favoriteService.favoriteList.indexAt('before') favoriteService.favoriteList.indexAt('before')
); );
workbenchService.workbench.openDoc(newDoc.id); workbenchService.workbench.openDoc(newDoc.id, {
at: e.ctrlKey || e.metaKey ? 'new-tab' : 'active',
});
explorerSection.setCollapsed(false); explorerSection.setCollapsed(false);
}, [ },
[
docsService, docsService,
explorerSection, explorerSection,
favoriteService.favoriteList, favoriteService.favoriteList,
workbenchService.workbench, workbenchService.workbench,
]); ]
);
const handleOnChildrenDrop = useCallback( const handleOnChildrenDrop = useCallback(
( (

View File

@@ -59,8 +59,12 @@ export const AllPageHeader = ({
styles.headerCreateNewButton, styles.headerCreateNewButton,
!showCreateNew && styles.headerCreateNewButtonHidden !showCreateNew && styles.headerCreateNewButtonHidden
)} )}
onCreateEdgeless={createEdgeless} onCreateEdgeless={e =>
onCreatePage={createPage} createEdgeless(e?.metaKey || e?.ctrlKey ? 'new-tab' : true)
}
onCreatePage={e =>
createPage(e?.metaKey || e?.ctrlKey ? 'new-tab' : true)
}
onImportFile={onImportFile} onImportFile={onImportFile}
> >
<PlusIcon /> <PlusIcon />

View File

@@ -38,6 +38,9 @@ const createAndPinCollection = async (
await getBlockSuiteEditorTitle(page).click(); await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('test page'); await getBlockSuiteEditorTitle(page).fill('test page');
// fixme: remove this timeout. looks like an issue with useBindWorkbenchToBrowserRouter?
await page.waitForTimeout(500);
await page.getByTestId('all-pages').click(); await page.getByTestId('all-pages').click();
const cell = page.getByTestId('page-list-item-title').getByText('test page'); const cell = page.getByTestId('page-list-item-title').getByText('test page');
@@ -67,7 +70,7 @@ test('Show collections items in sidebar', async ({ page }) => {
const collections = page.getByTestId('explorer-collections'); const collections = page.getByTestId('explorer-collections');
await collections.getByTestId('category-divider-collapse-button').click(); await collections.getByTestId('category-divider-collapse-button').click();
const items = collections.locator('[data-testid^="explorer-collection-"]'); const items = collections.locator('[data-testid^="explorer-collection-"]');
expect(await items.count()).toBe(1); await expect(items).toHaveCount(1);
const first = items.first(); const first = items.first();
expect(await first.textContent()).toBe('test collection'); expect(await first.textContent()).toBe('test collection');
await first.getByTestId('explorer-collapsed-button').click(); await first.getByTestId('explorer-collapsed-button').click();
@@ -87,7 +90,7 @@ test('Show collections items in sidebar', async ({ page }) => {
const deleteCollection = page.getByText('Delete'); const deleteCollection = page.getByText('Delete');
await deleteCollection.click(); await deleteCollection.click();
await page.waitForTimeout(50); await page.waitForTimeout(50);
expect(await items.count()).toBe(0); await expect(items).toHaveCount(0);
await createAndPinCollection(page); await createAndPinCollection(page);
expect(await items.count()).toBe(1); expect(await items.count()).toBe(1);
await clickSideBarAllPageButton(page); await clickSideBarAllPageButton(page);