feat(electron): new tab/split view entries (#7708)

fix AF-1146
This commit is contained in:
pengx17
2024-08-05 05:37:50 +00:00
parent bd31c8388c
commit 51ca7657d8
11 changed files with 272 additions and 158 deletions

View File

@@ -18,7 +18,6 @@ import { useI18n } from '@affine/i18n';
import { import {
DeleteIcon, DeleteIcon,
DeletePermanentlyIcon, DeletePermanentlyIcon,
DualLinkIcon,
DuplicateIcon, DuplicateIcon,
EditIcon, EditIcon,
FavoritedIcon, FavoritedIcon,
@@ -27,6 +26,7 @@ import {
FilterMinusIcon, FilterMinusIcon,
InformationIcon, InformationIcon,
MoreVerticalIcon, MoreVerticalIcon,
OpenInNewIcon,
PlusIcon, PlusIcon,
ResetIcon, ResetIcon,
SplitViewIcon, SplitViewIcon,
@@ -39,7 +39,6 @@ import {
WorkspaceService, WorkspaceService,
} from '@toeverything/infra'; } from '@toeverything/infra';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { Link } from 'react-router-dom';
import type { CollectionService } from '../../modules/collection'; import type { CollectionService } from '../../modules/collection';
import { InfoModal } from '../affine/page-properties'; import { InfoModal } from '../affine/page-properties';
@@ -49,7 +48,7 @@ import * as styles from './list.css';
import { DisablePublicSharing, MoveToTrash } from './operation-menu-items'; import { DisablePublicSharing, MoveToTrash } from './operation-menu-items';
import { CreateOrEditTag } from './tags/create-tag'; import { CreateOrEditTag } from './tags/create-tag';
import type { TagMeta } from './types'; import type { TagMeta } from './types';
import { ColWrapper, stopPropagationWithoutPrevent } from './utils'; import { ColWrapper } from './utils';
import { useEditCollection, useEditCollectionName } from './view'; import { useEditCollection, useEditCollectionName } from './view';
const tooltipSideTop = { side: 'top' as const }; const tooltipSideTop = { side: 'top' as const };
@@ -100,6 +99,10 @@ export const PageOperationCell = ({
workbench.openDoc(page.id, { at: 'tail' }); workbench.openDoc(page.id, { at: 'tail' });
}, [page.id, workbench]); }, [page.id, workbench]);
const onOpenInNewTab = useCallback(() => {
workbench.openDoc(page.id, { at: 'new-tab' });
}, [page.id, workbench]);
const onToggleFavoritePage = useCallback(() => { const onToggleFavoritePage = useCallback(() => {
const status = favAdapter.isFavorite(page.id, 'doc'); const status = favAdapter.isFavorite(page.id, 'doc');
favAdapter.toggle(page.id, 'doc'); favAdapter.toggle(page.id, 'doc');
@@ -173,6 +176,17 @@ export const PageOperationCell = ({
</MenuItem> </MenuItem>
) : null} ) : null}
<MenuItem
onClick={onOpenInNewTab}
preFix={
<MenuIcon>
<OpenInNewIcon />
</MenuIcon>
}
>
{t['com.affine.workbench.tab.page-menu-open']()}
</MenuItem>
{environment.isDesktop && appSettings.enableMultiView ? ( {environment.isDesktop && appSettings.enableMultiView ? (
<MenuItem <MenuItem
onClick={onOpenInSplitView} onClick={onOpenInSplitView}
@@ -186,27 +200,6 @@ export const PageOperationCell = ({
</MenuItem> </MenuItem>
) : null} ) : null}
{!environment.isDesktop && (
<Link
className={styles.clearLinkStyle}
onClick={stopPropagationWithoutPrevent}
to={`/workspace/${currentWorkspace.id}/${page.id}`}
target={'_blank'}
rel="noopener noreferrer"
>
<MenuItem
style={{ marginBottom: 4 }}
preFix={
<MenuIcon>
<DualLinkIcon />
</MenuIcon>
}
>
{t['com.affine.openPageOperation.newTab']()}
</MenuItem>
</Link>
)}
<MenuItem <MenuItem
preFix={ preFix={
<MenuIcon> <MenuIcon>

View File

@@ -12,6 +12,7 @@ import {
FavoritedIcon, FavoritedIcon,
FavoriteIcon, FavoriteIcon,
FilterIcon, FilterIcon,
OpenInNewIcon,
PlusIcon, PlusIcon,
SplitViewIcon, SplitViewIcon,
} from '@blocksuite/icons/rc'; } from '@blocksuite/icons/rc';
@@ -79,6 +80,10 @@ export const CollectionOperations = ({
workbench.openCollection(collection.id, { at: 'tail' }); workbench.openCollection(collection.id, { at: 'tail' });
}, [collection.id, workbench]); }, [collection.id, workbench]);
const openCollectionNewTab = useCallback(() => {
workbench.openCollection(collection.id, { at: 'new-tab' });
}, [collection.id, workbench]);
const favAdapter = useService(CompatibleFavoriteItemsAdapter); const favAdapter = useService(CompatibleFavoriteItemsAdapter);
const onToggleFavoritePage = useCallback(() => { const onToggleFavoritePage = useCallback(() => {
@@ -153,6 +158,15 @@ export const CollectionOperations = ({
: t['com.affine.favoritePageOperation.add'](), : t['com.affine.favoritePageOperation.add'](),
click: onToggleFavoritePage, click: onToggleFavoritePage,
}, },
{
icon: (
<MenuIcon>
<OpenInNewIcon />
</MenuIcon>
),
name: t['com.affine.workbench.tab.page-menu-open'](),
click: openCollectionNewTab,
},
...(appSettings.enableMultiView ...(appSettings.enableMultiView
? [ ? [
{ {
@@ -189,6 +203,7 @@ export const CollectionOperations = ({
onAddDocToCollection, onAddDocToCollection,
favorite, favorite,
onToggleFavoritePage, onToggleFavoritePage,
openCollectionNewTab,
appSettings.enableMultiView, appSettings.enableMultiView,
openCollectionSplitView, openCollectionSplitView,
service, service,

View File

@@ -17,6 +17,7 @@ import {
FavoritedIcon, FavoritedIcon,
FavoriteIcon, FavoriteIcon,
FilterIcon, FilterIcon,
OpenInNewIcon,
PlusIcon, PlusIcon,
SplitViewIcon, SplitViewIcon,
} from '@blocksuite/icons/rc'; } from '@blocksuite/icons/rc';
@@ -110,6 +111,15 @@ export const useExplorerCollectionNodeOperations = (
}); });
}, [collectionId, workbenchService.workbench]); }, [collectionId, workbenchService.workbench]);
const handleOpenInNewTab = useCallback(() => {
workbenchService.workbench.openCollection(collectionId, { at: 'new-tab' });
mixpanel.track('OpenInNewTab', {
page: 'sidebar',
module: 'collection',
control: 'open in new tab button',
});
}, [collectionId, workbenchService.workbench]);
const handleDeleteCollection = useCallback(() => { const handleDeleteCollection = useCallback(() => {
collectionService.deleteCollection(deleteInfo, collectionId); collectionService.deleteCollection(deleteInfo, collectionId);
mixpanel.track('CollectionDeleted', { mixpanel.track('CollectionDeleted', {
@@ -187,6 +197,21 @@ export const useExplorerCollectionNodeOperations = (
</MenuItem> </MenuItem>
), ),
}, },
{
index: 99,
view: (
<MenuItem
preFix={
<MenuIcon>
<OpenInNewIcon />
</MenuIcon>
}
onClick={handleOpenInNewTab}
>
{t['com.affine.workbench.tab.page-menu-open']()}
</MenuItem>
),
},
...(appSettings.enableMultiView ...(appSettings.enableMultiView
? [ ? [
{ {
@@ -232,6 +257,7 @@ export const useExplorerCollectionNodeOperations = (
favorite, favorite,
handleAddDocToCollection, handleAddDocToCollection,
handleDeleteCollection, handleDeleteCollection,
handleOpenInNewTab,
handleOpenInSplitView, handleOpenInSplitView,
handleShowEdit, handleShowEdit,
handleToggleFavoriteCollection, handleToggleFavoriteCollection,

View File

@@ -17,6 +17,7 @@ import {
FavoriteIcon, FavoriteIcon,
InformationIcon, InformationIcon,
LinkedPageIcon, LinkedPageIcon,
OpenInNewIcon,
SplitViewIcon, SplitViewIcon,
} from '@blocksuite/icons/rc'; } from '@blocksuite/icons/rc';
import { DocsService, useLiveData, useServices } from '@toeverything/infra'; import { DocsService, useLiveData, useServices } from '@toeverything/infra';
@@ -75,6 +76,17 @@ export const useExplorerDocNodeOperations = (
}); });
}, [docRecord, openConfirmModal, t]); }, [docRecord, openConfirmModal, t]);
const handleOpenInNewTab = useCallback(() => {
workbenchService.workbench.openDoc(docId, {
at: 'new-tab',
});
mixpanel.track('OpenInNewTab', {
page: 'sidebar',
module: 'doc',
control: 'open in new tab button',
});
}, [docId, workbenchService]);
const handleOpenInSplitView = useCallback(() => { const handleOpenInSplitView = useCallback(() => {
workbenchService.workbench.openDoc(docId, { workbenchService.workbench.openDoc(docId, {
at: 'beside', at: 'beside',
@@ -151,6 +163,21 @@ export const useExplorerDocNodeOperations = (
</MenuItem> </MenuItem>
), ),
}, },
{
index: 99,
view: (
<MenuItem
preFix={
<MenuIcon>
<OpenInNewIcon />
</MenuIcon>
}
onClick={handleOpenInNewTab}
>
{t['com.affine.workbench.tab.page-menu-open']()}
</MenuItem>
),
},
...(appSettings.enableMultiView ...(appSettings.enableMultiView
? [ ? [
{ {
@@ -219,6 +246,7 @@ export const useExplorerDocNodeOperations = (
favorite, favorite,
handleAddLinkedPage, handleAddLinkedPage,
handleMoveToTrash, handleMoveToTrash,
handleOpenInNewTab,
handleOpenInSplitView, handleOpenInSplitView,
handleToggleFavoriteDoc, handleToggleFavoriteDoc,
options.openInfoModal, options.openInfoModal,

View File

@@ -15,6 +15,7 @@ import {
DeleteIcon, DeleteIcon,
FavoritedIcon, FavoritedIcon,
FavoriteIcon, FavoriteIcon,
OpenInNewIcon,
PlusIcon, PlusIcon,
SplitViewIcon, SplitViewIcon,
} from '@blocksuite/icons/rc'; } from '@blocksuite/icons/rc';
@@ -97,6 +98,17 @@ export const useExplorerTagNodeOperations = (
}); });
}, [favoriteService, tagId]); }, [favoriteService, tagId]);
const handleOpenInNewTab = useCallback(() => {
workbenchService.workbench.openTag(tagId, {
at: 'new-tab',
});
mixpanel.track('OpenInNewTab', {
page: 'sidebar',
module: 'tag',
control: 'open in new tab button',
});
}, [tagId, workbenchService]);
return useMemo( return useMemo(
() => [ () => [
{ {
@@ -108,6 +120,21 @@ export const useExplorerTagNodeOperations = (
</IconButton> </IconButton>
), ),
}, },
{
index: 50,
view: (
<MenuItem
preFix={
<MenuIcon>
<OpenInNewIcon />
</MenuIcon>
}
onClick={handleOpenInNewTab}
>
{t['com.affine.workbench.tab.page-menu-open']()}
</MenuItem>
),
},
...(appSettings.enableMultiView ...(appSettings.enableMultiView
? [ ? [
{ {
@@ -180,6 +207,7 @@ export const useExplorerTagNodeOperations = (
favorite, favorite,
handleMoveToTrash, handleMoveToTrash,
handleNewDoc, handleNewDoc,
handleOpenInNewTab,
handleOpenInSplitView, handleOpenInSplitView,
handleToggleFavoriteTag, handleToggleFavoriteTag,
t, t,

View File

@@ -18,30 +18,42 @@ export const resolveRouteLinkMeta = (href: string) => {
return null; return null;
} }
const hash = url.hash;
const pathname = url.pathname;
// http://---/workspace/{workspaceid}/xxx/yyy // http://---/workspace/{workspaceid}/xxx/yyy
// http://---/workspace/{workspaceid}/xxx // http://---/workspace/{workspaceid}/xxx
const [_, workspaceId, moduleName, subModuleName] = const [_, workspaceId, moduleName, subModuleName] =
pathname.match(/\/workspace\/([^/]+)\/([^/]+)(?:\/([^/]+))?/) || []; url.pathname.match(/\/workspace\/([^/]+)\/([^/]+)(?:\/([^/]+))?/) || [];
if (isRouteModulePath(moduleName)) { if (workspaceId) {
return { const basename = `/workspace/${workspaceId}`;
workspaceId, const pathname = url.pathname.replace(basename, '');
moduleName, const search = url.search;
subModuleName, const hash = url.hash;
}; const location = {
} else if (moduleName) { pathname,
// for now we assume all other cases are doc links search,
return { hash,
workspaceId,
moduleName: 'doc' as const,
docId: moduleName,
blockId: hash.slice(1),
}; };
if (isRouteModulePath(moduleName)) {
return {
location,
basename,
workspaceId,
moduleName,
subModuleName,
};
} else if (moduleName) {
// for now we assume all other cases are doc links
return {
location,
basename,
workspaceId,
moduleName: 'doc' as const,
docId: moduleName,
blockId: hash.slice(1),
};
}
} }
return; return null;
} catch { } catch {
return null; return null;
} }

View File

@@ -24,12 +24,13 @@ export class DesktopStateSynchronizer extends Service {
event.type === 'open-in-split-view' && event.type === 'open-in-split-view' &&
event.payload.tabId === appInfo?.viewId event.payload.tabId === appInfo?.viewId
) { ) {
const activeView = workbench.activeView$.value; const to =
if (activeView) { event.payload.view?.path ??
workbench.open(activeView.location$.value, { workbench.activeView$.value?.location$.value;
at: 'beside',
}); workbench.open(to, {
} at: 'beside',
});
} }
if ( if (

View File

@@ -22,10 +22,10 @@ import {
pingAppLayoutReady, pingAppLayoutReady,
showDevTools, showDevTools,
showTab, showTab,
showTabContextMenu,
updateWorkbenchMeta, updateWorkbenchMeta,
updateWorkbenchViewMeta, updateWorkbenchViewMeta,
} from '../windows-manager'; } from '../windows-manager';
import { showTabContextMenu } from '../windows-manager/context-menu';
import { getChallengeResponse } from './challenge'; import { getChallengeResponse } from './challenge';
import { uiSubjects } from './subject'; import { uiSubjects } from './subject';

View File

@@ -0,0 +1,94 @@
import { Menu } from 'electron';
import { logger } from '../logger';
import {
addTab,
closeTab,
reloadView,
WebContentViewsManager,
} from './tab-views';
export const showTabContextMenu = async (tabId: string, viewIndex: number) => {
const workbenches = WebContentViewsManager.instance.tabViewsMeta.workbenches;
const unpinned = workbenches.filter(w => !w.pinned);
const tabMeta = workbenches.find(w => w.id === tabId);
if (!tabMeta) {
return;
}
const template: Parameters<typeof Menu.buildFromTemplate>[0] = [
tabMeta.pinned
? {
label: 'Unpin tab',
click: () => {
WebContentViewsManager.instance.pinTab(tabId, false);
},
}
: {
label: 'Pin tab',
click: () => {
WebContentViewsManager.instance.pinTab(tabId, true);
},
},
{
label: 'Refresh tab',
click: () => {
reloadView().catch(logger.error);
},
},
{
label: 'Duplicate tab',
click: () => {
addTab({
basename: tabMeta.basename,
view: tabMeta.views,
show: false,
}).catch(logger.error);
},
},
{ type: 'separator' },
tabMeta.views.length > 1
? {
label: 'Separate tabs',
click: () => {
WebContentViewsManager.instance.separateView(tabId, viewIndex);
},
}
: {
label: 'Open in split view',
click: () => {
WebContentViewsManager.instance.openInSplitView({ tabId });
},
},
...(unpinned.length > 0
? ([
{ type: 'separator' },
{
label: 'Close tab',
click: () => {
closeTab(tabId).catch(logger.error);
},
},
{
label: 'Close other tabs',
click: () => {
const tabsToRetain =
WebContentViewsManager.instance.tabViewsMeta.workbenches.filter(
w => w.id === tabId || w.pinned
);
WebContentViewsManager.instance.patchTabViewsMeta({
workbenches: tabsToRetain,
activeWorkbenchId: tabId,
});
},
},
] as const)
: []),
];
const menu = Menu.buildFromTemplate(template);
menu.popup();
};

View File

@@ -4,7 +4,6 @@ import {
app, app,
type CookiesSetDetails, type CookiesSetDetails,
globalShortcut, globalShortcut,
Menu,
type View, type View,
type WebContents, type WebContents,
WebContentsView, WebContentsView,
@@ -103,7 +102,10 @@ type SeparateViewAction = {
type OpenInSplitViewAction = { type OpenInSplitViewAction = {
type: 'open-in-split-view'; type: 'open-in-split-view';
payload: { tabId: string }; payload: {
tabId: string;
view?: Omit<WorkbenchViewMeta, 'id'>;
};
}; };
type TabAction = type TabAction =
@@ -114,7 +116,7 @@ type TabAction =
| SeparateViewAction | SeparateViewAction
| OpenInSplitViewAction; | OpenInSplitViewAction;
type AddTabOption = { export type AddTabOption = {
basename?: string; basename?: string;
view?: Omit<WorkbenchViewMeta, 'id'> | Array<Omit<WorkbenchViewMeta, 'id'>>; view?: Omit<WorkbenchViewMeta, 'id'> | Array<Omit<WorkbenchViewMeta, 'id'>>;
target?: string; target?: string;
@@ -384,23 +386,19 @@ export class WebContentViewsManager {
} }
}; };
addTab = async (option?: AddTabOption) => { addTab = async (option: AddTabOption = {}) => {
if (!option) { const activeWorkbench = this.activeWorkbenchMeta;
const activeWorkbench = this.activeWorkbenchMeta;
const basename = activeWorkbench?.basename ?? '/'; option.basename ??= activeWorkbench?.basename ?? '/';
option.view ??= {
title: 'New Tab',
path: option.basename?.startsWith('/workspace')
? {
pathname: '/all',
}
: undefined,
};
option = {
basename,
view: {
title: 'New Tab',
path: basename.startsWith('/workspace')
? {
pathname: '/all',
}
: undefined,
},
};
}
const workbenches = this.tabViewsMeta.workbenches; const workbenches = this.tabViewsMeta.workbenches;
const newKey = this.generateViewId('app'); const newKey = this.generateViewId('app');
const views = ( const views = (
@@ -420,7 +418,7 @@ export class WebContentViewsManager {
(option.edge === 'left' ? 0 : 1); (option.edge === 'left' ? 0 : 1);
const workbench: WorkbenchMeta = { const workbench: WorkbenchMeta = {
basename: option.basename ?? this.activeWorkbenchMeta?.basename ?? '/', basename: option.basename,
activeViewIndex: 0, activeViewIndex: 0,
views: views, views: views,
id: newKey, id: newKey,
@@ -588,14 +586,16 @@ export class WebContentViewsManager {
addTab(newTabMeta).catch(logger.error); addTab(newTabMeta).catch(logger.error);
}; };
openInSplitView = (tabId: string) => { openInSplitView = (payload: OpenInSplitViewAction['payload']) => {
const tabMeta = this.tabViewsMeta.workbenches.find(w => w.id === tabId); const tabMeta = this.tabViewsMeta.workbenches.find(
w => w.id === payload.tabId
);
if (!tabMeta) { if (!tabMeta) {
return; return;
} }
this.tabAction$.next({ this.tabAction$.next({
type: 'open-in-split-view', type: 'open-in-split-view',
payload: { tabId }, payload: payload,
}); });
}; };
@@ -954,6 +954,7 @@ export const closeTab = WebContentViewsManager.instance.closeTab;
export const undoCloseTab = WebContentViewsManager.instance.undoCloseTab; export const undoCloseTab = WebContentViewsManager.instance.undoCloseTab;
export const activateView = WebContentViewsManager.instance.activateView; export const activateView = WebContentViewsManager.instance.activateView;
export const moveTab = WebContentViewsManager.instance.moveTab; export const moveTab = WebContentViewsManager.instance.moveTab;
export const openInSplitView = WebContentViewsManager.instance.openInSplitView;
export const reloadView = async () => { export const reloadView = async () => {
const id = WebContentViewsManager.instance.activeWorkbenchId; const id = WebContentViewsManager.instance.activeWorkbenchId;
@@ -993,88 +994,3 @@ export const pingAppLayoutReady = (wc: WebContents) => {
WebContentViewsManager.instance.setTabUIReady(viewId); WebContentViewsManager.instance.setTabUIReady(viewId);
} }
}; };
export const showTabContextMenu = async (tabId: string, viewIndex: number) => {
const workbenches = WebContentViewsManager.instance.tabViewsMeta.workbenches;
const unpinned = workbenches.filter(w => !w.pinned);
const tabMeta = workbenches.find(w => w.id === tabId);
if (!tabMeta) {
return;
}
const template: Parameters<typeof Menu.buildFromTemplate>[0] = [
tabMeta.pinned
? {
label: 'Unpin tab',
click: () => {
WebContentViewsManager.instance.pinTab(tabId, false);
},
}
: {
label: 'Pin tab',
click: () => {
WebContentViewsManager.instance.pinTab(tabId, true);
},
},
{
label: 'Refresh tab',
click: () => {
reloadView().catch(logger.error);
},
},
{
label: 'Duplicate tab',
click: () => {
addTab({
basename: tabMeta.basename,
view: tabMeta.views,
show: false,
}).catch(logger.error);
},
},
{ type: 'separator' },
tabMeta.views.length > 1
? {
label: 'Separate tabs',
click: () => {
WebContentViewsManager.instance.separateView(tabId, viewIndex);
},
}
: {
label: 'Open in split view',
click: () => {
WebContentViewsManager.instance.openInSplitView(tabId);
},
},
...(unpinned.length > 0
? ([
{ type: 'separator' },
{
label: 'Close tab',
click: () => {
closeTab(tabId).catch(logger.error);
},
},
{
label: 'Close other tabs',
click: () => {
const tabsToRetain =
WebContentViewsManager.instance.tabViewsMeta.workbenches.filter(
w => w.id === tabId || w.pinned
);
WebContentViewsManager.instance.patchTabViewsMeta({
workbenches: tabsToRetain,
activeWorkbenchId: tabId,
});
},
},
] as const)
: []),
];
const menu = Menu.buildFromTemplate(template);
menu.popup();
};

View File

@@ -25,7 +25,8 @@ test('click btn bew page and open in tab', async ({ page, workspace }) => {
page.getByRole('menuitem', { name: 'Open in new tab' }).click(), page.getByRole('menuitem', { name: 'Open in new tab' }).click(),
]); ]);
expect(newTabPage.url()).toBe(newPageUrl); await expect(newTabPage).toHaveURL(newPageUrl, { timeout: 15000 });
const currentWorkspace = await workspace.current(); const currentWorkspace = await workspace.current();
expect(currentWorkspace.meta.flavour).toContain('local'); expect(currentWorkspace.meta.flavour).toContain('local');