mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 10:22:55 +08:00
@@ -18,6 +18,7 @@ type NavigationEvents =
|
|||||||
| 'openInSplitView'
|
| 'openInSplitView'
|
||||||
| 'switchTab'
|
| 'switchTab'
|
||||||
| 'switchSplitView'
|
| 'switchSplitView'
|
||||||
|
| 'tabAction'
|
||||||
| 'navigate'
|
| 'navigate'
|
||||||
| 'goBack'
|
| 'goBack'
|
||||||
| 'goForward'
|
| 'goForward'
|
||||||
@@ -229,6 +230,9 @@ const PageEvents = {
|
|||||||
storage: ['viewPlans'],
|
storage: ['viewPlans'],
|
||||||
aiAction: ['viewPlans'],
|
aiAction: ['viewPlans'],
|
||||||
},
|
},
|
||||||
|
appTabsHeader: {
|
||||||
|
$: ['tabAction'],
|
||||||
|
},
|
||||||
header: {
|
header: {
|
||||||
actions: [
|
actions: [
|
||||||
'createDoc',
|
'createDoc',
|
||||||
@@ -319,6 +323,24 @@ type PaymentEventArgs = {
|
|||||||
recurring: string;
|
recurring: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type TabActionControlType =
|
||||||
|
| 'click'
|
||||||
|
| 'dnd'
|
||||||
|
| 'midClick'
|
||||||
|
| 'xButton'
|
||||||
|
| 'contextMenu';
|
||||||
|
type TabActionType =
|
||||||
|
| 'pin'
|
||||||
|
| 'unpin'
|
||||||
|
| 'close'
|
||||||
|
| 'refresh'
|
||||||
|
| 'moveTab'
|
||||||
|
| 'openInSplitView'
|
||||||
|
| 'openInNewTab'
|
||||||
|
| 'switchSplitView'
|
||||||
|
| 'switchTab'
|
||||||
|
| 'separateTabs';
|
||||||
|
|
||||||
export type EventArgs = {
|
export type EventArgs = {
|
||||||
createWorkspace: { flavour: string };
|
createWorkspace: { flavour: string };
|
||||||
oauth: { provider: string };
|
oauth: { provider: string };
|
||||||
@@ -342,6 +364,11 @@ export type EventArgs = {
|
|||||||
orderOrganizeItem: OrganizeItemArgs;
|
orderOrganizeItem: OrganizeItemArgs;
|
||||||
openInNewTab: { type: OrganizeItemType };
|
openInNewTab: { type: OrganizeItemType };
|
||||||
openInSplitView: { type: OrganizeItemType };
|
openInSplitView: { type: OrganizeItemType };
|
||||||
|
tabAction: {
|
||||||
|
type?: OrganizeItemType;
|
||||||
|
control: TabActionControlType;
|
||||||
|
action: TabActionType;
|
||||||
|
};
|
||||||
toggleFavorite: OrganizeItemArgs & { on: boolean };
|
toggleFavorite: OrganizeItemArgs & { on: boolean };
|
||||||
createDoc: { mode?: 'edgeless' | 'page' };
|
createDoc: { mode?: 'edgeless' | 'page' };
|
||||||
switchPageMode: { mode: 'edgeless' | 'page' };
|
switchPageMode: { mode: 'edgeless' | 'page' };
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { appSidebarWidthAtom } from '@affine/core/components/app-sidebar/index.j
|
|||||||
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
|
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
|
||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import { useCatchEventCallback } from '@affine/core/hooks/use-catch-event-hook';
|
import { useCatchEventCallback } from '@affine/core/hooks/use-catch-event-hook';
|
||||||
|
import { track } from '@affine/core/mixpanel';
|
||||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||||
import { apis, events } from '@affine/electron-api';
|
import { apis, events } from '@affine/electron-api';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
@@ -82,20 +83,78 @@ const WorkbenchTab = ({
|
|||||||
const activeViewIndex = workbench.activeViewIndex ?? 0;
|
const activeViewIndex = workbench.activeViewIndex ?? 0;
|
||||||
const onContextMenu = useAsyncCallback(
|
const onContextMenu = useAsyncCallback(
|
||||||
async (viewIdx: number) => {
|
async (viewIdx: number) => {
|
||||||
await tabsHeaderService.showContextMenu?.(workbench.id, viewIdx);
|
const action = await tabsHeaderService.showContextMenu?.(
|
||||||
|
workbench.id,
|
||||||
|
viewIdx
|
||||||
|
);
|
||||||
|
switch (action?.type) {
|
||||||
|
case 'open-in-split-view': {
|
||||||
|
track.$.appTabsHeader.$.tabAction({
|
||||||
|
control: 'contextMenu',
|
||||||
|
action: 'openInSplitView',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'separate-view': {
|
||||||
|
track.$.appTabsHeader.$.tabAction({
|
||||||
|
control: 'contextMenu',
|
||||||
|
action: 'separateTabs',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'pin-tab': {
|
||||||
|
if (action.payload.shouldPin) {
|
||||||
|
track.$.appTabsHeader.$.tabAction({
|
||||||
|
control: 'contextMenu',
|
||||||
|
action: 'pin',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
track.$.appTabsHeader.$.tabAction({
|
||||||
|
control: 'contextMenu',
|
||||||
|
action: 'unpin',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// fixme: when close tab the view may already be gc'ed
|
||||||
|
case 'close-tab': {
|
||||||
|
track.$.appTabsHeader.$.tabAction({
|
||||||
|
control: 'contextMenu',
|
||||||
|
action: 'close',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[tabsHeaderService, workbench.id]
|
[tabsHeaderService, workbench.id]
|
||||||
);
|
);
|
||||||
const onActivateView = useAsyncCallback(
|
const onActivateView = useAsyncCallback(
|
||||||
async (viewIdx: number) => {
|
async (viewIdx: number) => {
|
||||||
await tabsHeaderService.activateView?.(workbench.id, viewIdx);
|
await tabsHeaderService.activateView?.(workbench.id, viewIdx);
|
||||||
|
if (tabActive) {
|
||||||
|
track.$.appTabsHeader.$.tabAction({
|
||||||
|
control: 'click',
|
||||||
|
action: 'switchSplitView',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
track.$.appTabsHeader.$.tabAction({
|
||||||
|
control: 'click',
|
||||||
|
action: 'switchTab',
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[tabsHeaderService, workbench.id]
|
[tabActive, tabsHeaderService, workbench.id]
|
||||||
);
|
);
|
||||||
const handleAuxClick: MouseEventHandler = useCatchEventCallback(
|
const handleAuxClick: MouseEventHandler = useCatchEventCallback(
|
||||||
async e => {
|
async e => {
|
||||||
if (e.button === 1) {
|
if (e.button === 1) {
|
||||||
await tabsHeaderService.closeTab?.(workbench.id);
|
await tabsHeaderService.closeTab?.(workbench.id);
|
||||||
|
track.$.appTabsHeader.$.tabAction({
|
||||||
|
control: 'midClick',
|
||||||
|
action: 'close',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[tabsHeaderService, workbench.id]
|
[tabsHeaderService, workbench.id]
|
||||||
@@ -103,6 +162,10 @@ const WorkbenchTab = ({
|
|||||||
|
|
||||||
const handleCloseTab = useCatchEventCallback(async () => {
|
const handleCloseTab = useCatchEventCallback(async () => {
|
||||||
await tabsHeaderService.closeTab?.(workbench.id);
|
await tabsHeaderService.closeTab?.(workbench.id);
|
||||||
|
track.$.appTabsHeader.$.tabAction({
|
||||||
|
control: 'xButton',
|
||||||
|
action: 'close',
|
||||||
|
});
|
||||||
}, [tabsHeaderService, workbench.id]);
|
}, [tabsHeaderService, workbench.id]);
|
||||||
|
|
||||||
const { dropTargetRef, closestEdge } = useDropTarget<AffineDNDData>(
|
const { dropTargetRef, closestEdge } = useDropTarget<AffineDNDData>(
|
||||||
@@ -243,6 +306,10 @@ export const AppTabsHeader = ({
|
|||||||
|
|
||||||
const onAddTab = useAsyncCallback(async () => {
|
const onAddTab = useAsyncCallback(async () => {
|
||||||
await tabsHeaderService.onAddTab?.();
|
await tabsHeaderService.onAddTab?.();
|
||||||
|
track.$.appTabsHeader.$.tabAction({
|
||||||
|
control: 'click',
|
||||||
|
action: 'openInNewTab',
|
||||||
|
});
|
||||||
}, [tabsHeaderService]);
|
}, [tabsHeaderService]);
|
||||||
|
|
||||||
const onToggleRightSidebar = useAsyncCallback(async () => {
|
const onToggleRightSidebar = useAsyncCallback(async () => {
|
||||||
@@ -268,6 +335,10 @@ export const AppTabsHeader = ({
|
|||||||
if (targetId === data.source.data.from.tabId) {
|
if (targetId === data.source.data.from.tabId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
track.$.appTabsHeader.$.tabAction({
|
||||||
|
control: 'dnd',
|
||||||
|
action: 'moveTab',
|
||||||
|
});
|
||||||
return await tabsHeaderService.moveTab?.(
|
return await tabsHeaderService.moveTab?.(
|
||||||
data.source.data.from.tabId,
|
data.source.data.from.tabId,
|
||||||
targetId,
|
targetId,
|
||||||
@@ -276,6 +347,11 @@ export const AppTabsHeader = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data.source.data.entity?.type === 'doc') {
|
if (data.source.data.entity?.type === 'doc') {
|
||||||
|
track.$.appTabsHeader.$.tabAction({
|
||||||
|
control: 'dnd',
|
||||||
|
action: 'openInNewTab',
|
||||||
|
type: 'doc',
|
||||||
|
});
|
||||||
return await tabsHeaderService.onAddDocTab?.(
|
return await tabsHeaderService.onAddDocTab?.(
|
||||||
data.source.data.entity.id,
|
data.source.data.entity.id,
|
||||||
targetId,
|
targetId,
|
||||||
@@ -284,6 +360,11 @@ export const AppTabsHeader = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data.source.data.entity?.type === 'tag') {
|
if (data.source.data.entity?.type === 'tag') {
|
||||||
|
track.$.appTabsHeader.$.tabAction({
|
||||||
|
type: 'tag',
|
||||||
|
control: 'dnd',
|
||||||
|
action: 'openInNewTab',
|
||||||
|
});
|
||||||
return await tabsHeaderService.onAddTagTab?.(
|
return await tabsHeaderService.onAddTagTab?.(
|
||||||
data.source.data.entity.id,
|
data.source.data.entity.id,
|
||||||
targetId,
|
targetId,
|
||||||
@@ -292,6 +373,11 @@ export const AppTabsHeader = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data.source.data.entity?.type === 'collection') {
|
if (data.source.data.entity?.type === 'collection') {
|
||||||
|
track.$.appTabsHeader.$.tabAction({
|
||||||
|
type: 'collection',
|
||||||
|
control: 'dnd',
|
||||||
|
action: 'openInNewTab',
|
||||||
|
});
|
||||||
return await tabsHeaderService.onAddCollectionTab?.(
|
return await tabsHeaderService.onAddCollectionTab?.(
|
||||||
data.source.data.entity.id,
|
data.source.data.entity.id,
|
||||||
targetId,
|
targetId,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
addTab,
|
addTab,
|
||||||
closeTab,
|
closeTab,
|
||||||
reloadView,
|
reloadView,
|
||||||
|
type TabAction,
|
||||||
WebContentViewsManager,
|
WebContentViewsManager,
|
||||||
} from './tab-views';
|
} from './tab-views';
|
||||||
|
|
||||||
@@ -15,6 +16,8 @@ export const showTabContextMenu = async (tabId: string, viewIndex: number) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { resolve, promise } = Promise.withResolvers<TabAction | null>();
|
||||||
|
|
||||||
const template: Parameters<typeof Menu.buildFromTemplate>[0] = [
|
const template: Parameters<typeof Menu.buildFromTemplate>[0] = [
|
||||||
tabMeta.pinned
|
tabMeta.pinned
|
||||||
? {
|
? {
|
||||||
@@ -90,4 +93,22 @@ export const showTabContextMenu = async (tabId: string, viewIndex: number) => {
|
|||||||
];
|
];
|
||||||
const menu = Menu.buildFromTemplate(template);
|
const menu = Menu.buildFromTemplate(template);
|
||||||
menu.popup();
|
menu.popup();
|
||||||
|
// eslint-disable-next-line prefer-const
|
||||||
|
let unsub: (() => void) | undefined;
|
||||||
|
const subscription = WebContentViewsManager.instance.tabAction$.subscribe(
|
||||||
|
action => {
|
||||||
|
resolve(action);
|
||||||
|
unsub?.();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
menu.on('menu-will-close', () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve(null);
|
||||||
|
unsub?.();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
unsub = () => {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
};
|
||||||
|
return promise;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ type OpenInSplitViewAction = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type TabAction =
|
export type TabAction =
|
||||||
| AddTabAction
|
| AddTabAction
|
||||||
| CloseTabAction
|
| CloseTabAction
|
||||||
| PinTabAction
|
| PinTabAction
|
||||||
|
|||||||
Reference in New Issue
Block a user