refactor(electron): tab title/icon update logic (#7675)

fix AF-1122
fix AF-1136
This commit is contained in:
pengx17
2024-08-01 16:43:18 +00:00
parent e60b2d64e5
commit 07409b8a91
20 changed files with 261 additions and 217 deletions

View File

@@ -7,21 +7,8 @@ import {
import { appSidebarWidthAtom } from '@affine/core/components/app-sidebar/index.jotai'; import { appSidebarWidthAtom } from '@affine/core/components/app-sidebar/index.jotai';
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 { DesktopStateSynchronizer } from '@affine/core/modules/workbench/services/desktop-state-synchronizer';
import type { WorkbenchMeta } from '@affine/electron-api';
import { apis, events } from '@affine/electron-api'; import { apis, events } from '@affine/electron-api';
import { import { CloseIcon, PlusIcon, RightSidebarIcon } from '@blocksuite/icons/rc';
AllDocsIcon,
CloseIcon,
DeleteIcon,
EdgelessIcon,
PageIcon,
PlusIcon,
RightSidebarIcon,
TagIcon,
TodayIcon,
ViewLayersIcon,
} from '@blocksuite/icons/rc';
import { import {
useLiveData, useLiveData,
useService, useService,
@@ -38,25 +25,14 @@ import {
useState, useState,
} from 'react'; } from 'react';
import { iconNameToIcon } from '../../workbench/constants';
import { DesktopStateSynchronizer } from '../../workbench/services/desktop-state-synchronizer';
import { import {
AppTabsHeaderService, AppTabsHeaderService,
type TabStatus, type TabStatus,
} from '../services/app-tabs-header-service'; } from '../services/app-tabs-header-service';
import * as styles from './styles.css'; import * as styles from './styles.css';
type ModuleName = NonNullable<WorkbenchMeta['views'][0]['moduleName']>;
const moduleNameToIcon = {
all: <AllDocsIcon />,
collection: <ViewLayersIcon />,
doc: <PageIcon />,
page: <PageIcon />,
edgeless: <EdgelessIcon />,
journal: <TodayIcon />,
tag: <TagIcon />,
trash: <DeleteIcon />,
} satisfies Record<ModuleName, ReactNode>;
const WorkbenchTab = ({ const WorkbenchTab = ({
workbench, workbench,
active: tabActive, active: tabActive,
@@ -66,6 +42,7 @@ const WorkbenchTab = ({
active: boolean; active: boolean;
tabsLength: number; tabsLength: number;
}) => { }) => {
useServiceOptional(DesktopStateSynchronizer);
const tabsHeaderService = useService(AppTabsHeaderService); const tabsHeaderService = useService(AppTabsHeaderService);
const activeViewIndex = workbench.activeViewIndex ?? 0; const activeViewIndex = workbench.activeViewIndex ?? 0;
const onContextMenu = useAsyncCallback( const onContextMenu = useAsyncCallback(
@@ -115,7 +92,7 @@ const WorkbenchTab = ({
> >
<div className={styles.labelIcon}> <div className={styles.labelIcon}>
{workbench.ready || !workbench.loaded ? ( {workbench.ready || !workbench.loaded ? (
moduleNameToIcon[view.moduleName ?? 'all'] iconNameToIcon[view.iconName ?? 'allDocs']
) : ( ) : (
<Loading /> <Loading />
)} )}
@@ -194,8 +171,6 @@ export const AppTabsHeader = ({
await tabsHeaderService.onToggleRightSidebar(); await tabsHeaderService.onToggleRightSidebar();
}, [tabsHeaderService]); }, [tabsHeaderService]);
useServiceOptional(DesktopStateSynchronizer);
useEffect(() => { useEffect(() => {
if (mode === 'app') { if (mode === 'app') {
apis?.ui.pingAppLayoutReady().catch(console.error); apis?.ui.pingAppLayoutReady().catch(console.error);

View File

@@ -80,7 +80,7 @@ export const tab = style({
boxShadow: cssVar('shadow1'), boxShadow: cssVar('shadow1'),
}, },
'&[data-pinned="false"]': { '&[data-pinned="false"]': {
paddingRight: 8, paddingRight: 20,
}, },
'&[data-pinned="true"]': { '&[data-pinned="true"]': {
flexShrink: 0, flexShrink: 0,

View File

@@ -0,0 +1,23 @@
import {
AllDocsIcon,
DeleteIcon,
EdgelessIcon,
PageIcon,
TagIcon,
TodayIcon,
ViewLayersIcon,
} from '@blocksuite/icons/rc';
import type { ReactNode } from 'react';
export const iconNameToIcon = {
allDocs: <AllDocsIcon />,
collection: <ViewLayersIcon />,
doc: <PageIcon />,
page: <PageIcon />,
edgeless: <EdgelessIcon />,
journal: <TodayIcon />,
tag: <TagIcon />,
trash: <DeleteIcon />,
} satisfies Record<string, ReactNode>;
export type ViewIconName = keyof typeof iconNameToIcon;

View File

@@ -3,12 +3,15 @@ import type { Location, To } from 'history';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { createNavigableHistory } from '../../../utils/navigable-history'; import { createNavigableHistory } from '../../../utils/navigable-history';
import type { ViewIconName } from '../constants';
import { ViewScope } from '../scopes/view'; import { ViewScope } from '../scopes/view';
import { SidebarTab } from './sidebar-tab'; import { SidebarTab } from './sidebar-tab';
export class View extends Entity<{ export class View extends Entity<{
id: string; id: string;
defaultLocation?: To | undefined; defaultLocation?: To | undefined;
title?: string;
icon?: ViewIconName;
}> { }> {
scope = this.framework.createScope(ViewScope, { scope = this.framework.createScope(ViewScope, {
view: this as View, view: this as View,
@@ -70,6 +73,10 @@ export class View extends Entity<{
size$ = new LiveData(100); size$ = new LiveData(100);
title$ = new LiveData(this.props.title ?? '');
icon$ = new LiveData(this.props.icon ?? 'allDocs');
push(path: To) { push(path: To) {
this.history.push(path); this.history.push(path);
} }
@@ -105,4 +112,12 @@ export class View extends Entity<{
activeSidebarTab(id: string | null) { activeSidebarTab(id: string | null) {
this._activeSidebarTabId$.next(id); this._activeSidebarTabId$.next(id);
} }
setTitle(title: string) {
this.title$.next(title);
}
setIcon(icon: ViewIconName) {
this.icon$.next(icon);
}
} }

View File

@@ -3,17 +3,16 @@ export { ViewScope } from './scopes/view';
export { WorkbenchService } from './services/workbench'; export { WorkbenchService } from './services/workbench';
export { useIsActiveView } from './view/use-is-active-view'; export { useIsActiveView } from './view/use-is-active-view';
export { ViewBody, ViewHeader, ViewSidebarTab } from './view/view-islands'; export { ViewBody, ViewHeader, ViewSidebarTab } from './view/view-islands';
export { ViewIcon, ViewTitle } from './view/view-meta';
export { WorkbenchLink } from './view/workbench-link'; export { WorkbenchLink } from './view/workbench-link';
export { WorkbenchRoot } from './view/workbench-root'; export { WorkbenchRoot } from './view/workbench-root';
import { import {
DocsService,
type Framework, type Framework,
GlobalStateService, GlobalStateService,
WorkspaceScope, WorkspaceScope,
} from '@toeverything/infra'; } from '@toeverything/infra';
import { WorkspacePropertiesAdapter } from '../properties';
import { SidebarTab } from './entities/sidebar-tab'; import { SidebarTab } from './entities/sidebar-tab';
import { View } from './entities/view'; import { View } from './entities/view';
import { Workbench } from './entities/workbench'; import { Workbench } from './entities/workbench';
@@ -52,9 +51,5 @@ export function configureDesktopWorkbenchModule(services: Framework) {
.impl(WorkbenchDefaultState, DesktopWorkbenchDefaultState, [ .impl(WorkbenchDefaultState, DesktopWorkbenchDefaultState, [
GlobalStateService, GlobalStateService,
]) ])
.service(DesktopStateSynchronizer, [ .service(DesktopStateSynchronizer, [WorkbenchService]);
WorkbenchService,
WorkspacePropertiesAdapter,
DocsService,
]);
} }

View File

@@ -1,35 +1,13 @@
import { import { apis, appInfo, events } from '@affine/electron-api';
apis, import { LiveData, Service } from '@toeverything/infra';
appInfo,
events,
type WorkbenchViewMeta,
} from '@affine/electron-api';
import { I18n, type I18nKeys, i18nTime } from '@affine/i18n';
import type { DocsService } from '@toeverything/infra';
import { Service } from '@toeverything/infra';
import { combineLatest, filter, map, of, switchMap } from 'rxjs';
import { resolveRouteLinkMeta } from '../../navigation';
import type { RouteModulePath } from '../../navigation/utils';
import type { WorkspacePropertiesAdapter } from '../../properties';
import type { WorkbenchService } from '../../workbench'; import type { WorkbenchService } from '../../workbench';
const routeModuleToI18n = {
all: 'All pages',
collection: 'Collections',
tag: 'Tags',
trash: 'Trash',
} satisfies Record<RouteModulePath, I18nKeys>;
/** /**
* Synchronize workbench state with state stored in main process * Synchronize workbench state with state stored in main process
*/ */
export class DesktopStateSynchronizer extends Service { export class DesktopStateSynchronizer extends Service {
constructor( constructor(private readonly workbenchService: WorkbenchService) {
private readonly workbenchService: WorkbenchService,
private readonly workspaceProperties: WorkspacePropertiesAdapter,
private readonly docsService: DocsService
) {
super(); super();
this.startSync(); this.startSync();
} }
@@ -80,70 +58,31 @@ export class DesktopStateSynchronizer extends Service {
// sync workbench state with main process // sync workbench state with main process
// also fill tab view meta with title & moduleName // also fill tab view meta with title & moduleName
this.workspaceProperties.workspace.engine.rootDocState$ LiveData.computed(get => {
.pipe( return get(workbench.views$).map(view => {
filter(v => v.ready), const location = get(view.location$);
switchMap(() => workbench.views$), return {
switchMap(views => { id: view.id,
return combineLatest( title: get(view.title$),
views.map(view => iconName: get(view.icon$),
view.location$.map(location => { path: {
return { pathname: location.pathname,
view, search: location.search,
location, hash: location.hash,
}; },
}) };
)
);
}),
map(viewLocations => {
if (!apis || !appInfo?.viewId) {
return;
}
const viewMetas = viewLocations.map(({ view, location }) => {
return {
id: view.id,
path: location,
};
});
return viewMetas.map(viewMeta => this.fillTabViewMeta(viewMeta));
}),
filter(v => !!v),
switchMap(viewMetas => {
return this.docsService.list.docs$.pipe(
switchMap(docs => {
return combineLatest(
viewMetas.map(vm => {
return (
docs
.find(doc => doc.id === vm.docId)
?.mode$.asObservable() ?? of('page')
).pipe(
map(mode => ({
...vm,
moduleName:
vm.moduleName === 'page' ? mode : vm.moduleName,
}))
);
})
);
})
);
})
)
.subscribe(viewMetas => {
if (!apis || !appInfo?.viewId) {
return;
}
apis.ui
.updateWorkbenchMeta(appInfo.viewId, {
views: viewMetas,
})
.catch(console.error);
}); });
}).subscribe(views => {
if (!apis || !appInfo?.viewId) {
return;
}
apis.ui
.updateWorkbenchMeta(appInfo.viewId, {
views,
})
.catch(console.error);
});
workbench.activeViewIndex$.subscribe(activeViewIndex => { workbench.activeViewIndex$.subscribe(activeViewIndex => {
if (!apis || !appInfo?.viewId) { if (!apis || !appInfo?.viewId) {
@@ -169,59 +108,4 @@ export class DesktopStateSynchronizer extends Service {
.catch(console.error); .catch(console.error);
}); });
}; };
private toFullUrl(
basename: string,
location: { hash?: string; pathname?: string; search?: string }
) {
return basename + location.pathname + location.search + location.hash;
}
// fill tab view meta with title & moduleName
private fillTabViewMeta(
view: WorkbenchViewMeta
): WorkbenchViewMeta & { docId?: string } {
if (!view.path) {
return view;
}
const url = this.toFullUrl(
this.workbenchService.workbench.basename$.value,
view.path
);
const linkMeta = resolveRouteLinkMeta(url);
if (!linkMeta) {
return view;
}
const journalString =
linkMeta.moduleName === 'doc'
? this.workspaceProperties.getJournalPageDateString(linkMeta.docId)
: undefined;
const isJournal = !!journalString;
const title = (() => {
// todo: resolve more module types like collections?
if (linkMeta?.moduleName === 'doc') {
if (journalString) {
return i18nTime(journalString, { absolute: { accuracy: 'day' } });
}
return (
this.workspaceProperties.workspace.docCollection.meta.getDocMeta(
linkMeta.docId
)?.title || I18n['Untitled']()
);
} else {
return I18n[routeModuleToI18n[linkMeta.moduleName]]();
}
})();
return {
...view,
title: title,
docId: linkMeta.docId,
moduleName: isJournal ? 'journal' : linkMeta.moduleName,
};
}
} }

View File

@@ -21,6 +21,7 @@ export function useBindWorkbenchToDesktopRouter(
basename: string basename: string
) { ) {
const browserLocation = useLocation(); const browserLocation = useLocation();
useEffect(() => { useEffect(() => {
const newLocation = browserLocationToViewLocation( const newLocation = browserLocationToViewLocation(
browserLocation, browserLocation,
@@ -36,6 +37,7 @@ export function useBindWorkbenchToDesktopRouter(
) { ) {
return; return;
} }
workbench.open(newLocation); workbench.open(newLocation);
}, [basename, browserLocation, workbench]); }, [basename, browserLocation, workbench]);
} }

View File

@@ -0,0 +1,29 @@
import { useServiceOptional } from '@toeverything/infra';
import { useEffect } from 'react';
import type { ViewIconName } from '../constants';
import { ViewService } from '../services/view';
export const ViewTitle = ({ title }: { title: string }) => {
const view = useServiceOptional(ViewService)?.view;
useEffect(() => {
if (view) {
view.setTitle(title);
}
}, [title, view]);
return null;
};
export const ViewIcon = ({ icon }: { icon: ViewIconName }) => {
const view = useServiceOptional(ViewService)?.view;
useEffect(() => {
if (view) {
view.setIcon(icon);
}
}, [icon, view]);
return null;
};

View File

@@ -6,6 +6,10 @@ import {
VirtualizedCollectionList, VirtualizedCollectionList,
} from '@affine/core/components/page-list'; } from '@affine/core/components/page-list';
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper'; import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
import {
ViewIcon,
ViewTitle,
} from '@affine/core/modules/workbench/view/view-meta';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra'; import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
@@ -55,6 +59,8 @@ export const AllCollection = () => {
return ( return (
<> <>
<ViewTitle title={t['Collections']()} />
<ViewIcon icon="collection" />
<ViewHeader> <ViewHeader>
<AllCollectionHeader <AllCollectionHeader
showCreateNew={!hideHeaderCreateNew} showCreateNew={!hideHeaderCreateNew}

View File

@@ -6,6 +6,7 @@ import {
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta'; import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { performanceRenderLogger } from '@affine/core/shared'; import { performanceRenderLogger } from '@affine/core/shared';
import type { Filter } from '@affine/env/filter'; import type { Filter } from '@affine/env/filter';
import { useI18n } from '@affine/i18n';
import { import {
GlobalContextService, GlobalContextService,
useService, useService,
@@ -17,6 +18,8 @@ import {
useIsActiveView, useIsActiveView,
ViewBody, ViewBody,
ViewHeader, ViewHeader,
ViewIcon,
ViewTitle,
} from '../../../modules/workbench'; } from '../../../modules/workbench';
import { EmptyPageList } from '../page-list-empty'; import { EmptyPageList } from '../page-list-empty';
import * as styles from './all-page.css'; import * as styles from './all-page.css';
@@ -47,8 +50,12 @@ export const AllPage = () => {
return; return;
}, [globalContext, isActiveView]); }, [globalContext, isActiveView]);
const t = useI18n();
return ( return (
<> <>
<ViewTitle title={t['All pages']()} />
<ViewIcon icon="allDocs" />
<ViewHeader> <ViewHeader>
<AllPageHeader <AllPageHeader
showCreateNew={!hideHeaderCreateNew} showCreateNew={!hideHeaderCreateNew}

View File

@@ -5,10 +5,16 @@ import {
import { CreateOrEditTag } from '@affine/core/components/page-list/tags/create-tag'; import { CreateOrEditTag } from '@affine/core/components/page-list/tags/create-tag';
import type { TagMeta } from '@affine/core/components/page-list/types'; import type { TagMeta } from '@affine/core/components/page-list/types';
import { DeleteTagConfirmModal, TagService } from '@affine/core/modules/tag'; import { DeleteTagConfirmModal, TagService } from '@affine/core/modules/tag';
import { useI18n } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { ViewBody, ViewHeader } from '../../../modules/workbench'; import {
ViewBody,
ViewHeader,
ViewIcon,
ViewTitle,
} from '../../../modules/workbench';
import { EmptyTagList } from '../page-list-empty'; import { EmptyTagList } from '../page-list-empty';
import * as styles from './all-tag.css'; import * as styles from './all-tag.css';
import { AllTagHeader } from './header'; import { AllTagHeader } from './header';
@@ -54,8 +60,12 @@ export const AllTag = () => {
[setOpen, setSelectedTagIds] [setOpen, setSelectedTagIds]
); );
const t = useI18n();
return ( return (
<> <>
<ViewTitle title={t['Tags']()} />
<ViewIcon icon="tag" />
<ViewHeader> <ViewHeader>
<AllTagHeader /> <AllTagHeader />
</ViewHeader> </ViewHeader>

View File

@@ -29,6 +29,8 @@ import {
useIsActiveView, useIsActiveView,
ViewBody, ViewBody,
ViewHeader, ViewHeader,
ViewIcon,
ViewTitle,
} from '../../../modules/workbench'; } from '../../../modules/workbench';
import { WorkspaceSubPath } from '../../../shared'; import { WorkspaceSubPath } from '../../../shared';
import * as styles from './collection.css'; import * as styles from './collection.css';
@@ -155,6 +157,8 @@ const Placeholder = ({ collection }: { collection: Collection }) => {
return ( return (
<> <>
<ViewTitle title={collection.name} />
<ViewIcon icon="collection" />
<ViewHeader> <ViewHeader>
<div <div
style={{ style={{

View File

@@ -13,9 +13,16 @@ import { PageHeaderMenuButton } from '@affine/core/components/blocksuite/block-s
import { DetailPageHeaderPresentButton } from '@affine/core/components/blocksuite/block-suite-header/present/detail-header-present-button'; import { DetailPageHeaderPresentButton } from '@affine/core/components/blocksuite/block-suite-header/present/detail-header-present-button';
import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch'; import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch';
import { useRegisterCopyLinkCommands } from '@affine/core/hooks/affine/use-register-copy-link-commands'; import { useRegisterCopyLinkCommands } from '@affine/core/hooks/affine/use-register-copy-link-commands';
import { useDocCollectionPageTitle } from '@affine/core/hooks/use-block-suite-workspace-page-title';
import { useJournalInfoHelper } from '@affine/core/hooks/use-journal'; import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
import { ViewIcon, ViewTitle } from '@affine/core/modules/workbench';
import type { Doc } from '@blocksuite/store'; import type { Doc } from '@blocksuite/store';
import { type Workspace } from '@toeverything/infra'; import {
DocService,
useLiveData,
useService,
type Workspace,
} from '@toeverything/infra';
import { useAtom, useAtomValue } from 'jotai'; import { useAtom, useAtomValue } from 'jotai';
import { forwardRef, useCallback, useEffect, useRef, useState } from 'react'; import { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
@@ -68,8 +75,11 @@ export function JournalPageHeader({ page, workspace }: PageHeaderProps) {
const { hideShare, hideToday } = const { hideShare, hideToday } =
useDetailPageHeaderResponsive(containerWidth); useDetailPageHeaderResponsive(containerWidth);
const title = useDocCollectionPageTitle(workspace.docCollection, page?.id);
return ( return (
<Header className={styles.header} ref={containerRef}> <Header className={styles.header} ref={containerRef}>
<ViewTitle title={title} />
<ViewIcon icon="journal" />
<EditorModeSwitch <EditorModeSwitch
docCollection={workspace.docCollection} docCollection={workspace.docCollection}
pageId={page?.id} pageId={page?.id}
@@ -115,8 +125,15 @@ export function NormalPageHeader({ page, workspace }: PageHeaderProps) {
const onRename = useCallback(() => { const onRename = useCallback(() => {
setTimeout(() => titleInputHandleRef.current?.triggerEdit()); setTimeout(() => titleInputHandleRef.current?.triggerEdit());
}, []); }, []);
const title = useDocCollectionPageTitle(workspace.docCollection, page?.id);
const doc = useService(DocService).doc;
const currentMode = useLiveData(doc.mode$);
return ( return (
<Header className={styles.header} ref={containerRef}> <Header className={styles.header} ref={containerRef}>
<ViewTitle title={title} />
<ViewIcon icon={currentMode ?? 'page'} />
<EditorModeSwitch <EditorModeSwitch
docCollection={workspace.docCollection} docCollection={workspace.docCollection}
pageId={page?.id} pageId={page?.id}

View File

@@ -8,6 +8,8 @@ import {
useIsActiveView, useIsActiveView,
ViewBody, ViewBody,
ViewHeader, ViewHeader,
ViewIcon,
ViewTitle,
} from '@affine/core/modules/workbench'; } from '@affine/core/modules/workbench';
import { import {
GlobalContextService, GlobalContextService,
@@ -39,6 +41,7 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => {
}, [pageIds, pageMetas]); }, [pageIds, pageMetas]);
const isActiveView = useIsActiveView(); const isActiveView = useIsActiveView();
const tagName = useLiveData(currentTag?.value$);
useEffect(() => { useEffect(() => {
if (isActiveView && currentTag) { if (isActiveView && currentTag) {
@@ -59,6 +62,8 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => {
return ( return (
<> <>
<ViewTitle title={tagName ?? 'Untitled'} />
<ViewIcon icon="tag" />
<ViewHeader> <ViewHeader>
<TagDetailHeader /> <TagDetailHeader />
</ViewHeader> </ViewHeader>

View File

@@ -14,7 +14,13 @@ import {
} from '@toeverything/infra'; } from '@toeverything/infra';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useIsActiveView, ViewBody, ViewHeader } from '../../modules/workbench'; import {
useIsActiveView,
ViewBody,
ViewHeader,
ViewIcon,
ViewTitle,
} from '../../modules/workbench';
import { EmptyPageList } from './page-list-empty'; import { EmptyPageList } from './page-list-empty';
import * as styles from './trash-page.css'; import * as styles from './trash-page.css';
@@ -56,8 +62,11 @@ export const TrashPage = () => {
return; return;
}, [globalContextService.globalContext.isTrash, isActiveView]); }, [globalContextService.globalContext.isTrash, isActiveView]);
const t = useI18n();
return ( return (
<> <>
<ViewTitle title={t['Trash']()} />
<ViewIcon icon={'trash'} />
<ViewHeader> <ViewHeader>
<TrashHeader /> <TrashHeader />
</ViewHeader> </ViewHeader>

View File

@@ -4,7 +4,6 @@ import '@affine/component/theme/theme.css';
import { NotificationCenter } from '@affine/component'; import { NotificationCenter } from '@affine/component';
import { AffineContext } from '@affine/component/context'; import { AffineContext } from '@affine/component/context';
import { GlobalLoading } from '@affine/component/global-loading'; import { GlobalLoading } from '@affine/component/global-loading';
import { registerAffineCommand } from '@affine/core/commands';
import { AppFallback } from '@affine/core/components/affine/app-container'; import { AppFallback } from '@affine/core/components/affine/app-container';
import { configureCommonModules } from '@affine/core/modules'; import { configureCommonModules } from '@affine/core/modules';
import { configureAppTabsHeaderModule } from '@affine/core/modules/app-tabs-header'; import { configureAppTabsHeaderModule } from '@affine/core/modules/app-tabs-header';
@@ -22,7 +21,6 @@ import {
import { Telemetry } from '@affine/core/telemetry'; import { Telemetry } from '@affine/core/telemetry';
import createEmotionCache from '@affine/core/utils/create-emotion-cache'; import createEmotionCache from '@affine/core/utils/create-emotion-cache';
import { createI18n, setUpLanguage } from '@affine/i18n'; import { createI18n, setUpLanguage } from '@affine/i18n';
import { SettingsIcon } from '@blocksuite/icons/rc';
import { CacheProvider } from '@emotion/react'; import { CacheProvider } from '@emotion/react';
import { import {
Framework, Framework,
@@ -127,14 +125,3 @@ export function App() {
</Suspense> </Suspense>
); );
} }
registerAffineCommand({
id: 'affine:reload',
category: 'affine:general',
label: 'Reload current tab',
icon: <SettingsIcon />,
keyBinding: '$mod+R',
run() {
location.reload();
},
});

View File

@@ -7,6 +7,7 @@ import {
addTab, addTab,
closeTab, closeTab,
initAndShowMainWindow, initAndShowMainWindow,
reloadView,
showDevTools, showDevTools,
showMainWindow, showMainWindow,
undoCloseTab, undoCloseTab,
@@ -92,8 +93,13 @@ export function createApplicationMenu() {
{ {
label: 'View', label: 'View',
submenu: [ submenu: [
{ role: 'reload' }, {
{ role: 'forceReload' }, label: 'Reload',
accelerator: 'CommandOrControl+R',
click() {
reloadView().catch(console.error);
},
},
{ {
label: 'Open devtools', label: 'Open devtools',
accelerator: isMac ? 'Cmd+Option+I' : 'Ctrl+Shift+I', accelerator: isMac ? 'Cmd+Option+I' : 'Ctrl+Shift+I',

View File

@@ -23,6 +23,7 @@ import {
showTab, showTab,
showTabContextMenu, showTabContextMenu,
updateWorkbenchMeta, updateWorkbenchMeta,
updateWorkbenchViewMeta,
} from '../windows-manager'; } from '../windows-manager';
import { getChallengeResponse } from './challenge'; import { getChallengeResponse } from './challenge';
import { uiSubjects } from './subject'; import { uiSubjects } from './subject';
@@ -168,6 +169,12 @@ export const uiHandlers = {
) => { ) => {
return updateWorkbenchMeta(...args); return updateWorkbenchMeta(...args);
}, },
updateWorkbenchViewMeta: async (
_,
...args: Parameters<typeof updateWorkbenchViewMeta>
) => {
return updateWorkbenchViewMeta(...args);
},
getTabViewsMeta: async () => { getTabViewsMeta: async () => {
return getTabViewsMeta(); return getTabViewsMeta();
}, },

View File

@@ -1,8 +1,8 @@
import { z } from 'zod'; import { z } from 'zod';
export const workbenchViewModuleSchema = z.enum([ export const workbenchViewIconNameSchema = z.enum([
'trash', 'trash',
'all', 'allDocs',
'collection', 'collection',
'tag', 'tag',
'doc', // refers to a doc whose mode is not yet being resolved 'doc', // refers to a doc whose mode is not yet being resolved
@@ -22,7 +22,7 @@ export const workbenchViewMetaSchema = z.object({
.optional(), .optional(),
// todo: move title/module to cached stated // todo: move title/module to cached stated
title: z.string().optional(), title: z.string().optional(),
moduleName: workbenchViewModuleSchema.optional(), iconName: workbenchViewIconNameSchema.optional(),
}); });
export const workbenchMetaSchema = z.object({ export const workbenchMetaSchema = z.object({
@@ -42,4 +42,4 @@ export const TabViewsMetaKey = 'tabViewsMetaSchema';
export type TabViewsMetaSchema = z.infer<typeof tabViewsMetaSchema>; export type TabViewsMetaSchema = z.infer<typeof tabViewsMetaSchema>;
export type WorkbenchMeta = z.infer<typeof workbenchMetaSchema>; export type WorkbenchMeta = z.infer<typeof workbenchMetaSchema>;
export type WorkbenchViewMeta = z.infer<typeof workbenchViewMetaSchema>; export type WorkbenchViewMeta = z.infer<typeof workbenchViewMetaSchema>;
export type WorkbenchViewModule = z.infer<typeof workbenchViewModuleSchema>; export type WorkbenchViewModule = z.infer<typeof workbenchViewIconNameSchema>;

View File

@@ -258,15 +258,50 @@ export class WebContentViewsManager {
if (index === -1) { if (index === -1) {
return; return;
} }
const workbench = workbenches[index];
const newWorkbenches = workbenches.toSpliced(index, 1, { const newWorkbenches = workbenches.toSpliced(index, 1, {
...workbenches[index], ...workbench,
...patch, ...patch,
views: patch.views
? patch.views.map(v => {
const existing = workbench.views.find(e => e.id === v.id);
return {
...existing,
...v,
};
})
: workbench.views,
}); });
this.patchTabViewsMeta({ this.patchTabViewsMeta({
workbenches: newWorkbenches, workbenches: newWorkbenches,
}); });
}; };
updateWorkbenchViewMeta = (
workbenchId: string,
viewId: string,
patch: Partial<WorkbenchViewMeta>
) => {
const workbench = this.tabViewsMeta.workbenches.find(
w => w.id === workbenchId
);
if (!workbench) {
return;
}
const views = workbench.views;
const viewIndex = views.findIndex(v => v.id === viewId);
if (viewIndex === -1) {
return;
}
const newViews = views.toSpliced(viewIndex, 1, {
...views[viewIndex],
...patch,
});
this.updateWorkbenchMeta(workbenchId, {
views: newViews,
});
};
isActiveTab = (id: string) => { isActiveTab = (id: string) => {
return this.activeWorkbenchId === id; return this.activeWorkbenchId === id;
}; };
@@ -345,10 +380,18 @@ export class WebContentViewsManager {
addTab = async (option?: AddTabOption) => { addTab = async (option?: AddTabOption) => {
if (!option) { if (!option) {
const activeWorkbench = this.activeWorkbenchMeta;
const basename = (activeWorkbench?.basename ?? '') + '/';
option = { option = {
basename: '/', basename,
view: { view: {
title: 'New Tab', title: 'New Tab',
path: basename.startsWith('/workspace')
? {
pathname: 'all',
}
: undefined,
}, },
}; };
} }
@@ -393,18 +436,18 @@ export class WebContentViewsManager {
let view = this.tabViewsMap.get(id); let view = this.tabViewsMap.get(id);
if (!view) { if (!view) {
view = await this.createAndAddView('app', id); view = await this.createAndAddView('app', id);
const workbench = this.tabViewsMeta.workbenches.find(w => w.id === id); }
const viewMeta = workbench?.views[workbench.activeViewIndex]; const workbench = this.tabViewsMeta.workbenches.find(w => w.id === id);
if (workbench && viewMeta) { const viewMeta = workbench?.views[workbench.activeViewIndex];
const url = new URL( if (workbench && viewMeta) {
workbench.basename + (viewMeta.path?.pathname ?? ''), const url = new URL(
mainWindowOrigin workbench.basename + (viewMeta.path?.pathname ?? ''),
); mainWindowOrigin
url.hash = viewMeta.path?.hash ?? ''; );
url.search = viewMeta.path?.search ?? ''; url.hash = viewMeta.path?.hash ?? '';
logger.info(`loading tab ${id} at ${url.href}`); url.search = viewMeta.path?.search ?? '';
view.webContents.loadURL(url.href).catch(logger.error); logger.info(`loading tab ${id} at ${url.href}`);
} view.webContents.loadURL(url.href).catch(logger.error);
} }
return view; return view;
}; };
@@ -835,6 +878,19 @@ export const updateWorkbenchMeta = (
) => { ) => {
WebContentViewsManager.instance.updateWorkbenchMeta(id, meta); WebContentViewsManager.instance.updateWorkbenchMeta(id, meta);
}; };
export const updateWorkbenchViewMeta = (
workbenchId: string,
viewId: string,
meta: Partial<WorkbenchViewMeta>
) => {
WebContentViewsManager.instance.updateWorkbenchViewMeta(
workbenchId,
viewId,
meta
);
};
export const getWorkbenchMeta = (id: string) => { export const getWorkbenchMeta = (id: string) => {
return TabViewsMetaState.value.workbenches.find(w => w.id === id); return TabViewsMetaState.value.workbenches.find(w => w.id === id);
}; };
@@ -851,6 +907,13 @@ 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 reloadView = async () => {
const id = WebContentViewsManager.instance.activeWorkbenchId;
if (id) {
await WebContentViewsManager.instance.loadTab(id);
}
};
export const onTabAction = (fn: (event: TabAction) => void) => { export const onTabAction = (fn: (event: TabAction) => void) => {
const { unsubscribe } = const { unsubscribe } =
WebContentViewsManager.instance.tabAction$.subscribe(fn); WebContentViewsManager.instance.tabAction$.subscribe(fn);