mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 18:26:05 +08:00
refactor(core): workbench (#7355)
Merge the right sidebar logic into the workbench. this can simplify our logic. Previously we had 3 modules * workbench * right-sidebar (Control sidebar open&close) * multi-tab-sidebar (Control tabs) Now everything is managed in Workbench. # Behavioral changes The sidebar button is always visible and can be opened at any time. If there is no content to display, will be `No Selection`  Elements in the sidebar can now be defined as`unmountOnInactive=false`. Inactive sidebars are marked with `display: none` but not unmount, so the `ChatPanel` can always remain in the DOM and user input will be retained even if the sidebar is closed.
This commit is contained in:
@@ -39,6 +39,7 @@ export interface ResizePanelProps
|
|||||||
resizeHandleVerticalPadding?: number;
|
resizeHandleVerticalPadding?: number;
|
||||||
enableAnimation?: boolean;
|
enableAnimation?: boolean;
|
||||||
width: number;
|
width: number;
|
||||||
|
unmountOnExit?: boolean;
|
||||||
onOpen: (open: boolean) => void;
|
onOpen: (open: boolean) => void;
|
||||||
onResizing: (resizing: boolean) => void;
|
onResizing: (resizing: boolean) => void;
|
||||||
onWidthChange: (width: number) => void;
|
onWidthChange: (width: number) => void;
|
||||||
@@ -149,6 +150,7 @@ export const ResizePanel = forwardRef<HTMLDivElement, ResizePanelProps>(
|
|||||||
floating,
|
floating,
|
||||||
enableAnimation: _enableAnimation = true,
|
enableAnimation: _enableAnimation = true,
|
||||||
open,
|
open,
|
||||||
|
unmountOnExit,
|
||||||
onOpen,
|
onOpen,
|
||||||
onResizing,
|
onResizing,
|
||||||
onWidthChange,
|
onWidthChange,
|
||||||
@@ -182,7 +184,7 @@ export const ResizePanel = forwardRef<HTMLDivElement, ResizePanelProps>(
|
|||||||
data-handle-position={resizeHandlePos}
|
data-handle-position={resizeHandlePos}
|
||||||
data-enable-animation={enableAnimation && !resizing}
|
data-enable-animation={enableAnimation && !resizing}
|
||||||
>
|
>
|
||||||
{status !== 'exited' && children}
|
{!(status === 'exited' && unmountOnExit !== false) && children}
|
||||||
<ResizeHandle
|
<ResizeHandle
|
||||||
resizeHandlePos={resizeHandlePos}
|
resizeHandlePos={resizeHandlePos}
|
||||||
resizeHandleOffset={resizeHandleOffset}
|
resizeHandleOffset={resizeHandleOffset}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import type { EditorHost } from '@blocksuite/block-std';
|
|||||||
import { PaymentRequiredError, UnauthorizedError } from '@blocksuite/blocks';
|
import { PaymentRequiredError, UnauthorizedError } from '@blocksuite/blocks';
|
||||||
import { Slot } from '@blocksuite/store';
|
import { Slot } from '@blocksuite/store';
|
||||||
|
|
||||||
import type { ChatCards } from './chat-panel/chat-cards';
|
|
||||||
|
|
||||||
export interface AIUserInfo {
|
export interface AIUserInfo {
|
||||||
id: string;
|
id: string;
|
||||||
email: string;
|
email: string;
|
||||||
@@ -72,19 +70,6 @@ export class AIProvider {
|
|||||||
return AIProvider.instance.toggleGeneralAIOnboarding;
|
return AIProvider.instance.toggleGeneralAIOnboarding;
|
||||||
}
|
}
|
||||||
|
|
||||||
static genRequestChatCardsFn(params: AIChatParams) {
|
|
||||||
return async (chatPanel: HTMLElement) => {
|
|
||||||
const chatCards: ChatCards | null = await new Promise(resolve =>
|
|
||||||
requestAnimationFrame(() =>
|
|
||||||
resolve(chatPanel.querySelector('chat-cards'))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
if (!chatCards) return;
|
|
||||||
if (chatCards.temporaryParams) return;
|
|
||||||
chatCards.temporaryParams = params;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static readonly instance = new AIProvider();
|
private static readonly instance = new AIProvider();
|
||||||
|
|
||||||
static LAST_ACTION_SESSIONID = '';
|
static LAST_ACTION_SESSIONID = '';
|
||||||
|
|||||||
@@ -52,16 +52,18 @@ type PageMenuProps = {
|
|||||||
rename?: () => void;
|
rename?: () => void;
|
||||||
page: Doc;
|
page: Doc;
|
||||||
isJournal?: boolean;
|
isJournal?: boolean;
|
||||||
|
containerWidth: number;
|
||||||
};
|
};
|
||||||
// fixme: refactor this file
|
// fixme: refactor this file
|
||||||
export const PageHeaderMenuButton = ({
|
export const PageHeaderMenuButton = ({
|
||||||
rename,
|
rename,
|
||||||
page,
|
page,
|
||||||
isJournal,
|
isJournal,
|
||||||
|
containerWidth,
|
||||||
}: PageMenuProps) => {
|
}: PageMenuProps) => {
|
||||||
const pageId = page?.id;
|
const pageId = page?.id;
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const { hideShare } = useDetailPageHeaderResponsive();
|
const { hideShare } = useDetailPageHeaderResponsive(containerWidth);
|
||||||
const confirmEnableCloud = useEnableCloud();
|
const confirmEnableCloud = useEnableCloud();
|
||||||
|
|
||||||
const workspace = useService(WorkspaceService).workspace;
|
const workspace = useService(WorkspaceService).workspace;
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { RightSidebarService } from '@affine/core/modules/right-sidebar';
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import {
|
||||||
|
GlobalStateService,
|
||||||
|
LiveData,
|
||||||
|
useLiveData,
|
||||||
|
useService,
|
||||||
|
} from '@toeverything/infra';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { ToolContainer } from '../../workspace';
|
import { ToolContainer } from '../../workspace';
|
||||||
@@ -11,18 +16,37 @@ import {
|
|||||||
gradient,
|
gradient,
|
||||||
} from './styles.css';
|
} from './styles.css';
|
||||||
|
|
||||||
|
const RIGHT_SIDEBAR_AI_HAS_EVER_OPENED_KEY =
|
||||||
|
'app:settings:rightsidebar:ai:has-ever-opened';
|
||||||
|
|
||||||
export const AIIsland = () => {
|
export const AIIsland = () => {
|
||||||
// to make sure ai island is hidden first and animate in
|
// to make sure ai island is hidden first and animate in
|
||||||
const [hide, setHide] = useState(true);
|
const [hide, setHide] = useState(true);
|
||||||
|
|
||||||
const rightSidebar = useService(RightSidebarService).rightSidebar;
|
const workbench = useService(WorkbenchService).workbench;
|
||||||
const activeTabName = useLiveData(rightSidebar.activeTabName$);
|
const activeView = useLiveData(workbench.activeView$);
|
||||||
const rightSidebarOpen = useLiveData(rightSidebar.isOpen$);
|
const haveChatTab = useLiveData(
|
||||||
const aiChatHasEverOpened = useLiveData(rightSidebar.aiChatHasEverOpened$);
|
activeView.sidebarTabs$.map(tabs => tabs.some(t => t.id === 'chat'))
|
||||||
|
);
|
||||||
|
const activeTab = useLiveData(activeView.activeSidebarTab$);
|
||||||
|
const sidebarOpen = useLiveData(workbench.sidebarOpen$);
|
||||||
|
const globalState = useService(GlobalStateService).globalState;
|
||||||
|
const aiChatHasEverOpened = useLiveData(
|
||||||
|
LiveData.from(
|
||||||
|
globalState.watch<boolean>(RIGHT_SIDEBAR_AI_HAS_EVER_OPENED_KEY),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setHide(rightSidebarOpen && activeTabName === 'chat');
|
if (sidebarOpen && activeTab?.id === 'chat') {
|
||||||
}, [activeTabName, rightSidebarOpen]);
|
globalState.set(RIGHT_SIDEBAR_AI_HAS_EVER_OPENED_KEY, true);
|
||||||
|
}
|
||||||
|
}, [activeTab, globalState, sidebarOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHide((sidebarOpen && activeTab?.id === 'chat') || !haveChatTab);
|
||||||
|
}, [activeTab, haveChatTab, sidebarOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToolContainer>
|
<ToolContainer>
|
||||||
@@ -43,8 +67,8 @@ export const AIIsland = () => {
|
|||||||
data-testid="ai-island"
|
data-testid="ai-island"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (hide) return;
|
if (hide) return;
|
||||||
rightSidebar.open();
|
workbench.openSidebar();
|
||||||
rightSidebar.setActiveTabName('chat');
|
activeView.activeSidebarTab('chat');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AIIcon />
|
<AIIcon />
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import { SyncAwareness } from '../components/affine/awareness';
|
|||||||
import { appSidebarResizingAtom } from '../components/app-sidebar';
|
import { appSidebarResizingAtom } from '../components/app-sidebar';
|
||||||
import { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils';
|
import { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils';
|
||||||
import type { DraggableTitleCellData } from '../components/page-list';
|
import type { DraggableTitleCellData } from '../components/page-list';
|
||||||
|
import { AIIsland } from '../components/pure/ai-island';
|
||||||
import { RootAppSidebar } from '../components/root-app-sidebar';
|
import { RootAppSidebar } from '../components/root-app-sidebar';
|
||||||
import { MainContainer } from '../components/workspace';
|
import { MainContainer } from '../components/workspace';
|
||||||
import { WorkspaceUpgrade } from '../components/workspace-upgrade';
|
import { WorkspaceUpgrade } from '../components/workspace-upgrade';
|
||||||
@@ -82,6 +83,7 @@ export const WorkspaceLayout = function WorkspaceLayout({
|
|||||||
<WorkspaceLayoutInner>{children}</WorkspaceLayoutInner>
|
<WorkspaceLayoutInner>{children}</WorkspaceLayoutInner>
|
||||||
{/* should show after workspace loaded */}
|
{/* should show after workspace loaded */}
|
||||||
<WorkspaceAIOnboarding />
|
<WorkspaceAIOnboarding />
|
||||||
|
<AIIsland />
|
||||||
</SWRConfigProvider>
|
</SWRConfigProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { configurePeekViewModule } from './peek-view';
|
|||||||
import { configurePermissionsModule } from './permissions';
|
import { configurePermissionsModule } from './permissions';
|
||||||
import { configureWorkspacePropertiesModule } from './properties';
|
import { configureWorkspacePropertiesModule } from './properties';
|
||||||
import { configureQuickSearchModule } from './quicksearch';
|
import { configureQuickSearchModule } from './quicksearch';
|
||||||
import { configureRightSidebarModule } from './right-sidebar';
|
|
||||||
import { configureShareDocsModule } from './share-doc';
|
import { configureShareDocsModule } from './share-doc';
|
||||||
import { configureStorageImpls } from './storage';
|
import { configureStorageImpls } from './storage';
|
||||||
import { configureTagModule } from './tag';
|
import { configureTagModule } from './tag';
|
||||||
@@ -22,7 +21,6 @@ export function configureCommonModules(framework: Framework) {
|
|||||||
configureInfraModules(framework);
|
configureInfraModules(framework);
|
||||||
configureCollectionModule(framework);
|
configureCollectionModule(framework);
|
||||||
configureNavigationModule(framework);
|
configureNavigationModule(framework);
|
||||||
configureRightSidebarModule(framework);
|
|
||||||
configureTagModule(framework);
|
configureTagModule(framework);
|
||||||
configureWorkbenchModule(framework);
|
configureWorkbenchModule(framework);
|
||||||
configureWorkspacePropertiesModule(framework);
|
configureWorkspacePropertiesModule(framework);
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
export type { SidebarTabName } from './multi-tabs/sidebar-tab';
|
|
||||||
export { sidebarTabs, type TabOnLoadFn } from './multi-tabs/sidebar-tabs';
|
|
||||||
export { MultiTabSidebarBody } from './view/body';
|
|
||||||
export { MultiTabSidebarHeaderSwitcher } from './view/header-switcher';
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import type { AffineEditorContainer } from '@blocksuite/presets';
|
|
||||||
|
|
||||||
export type SidebarTabName = 'outline' | 'frame' | 'chat' | 'journal';
|
|
||||||
|
|
||||||
export interface SidebarTabProps {
|
|
||||||
editor: AffineEditorContainer | null;
|
|
||||||
onLoad: ((component: HTMLElement) => void) | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SidebarTab {
|
|
||||||
name: SidebarTabName;
|
|
||||||
icon: React.ReactNode;
|
|
||||||
Component: React.ComponentType<SidebarTabProps>;
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import type { SidebarTab } from './sidebar-tab';
|
|
||||||
import { chatTab } from './tabs/chat';
|
|
||||||
import { framePanelTab } from './tabs/frame';
|
|
||||||
import { journalTab } from './tabs/journal';
|
|
||||||
import { outlineTab } from './tabs/outline';
|
|
||||||
|
|
||||||
export type TabOnLoadFn = (component: HTMLElement) => void;
|
|
||||||
|
|
||||||
// the list of all possible tabs in affine.
|
|
||||||
// order matters (determines the order of the tabs)
|
|
||||||
export const sidebarTabs: SidebarTab[] = [
|
|
||||||
chatTab,
|
|
||||||
journalTab,
|
|
||||||
outlineTab,
|
|
||||||
framePanelTab,
|
|
||||||
];
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { cssVar } from '@toeverything/theme';
|
|
||||||
import { style } from '@vanilla-extract/css';
|
|
||||||
export const root = style({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
flex: 1,
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
overflow: 'hidden',
|
|
||||||
alignItems: 'center',
|
|
||||||
borderTop: `1px solid ${cssVar('borderColor')}`,
|
|
||||||
});
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import type { PropsWithChildren } from 'react';
|
|
||||||
|
|
||||||
import type { SidebarTab, SidebarTabProps } from '../multi-tabs/sidebar-tab';
|
|
||||||
import * as styles from './body.css';
|
|
||||||
|
|
||||||
export const MultiTabSidebarBody = (
|
|
||||||
props: PropsWithChildren<SidebarTabProps & { tab?: SidebarTab | null }>
|
|
||||||
) => {
|
|
||||||
const Component = props.tab?.Component;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.root}>
|
|
||||||
{props.children}
|
|
||||||
{Component ? <Component {...props} /> : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import type { RadioItem } from '@affine/component';
|
|
||||||
import { RadioGroup } from '@affine/component';
|
|
||||||
import { cssVar } from '@toeverything/theme';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
import type { SidebarTab, SidebarTabName } from '../multi-tabs/sidebar-tab';
|
|
||||||
|
|
||||||
export interface MultiTabSidebarHeaderSwitcherProps {
|
|
||||||
tabs: SidebarTab[];
|
|
||||||
activeTabName: SidebarTabName | null;
|
|
||||||
setActiveTabName: (ext: SidebarTabName) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// provide a switcher for active extensions
|
|
||||||
// will be used in global top header (MacOS) or sidebar (Windows)
|
|
||||||
export const MultiTabSidebarHeaderSwitcher = ({
|
|
||||||
tabs,
|
|
||||||
activeTabName,
|
|
||||||
setActiveTabName,
|
|
||||||
}: MultiTabSidebarHeaderSwitcherProps) => {
|
|
||||||
const tabItems = useMemo(() => {
|
|
||||||
return tabs.map(extension => {
|
|
||||||
return {
|
|
||||||
value: extension.name,
|
|
||||||
label: extension.icon,
|
|
||||||
style: { padding: 0, fontSize: 20, width: 24 },
|
|
||||||
} satisfies RadioItem;
|
|
||||||
});
|
|
||||||
}, [tabs]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RadioGroup
|
|
||||||
borderRadius={8}
|
|
||||||
itemHeight={24}
|
|
||||||
padding={4}
|
|
||||||
gap={8}
|
|
||||||
items={tabItems}
|
|
||||||
value={activeTabName}
|
|
||||||
onChange={setActiveTabName}
|
|
||||||
activeItemStyle={{ color: cssVar('primaryColor') }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { Entity } from '@toeverything/infra';
|
|
||||||
|
|
||||||
import { createIsland } from '../../../utils/island';
|
|
||||||
|
|
||||||
export class RightSidebarView extends Entity {
|
|
||||||
readonly body = createIsland();
|
|
||||||
readonly header = createIsland();
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import type { GlobalState } from '@toeverything/infra';
|
|
||||||
import { Entity, LiveData } from '@toeverything/infra';
|
|
||||||
import { combineLatest } from 'rxjs';
|
|
||||||
|
|
||||||
import type { SidebarTabName } from '../../multi-tab-sidebar';
|
|
||||||
import { RightSidebarView } from './right-sidebar-view';
|
|
||||||
|
|
||||||
const RIGHT_SIDEBAR_KEY = 'app:settings:rightsidebar';
|
|
||||||
const RIGHT_SIDEBAR_TABS_ACTIVE_KEY = 'app:settings:rightsidebar:tabs:active';
|
|
||||||
const RIGHT_SIDEBAR_AI_HAS_EVER_OPENED_KEY =
|
|
||||||
'app:settings:rightsidebar:ai:has-ever-opened';
|
|
||||||
|
|
||||||
export class RightSidebar extends Entity {
|
|
||||||
_disposables: Array<() => void> = [];
|
|
||||||
constructor(private readonly globalState: GlobalState) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
const sub = combineLatest([this.activeTabName$, this.isOpen$]).subscribe(
|
|
||||||
([name, open]) => {
|
|
||||||
if (name === 'chat' && open) {
|
|
||||||
this.globalState.set(RIGHT_SIDEBAR_AI_HAS_EVER_OPENED_KEY, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
this._disposables.push(() => sub.unsubscribe());
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly isOpen$ = LiveData.from(
|
|
||||||
this.globalState.watch<boolean>(RIGHT_SIDEBAR_KEY),
|
|
||||||
false
|
|
||||||
).map(Boolean);
|
|
||||||
readonly views$ = new LiveData<RightSidebarView[]>([]);
|
|
||||||
readonly front$ = this.views$.map(
|
|
||||||
stack => stack[0] as RightSidebarView | undefined
|
|
||||||
);
|
|
||||||
readonly hasViews$ = this.views$.map(stack => stack.length > 0);
|
|
||||||
readonly activeTabName$ = LiveData.from(
|
|
||||||
this.globalState.watch<SidebarTabName>(RIGHT_SIDEBAR_TABS_ACTIVE_KEY),
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
/** To determine if AI chat has ever been opened, used to show the animation for the first time */
|
|
||||||
readonly aiChatHasEverOpened$ = LiveData.from(
|
|
||||||
this.globalState.watch<boolean>(RIGHT_SIDEBAR_AI_HAS_EVER_OPENED_KEY),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
override dispose() {
|
|
||||||
super.dispose();
|
|
||||||
this._disposables.forEach(dispose => dispose());
|
|
||||||
}
|
|
||||||
|
|
||||||
setActiveTabName(name: SidebarTabName) {
|
|
||||||
this.globalState.set(RIGHT_SIDEBAR_TABS_ACTIVE_KEY, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
open() {
|
|
||||||
this._set(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
toggle() {
|
|
||||||
this._set(!this.isOpen$.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
|
||||||
this._set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
_set(value: boolean) {
|
|
||||||
this.globalState.set(RIGHT_SIDEBAR_KEY, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private use `RightSidebarViewIsland` instead
|
|
||||||
*/
|
|
||||||
_append() {
|
|
||||||
const view = this.framework.createEntity(RightSidebarView);
|
|
||||||
this.views$.next([...this.views$.value, view]);
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private use `RightSidebarViewIsland` instead
|
|
||||||
*/
|
|
||||||
_moveToFront(view: RightSidebarView) {
|
|
||||||
if (this.views$.value.includes(view)) {
|
|
||||||
this.views$.next([view, ...this.views$.value.filter(v => v !== view)]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private use `RightSidebarViewIsland` instead
|
|
||||||
*/
|
|
||||||
_remove(view: RightSidebarView) {
|
|
||||||
this.views$.next(this.views$.value.filter(v => v !== view));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
export { RightSidebar } from './entities/right-sidebar';
|
|
||||||
export { RightSidebarService } from './services/right-sidebar';
|
|
||||||
export { RightSidebarContainer } from './view/container';
|
|
||||||
export { RightSidebarViewIsland } from './view/view-island';
|
|
||||||
|
|
||||||
import {
|
|
||||||
type Framework,
|
|
||||||
GlobalState,
|
|
||||||
WorkspaceScope,
|
|
||||||
} from '@toeverything/infra';
|
|
||||||
|
|
||||||
import { RightSidebar } from './entities/right-sidebar';
|
|
||||||
import { RightSidebarView } from './entities/right-sidebar-view';
|
|
||||||
import { RightSidebarService } from './services/right-sidebar';
|
|
||||||
|
|
||||||
export function configureRightSidebarModule(services: Framework) {
|
|
||||||
services
|
|
||||||
.scope(WorkspaceScope)
|
|
||||||
.service(RightSidebarService)
|
|
||||||
.entity(RightSidebar, [GlobalState])
|
|
||||||
.entity(RightSidebarView);
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { Service } from '@toeverything/infra';
|
|
||||||
|
|
||||||
import { RightSidebar } from '../entities/right-sidebar';
|
|
||||||
|
|
||||||
export class RightSidebarService extends Service {
|
|
||||||
rightSidebar = this.framework.createEntity(RightSidebar);
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import { ResizePanel } from '@affine/component/resize-panel';
|
|
||||||
import { rightSidebarWidthAtom } from '@affine/core/atoms';
|
|
||||||
import { appSettingAtom, useLiveData, useService } from '@toeverything/infra';
|
|
||||||
import { useAtom, useAtomValue } from 'jotai';
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { RightSidebarService } from '../services/right-sidebar';
|
|
||||||
import * as styles from './container.css';
|
|
||||||
import { Header } from './header';
|
|
||||||
|
|
||||||
const MIN_SIDEBAR_WIDTH = 320;
|
|
||||||
const MAX_SIDEBAR_WIDTH = 800;
|
|
||||||
|
|
||||||
export const RightSidebarContainer = () => {
|
|
||||||
const { clientBorder } = useAtomValue(appSettingAtom);
|
|
||||||
|
|
||||||
const [width, setWidth] = useAtom(rightSidebarWidthAtom);
|
|
||||||
const [resizing, setResizing] = useState(false);
|
|
||||||
const rightSidebar = useService(RightSidebarService).rightSidebar;
|
|
||||||
|
|
||||||
const frontView = useLiveData(rightSidebar.front$);
|
|
||||||
const open = useLiveData(rightSidebar.isOpen$) && frontView !== undefined;
|
|
||||||
const [floating, setFloating] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const onResize = () => setFloating(!!(window.innerWidth < 768));
|
|
||||||
onResize();
|
|
||||||
window.addEventListener('resize', onResize);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', onResize);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleOpenChange = useCallback(
|
|
||||||
(open: boolean) => {
|
|
||||||
if (open) {
|
|
||||||
rightSidebar.open();
|
|
||||||
} else {
|
|
||||||
rightSidebar.close();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[rightSidebar]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleToggleOpen = useCallback(() => {
|
|
||||||
rightSidebar.toggle();
|
|
||||||
}, [rightSidebar]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ResizePanel
|
|
||||||
floating={floating}
|
|
||||||
resizeHandlePos="left"
|
|
||||||
resizeHandleOffset={clientBorder ? 3.5 : 0}
|
|
||||||
width={width}
|
|
||||||
resizing={resizing}
|
|
||||||
onResizing={setResizing}
|
|
||||||
className={styles.sidebarContainer}
|
|
||||||
data-client-border={clientBorder && open}
|
|
||||||
open={open}
|
|
||||||
onOpen={handleOpenChange}
|
|
||||||
onWidthChange={setWidth}
|
|
||||||
minWidth={MIN_SIDEBAR_WIDTH}
|
|
||||||
maxWidth={MAX_SIDEBAR_WIDTH}
|
|
||||||
>
|
|
||||||
{frontView && (
|
|
||||||
<div className={styles.sidebarContainerInner}>
|
|
||||||
<Header
|
|
||||||
floating={false}
|
|
||||||
onToggle={handleToggleOpen}
|
|
||||||
view={frontView}
|
|
||||||
/>
|
|
||||||
<frontView.body.Target
|
|
||||||
className={styles.sidebarBodyTarget}
|
|
||||||
></frontView.body.Target>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</ResizePanel>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import { useService } from '@toeverything/infra';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import type { RightSidebarView } from '../entities/right-sidebar-view';
|
|
||||||
import { RightSidebarService } from '../services/right-sidebar';
|
|
||||||
|
|
||||||
export interface RightSidebarViewProps {
|
|
||||||
body: JSX.Element;
|
|
||||||
header?: JSX.Element | null;
|
|
||||||
name?: string;
|
|
||||||
active?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RightSidebarViewIsland = ({
|
|
||||||
body,
|
|
||||||
header,
|
|
||||||
active,
|
|
||||||
}: RightSidebarViewProps) => {
|
|
||||||
const rightSidebar = useService(RightSidebarService).rightSidebar;
|
|
||||||
|
|
||||||
const [view, setView] = useState<RightSidebarView | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const view = rightSidebar._append();
|
|
||||||
setView(view);
|
|
||||||
return () => {
|
|
||||||
rightSidebar._remove(view);
|
|
||||||
setView(null);
|
|
||||||
};
|
|
||||||
}, [rightSidebar]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (active && view) {
|
|
||||||
rightSidebar._moveToFront(view);
|
|
||||||
}
|
|
||||||
}, [active, rightSidebar, view]);
|
|
||||||
|
|
||||||
if (!view) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<view.header.Provider>{header}</view.header.Provider>
|
|
||||||
<view.body.Provider>{body}</view.body.Provider>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
43
packages/frontend/core/src/modules/workbench/README.md
Normal file
43
packages/frontend/core/src/modules/workbench/README.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Workbench
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────Workbench─────-----──────┐
|
||||||
|
| Tab1 | Tab2 | Tab3 - □ x |
|
||||||
|
│ ┌───────┐ ┌───────┐ ┌───────┐ ┌──────┤
|
||||||
|
│ │header │ │header │ │header │ │ │
|
||||||
|
│ │ │ │ │ │ │ │ side │
|
||||||
|
│ │ │ │ │ │ │ │ bar │
|
||||||
|
│ │ view │ │ view │ │ view │ │ │
|
||||||
|
│ │ │ │ │ │ │ │ │
|
||||||
|
│ │ │ │ │ │ │ │
|
||||||
|
│ │ │ │ │ │ │ │ │
|
||||||
|
│ └───────┘ └───────┘ └───────┘ │ │
|
||||||
|
└───────────────────────────────┴──────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
`Workbench` is the window manager in affine, including the main area and the right sidebar area.
|
||||||
|
|
||||||
|
`View` is a managed window under the workbench. Each view has its own history(Support go back and forward) and currently URL.
|
||||||
|
The view renders the content as defined by the router ([here](../../router.tsx)).
|
||||||
|
Each route can render its own `Header`, `Body`, and several `Sidebar`s by [ViewIsland](./view/view-islands.tsx).
|
||||||
|
|
||||||
|
The `Workbench` manages all Views and decides when to display and close them.
|
||||||
|
There is always one **active View**, and the URL of the active View is considered the URL of the entire application.
|
||||||
|
|
||||||
|
## Sidebar
|
||||||
|
|
||||||
|
Each `View` can define its `Sidebar`, which will be displayed in the right area of the screen.
|
||||||
|
If the same view has multiple sidebars, a switcher will be displayed so that users can switch between multiple sidebars.
|
||||||
|
|
||||||
|
> only the sidebar of the currently active view will be displayed.
|
||||||
|
|
||||||
|
## Tab
|
||||||
|
|
||||||
|
WIP
|
||||||
|
|
||||||
|
## Persistence
|
||||||
|
|
||||||
|
When close the application and reopen, the entire workbench should be restored to its previous state.
|
||||||
|
WIP
|
||||||
|
|
||||||
|
> If running in a browser, the workbench will passing the browser's back and forward navigation to the active view.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { Entity } from '@toeverything/infra';
|
||||||
|
|
||||||
|
export class SidebarTab extends Entity<{ id: string }> {
|
||||||
|
readonly id = this.props.id;
|
||||||
|
}
|
||||||
@@ -2,19 +2,36 @@ import { Entity, LiveData } from '@toeverything/infra';
|
|||||||
import type { Location, To } from 'history';
|
import type { Location, To } from 'history';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
import { createIsland } from '../../../utils/island';
|
|
||||||
import { createNavigableHistory } from '../../../utils/navigable-history';
|
import { createNavigableHistory } from '../../../utils/navigable-history';
|
||||||
import type { ViewScope } from '../scopes/view';
|
import { ViewScope } from '../scopes/view';
|
||||||
|
import { SidebarTab } from './sidebar-tab';
|
||||||
|
|
||||||
export class View extends Entity {
|
export class View extends Entity<{
|
||||||
id = this.scope.props.id;
|
id: string;
|
||||||
|
defaultLocation?: To | undefined;
|
||||||
|
}> {
|
||||||
|
scope = this.framework.createScope(ViewScope, {
|
||||||
|
view: this as View,
|
||||||
|
});
|
||||||
|
id = this.props.id;
|
||||||
|
|
||||||
constructor(public readonly scope: ViewScope) {
|
sidebarTabs$ = new LiveData<SidebarTab[]>([]);
|
||||||
|
|
||||||
|
// _activeTabId may point to a non-existent tab.
|
||||||
|
// In this case, we still retain the activeTabId data and wait for the non-existent tab to be mounted.
|
||||||
|
_activeSidebarTabId$ = new LiveData<string | null>(null);
|
||||||
|
activeSidebarTab$ = LiveData.computed(get => {
|
||||||
|
const activeTabId = get(this._activeSidebarTabId$);
|
||||||
|
const tabs = get(this.sidebarTabs$);
|
||||||
|
return tabs.length > 0
|
||||||
|
? tabs.find(tab => tab.id === activeTabId) ?? tabs[0]
|
||||||
|
: null;
|
||||||
|
});
|
||||||
|
|
||||||
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.history = createNavigableHistory({
|
this.history = createNavigableHistory({
|
||||||
initialEntries: [
|
initialEntries: [this.props.defaultLocation ?? { pathname: '/all' }],
|
||||||
this.scope.props.defaultLocation ?? { pathname: '/all' },
|
|
||||||
],
|
|
||||||
initialIndex: 0,
|
initialIndex: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -45,11 +62,6 @@ export class View extends Entity {
|
|||||||
);
|
);
|
||||||
|
|
||||||
size$ = new LiveData(100);
|
size$ = new LiveData(100);
|
||||||
/** Width of header content in px (excludes sidebar-toggle/windows button/...) */
|
|
||||||
headerContentWidth$ = new LiveData(1920);
|
|
||||||
|
|
||||||
header = createIsland();
|
|
||||||
body = createIsland();
|
|
||||||
|
|
||||||
push(path: To) {
|
push(path: To) {
|
||||||
this.history.push(path);
|
this.history.push(path);
|
||||||
@@ -66,4 +78,24 @@ export class View extends Entity {
|
|||||||
setSize(size?: number) {
|
setSize(size?: number) {
|
||||||
this.size$.next(size ?? 100);
|
this.size$.next(size ?? 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addSidebarTab(id: string) {
|
||||||
|
this.sidebarTabs$.next([
|
||||||
|
...this.sidebarTabs$.value,
|
||||||
|
this.scope.createEntity(SidebarTab, {
|
||||||
|
id,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeSidebarTab(id: string) {
|
||||||
|
this.sidebarTabs$.next(
|
||||||
|
this.sidebarTabs$.value.filter(tab => tab.id !== id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
activeSidebarTab(id: string | null) {
|
||||||
|
this._activeSidebarTabId$.next(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,8 @@ import { Unreachable } from '@affine/env/constant';
|
|||||||
import { Entity, LiveData } from '@toeverything/infra';
|
import { Entity, LiveData } from '@toeverything/infra';
|
||||||
import type { To } from 'history';
|
import type { To } from 'history';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { combineLatest, map, switchMap } from 'rxjs';
|
|
||||||
|
|
||||||
import { ViewScope } from '../scopes/view';
|
import { View } from './view';
|
||||||
import { ViewService } from '../services/view';
|
|
||||||
import type { View } from './view';
|
|
||||||
|
|
||||||
export type WorkbenchPosition = 'beside' | 'active' | 'head' | 'tail' | number;
|
export type WorkbenchPosition = 'beside' | 'active' | 'head' | 'tail' | number;
|
||||||
|
|
||||||
@@ -17,24 +14,20 @@ interface WorkbenchOpenOptions {
|
|||||||
|
|
||||||
export class Workbench extends Entity {
|
export class Workbench extends Entity {
|
||||||
readonly views$ = new LiveData([
|
readonly views$ = new LiveData([
|
||||||
this.framework.createScope(ViewScope, { id: nanoid() }).get(ViewService)
|
this.framework.createEntity(View, { id: nanoid() }),
|
||||||
.view,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
activeViewIndex$ = new LiveData(0);
|
activeViewIndex$ = new LiveData(0);
|
||||||
activeView$ = LiveData.from(
|
activeView$ = LiveData.computed(get => {
|
||||||
combineLatest([this.views$, this.activeViewIndex$]).pipe(
|
const activeIndex = get(this.activeViewIndex$);
|
||||||
map(([views, index]) => views[index])
|
const views = get(this.views$);
|
||||||
),
|
return views[activeIndex];
|
||||||
this.views$.value[this.activeViewIndex$.value]
|
});
|
||||||
);
|
|
||||||
|
|
||||||
basename$ = new LiveData('/');
|
basename$ = new LiveData('/');
|
||||||
|
location$ = LiveData.computed(get => {
|
||||||
location$ = LiveData.from(
|
return get(get(this.activeView$).location$);
|
||||||
this.activeView$.pipe(switchMap(view => view.location$)),
|
});
|
||||||
this.views$.value[this.activeViewIndex$.value].history.location
|
sidebarOpen$ = new LiveData(false);
|
||||||
);
|
|
||||||
|
|
||||||
active(index: number) {
|
active(index: number) {
|
||||||
index = Math.max(0, Math.min(index, this.views$.value.length - 1));
|
index = Math.max(0, Math.min(index, this.views$.value.length - 1));
|
||||||
@@ -42,13 +35,28 @@ export class Workbench extends Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createView(at: WorkbenchPosition = 'beside', defaultLocation: To) {
|
createView(at: WorkbenchPosition = 'beside', defaultLocation: To) {
|
||||||
const view = this.framework
|
const view = this.framework.createEntity(View, {
|
||||||
.createScope(ViewScope, { id: nanoid(), defaultLocation })
|
id: nanoid(),
|
||||||
.get(ViewService).view;
|
defaultLocation,
|
||||||
|
});
|
||||||
const newViews = [...this.views$.value];
|
const newViews = [...this.views$.value];
|
||||||
newViews.splice(this.indexAt(at), 0, view);
|
newViews.splice(this.indexAt(at), 0, view);
|
||||||
this.views$.next(newViews);
|
this.views$.next(newViews);
|
||||||
return newViews.indexOf(view);
|
const index = newViews.indexOf(view);
|
||||||
|
this.active(index);
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
openSidebar() {
|
||||||
|
this.sidebarOpen$.next(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
closeSidebar() {
|
||||||
|
this.sidebarOpen$.next(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSidebar() {
|
||||||
|
this.sidebarOpen$.next(!this.sidebarOpen$.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
open(
|
open(
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
export { Workbench } from './entities/workbench';
|
export { Workbench } from './entities/workbench';
|
||||||
export { ViewScope as View } from './scopes/view';
|
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 { ViewBodyIsland } from './view/view-body-island';
|
export { ViewBody, ViewHeader, ViewSidebarTab } from './view/view-islands';
|
||||||
export { ViewHeaderIsland } from './view/view-header-island';
|
|
||||||
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 { type Framework, WorkspaceScope } from '@toeverything/infra';
|
import { type Framework, WorkspaceScope } from '@toeverything/infra';
|
||||||
|
|
||||||
|
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';
|
||||||
import { ViewScope } from './scopes/view';
|
import { ViewScope } from './scopes/view';
|
||||||
@@ -20,7 +20,8 @@ export function configureWorkbenchModule(services: Framework) {
|
|||||||
.scope(WorkspaceScope)
|
.scope(WorkspaceScope)
|
||||||
.service(WorkbenchService)
|
.service(WorkbenchService)
|
||||||
.entity(Workbench)
|
.entity(Workbench)
|
||||||
|
.entity(View)
|
||||||
.scope(ViewScope)
|
.scope(ViewScope)
|
||||||
.entity(View, [ViewScope])
|
.service(ViewService, [ViewScope])
|
||||||
.service(ViewService);
|
.entity(SidebarTab);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Scope } from '@toeverything/infra';
|
import { Scope } from '@toeverything/infra';
|
||||||
import type { To } from 'history';
|
|
||||||
|
import type { View } from '../entities/view';
|
||||||
|
|
||||||
export class ViewScope extends Scope<{
|
export class ViewScope extends Scope<{
|
||||||
id: string;
|
view: View;
|
||||||
defaultLocation?: To | undefined;
|
|
||||||
}> {}
|
}> {}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { Service } from '@toeverything/infra';
|
import { Service } from '@toeverything/infra';
|
||||||
|
|
||||||
import { View } from '../entities/view';
|
import type { ViewScope } from '../scopes/view';
|
||||||
|
|
||||||
export class ViewService extends Service {
|
export class ViewService extends Service {
|
||||||
view = this.framework.createEntity(View);
|
view = this.scope.props.view;
|
||||||
|
|
||||||
|
constructor(private readonly scope: ViewScope) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import { IconButton, observeResize } from '@affine/component';
|
import { IconButton } from '@affine/component';
|
||||||
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
|
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
|
||||||
import { RightSidebarIcon } from '@blocksuite/icons/rc';
|
import { RightSidebarIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { Suspense, useCallback, useEffect, useRef } from 'react';
|
import { Suspense, useCallback } from 'react';
|
||||||
|
|
||||||
import { AffineErrorBoundary } from '../../../components/affine/affine-error-boundary';
|
import { AffineErrorBoundary } from '../../../components/affine/affine-error-boundary';
|
||||||
import { appSidebarOpenAtom } from '../../../components/app-sidebar/index.jotai';
|
import { appSidebarOpenAtom } from '../../../components/app-sidebar/index.jotai';
|
||||||
import { SidebarSwitch } from '../../../components/app-sidebar/sidebar-header/sidebar-switch';
|
import { SidebarSwitch } from '../../../components/app-sidebar/sidebar-header/sidebar-switch';
|
||||||
import { RightSidebarService } from '../../right-sidebar';
|
|
||||||
import { ViewService } from '../services/view';
|
import { ViewService } from '../services/view';
|
||||||
|
import { WorkbenchService } from '../services/workbench';
|
||||||
import * as styles from './route-container.css';
|
import * as styles from './route-container.css';
|
||||||
import { useViewPosition } from './use-view-position';
|
import { useViewPosition } from './use-view-position';
|
||||||
|
import { ViewBodyTarget, ViewHeaderTarget } from './view-islands';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
route: {
|
route: {
|
||||||
@@ -41,25 +42,16 @@ const ToggleButton = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const RouteContainer = ({ route }: Props) => {
|
export const RouteContainer = ({ route }: Props) => {
|
||||||
const viewHeaderContainerRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
const view = useService(ViewService).view;
|
|
||||||
const viewPosition = useViewPosition();
|
const viewPosition = useViewPosition();
|
||||||
const leftSidebarOpen = useAtomValue(appSidebarOpenAtom);
|
const leftSidebarOpen = useAtomValue(appSidebarOpenAtom);
|
||||||
const rightSidebar = useService(RightSidebarService).rightSidebar;
|
const workbench = useService(WorkbenchService).workbench;
|
||||||
const rightSidebarOpen = useLiveData(rightSidebar.isOpen$);
|
const view = useService(ViewService).view;
|
||||||
const rightSidebarHasViews = useLiveData(rightSidebar.hasViews$);
|
const sidebarOpen = useLiveData(workbench.sidebarOpen$);
|
||||||
const handleToggleRightSidebar = useCallback(() => {
|
const handleToggleSidebar = useCallback(() => {
|
||||||
rightSidebar.toggle();
|
workbench.toggleSidebar();
|
||||||
}, [rightSidebar]);
|
}, [workbench]);
|
||||||
const isWindowsDesktop = environment.isDesktop && environment.isWindows;
|
const isWindowsDesktop = environment.isDesktop && environment.isWindows;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const container = viewHeaderContainerRef.current;
|
|
||||||
if (!container) return;
|
|
||||||
return observeResize(container, entry => {
|
|
||||||
view.headerContentWidth$.next(entry.contentRect.width);
|
|
||||||
});
|
|
||||||
}, [view.headerContentWidth$]);
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.root}>
|
<div className={styles.root}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
@@ -69,34 +61,32 @@ export const RouteContainer = ({ route }: Props) => {
|
|||||||
className={styles.leftSidebarButton}
|
className={styles.leftSidebarButton}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<view.header.Target
|
<ViewHeaderTarget
|
||||||
ref={viewHeaderContainerRef}
|
viewId={view.id}
|
||||||
className={styles.viewHeaderContainer}
|
className={styles.viewHeaderContainer}
|
||||||
/>
|
/>
|
||||||
{viewPosition.isLast && (
|
{viewPosition.isLast && (
|
||||||
<>
|
<>
|
||||||
{rightSidebarHasViews && (
|
<ToggleButton
|
||||||
<ToggleButton
|
show={!sidebarOpen}
|
||||||
show={!rightSidebarOpen}
|
className={styles.rightSidebarButton}
|
||||||
className={styles.rightSidebarButton}
|
onToggle={handleToggleSidebar}
|
||||||
onToggle={handleToggleRightSidebar}
|
/>
|
||||||
/>
|
{isWindowsDesktop && !sidebarOpen && (
|
||||||
|
<div className={styles.windowsAppControlsContainer}>
|
||||||
|
<WindowsAppControls />
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{isWindowsDesktop &&
|
|
||||||
!(rightSidebarOpen && rightSidebarHasViews) && (
|
|
||||||
<div className={styles.windowsAppControlsContainer}>
|
|
||||||
<WindowsAppControls />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AffineErrorBoundary>
|
<AffineErrorBoundary>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<route.Component />
|
<route.Component />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</AffineErrorBoundary>
|
</AffineErrorBoundary>
|
||||||
<view.body.Target className={styles.viewBodyContainer} />
|
<ViewBodyTarget viewId={view.id} className={styles.viewBodyContainer} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,24 +17,26 @@ export const sidebarContainerInner = style({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const sidebarContainer = style({
|
|
||||||
display: 'flex',
|
|
||||||
flexShrink: 0,
|
|
||||||
height: '100%',
|
|
||||||
right: 0,
|
|
||||||
selectors: {
|
|
||||||
[`&[data-client-border=true]`]: {
|
|
||||||
paddingLeft: 8,
|
|
||||||
borderRadius: 6,
|
|
||||||
},
|
|
||||||
[`&[data-client-border=false]`]: {
|
|
||||||
borderLeft: `1px solid ${cssVar('borderColor')}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const sidebarBodyTarget = style({
|
export const sidebarBodyTarget = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderTop: `1px solid ${cssVar('borderColor')}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const sidebarBodyNoSelection = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
flex: 1,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
overflow: 'hidden',
|
||||||
|
justifyContent: 'center',
|
||||||
|
userSelect: 'none',
|
||||||
|
color: cssVar('--affine-text-secondary-color'),
|
||||||
|
alignItems: 'center',
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { ViewService } from '../../services/view';
|
||||||
|
import { WorkbenchService } from '../../services/workbench';
|
||||||
|
import { ViewSidebarTabBodyTarget } from '../view-islands';
|
||||||
|
import * as styles from './sidebar-container.css';
|
||||||
|
import { Header } from './sidebar-header';
|
||||||
|
import { SidebarHeaderSwitcher } from './sidebar-header-switcher';
|
||||||
|
|
||||||
|
export const SidebarContainer = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HtmlHTMLAttributes<HTMLDivElement>) => {
|
||||||
|
const workbenchService = useService(WorkbenchService);
|
||||||
|
const workbench = workbenchService.workbench;
|
||||||
|
const viewService = useService(ViewService);
|
||||||
|
const view = viewService.view;
|
||||||
|
const sidebarTabs = useLiveData(view.sidebarTabs$);
|
||||||
|
const activeSidebarTab = useLiveData(view.activeSidebarTab$);
|
||||||
|
|
||||||
|
const handleToggleOpen = useCallback(() => {
|
||||||
|
workbench.toggleSidebar();
|
||||||
|
}, [workbench]);
|
||||||
|
|
||||||
|
const isWindowsDesktop = environment.isDesktop && environment.isWindows;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={clsx(styles.sidebarContainerInner, className)} {...props}>
|
||||||
|
<Header floating={false} onToggle={handleToggleOpen}>
|
||||||
|
{!isWindowsDesktop && sidebarTabs.length > 0 && (
|
||||||
|
<SidebarHeaderSwitcher />
|
||||||
|
)}
|
||||||
|
</Header>
|
||||||
|
{isWindowsDesktop && sidebarTabs.length > 0 && <SidebarHeaderSwitcher />}
|
||||||
|
{sidebarTabs.length > 0 ? (
|
||||||
|
sidebarTabs.map(sidebar => (
|
||||||
|
<ViewSidebarTabBodyTarget
|
||||||
|
tabId={sidebar.id}
|
||||||
|
key={sidebar.id}
|
||||||
|
style={{ display: activeSidebarTab === sidebar ? 'block' : 'none' }}
|
||||||
|
viewId={view.id}
|
||||||
|
className={styles.sidebarBodyTarget}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className={styles.sidebarBodyNoSelection}>No Selection</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const iconContainer = style({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
});
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { RadioGroup } from '@affine/component';
|
||||||
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { ViewService } from '../../services/view';
|
||||||
|
import { ViewSidebarTabIconTarget } from '../view-islands';
|
||||||
|
import * as styles from './sidebar-header-switcher.css';
|
||||||
|
|
||||||
|
// provide a switcher for active extensions
|
||||||
|
// will be used in global top header (MacOS) or sidebar (Windows)
|
||||||
|
export const SidebarHeaderSwitcher = () => {
|
||||||
|
const view = useService(ViewService).view;
|
||||||
|
const tabs = useLiveData(view.sidebarTabs$);
|
||||||
|
const activeTab = useLiveData(view.activeSidebarTab$);
|
||||||
|
|
||||||
|
const tabItems = tabs.map(tab => ({
|
||||||
|
value: tab.id,
|
||||||
|
label: (
|
||||||
|
<ViewSidebarTabIconTarget
|
||||||
|
className={styles.iconContainer}
|
||||||
|
viewId={view.id}
|
||||||
|
tabId={tab.id}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
style: { padding: 0, fontSize: 20, width: 24 },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const handleActiveTabChange = useCallback(
|
||||||
|
(tabId: string) => {
|
||||||
|
view.activeSidebarTab(tabId);
|
||||||
|
},
|
||||||
|
[view]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RadioGroup
|
||||||
|
borderRadius={8}
|
||||||
|
itemHeight={24}
|
||||||
|
padding={4}
|
||||||
|
gap={8}
|
||||||
|
items={tabItems}
|
||||||
|
value={activeTab}
|
||||||
|
onChange={handleActiveTabChange}
|
||||||
|
activeItemStyle={{ color: cssVar('primaryColor') }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
import { IconButton } from '@affine/component';
|
import { IconButton } from '@affine/component';
|
||||||
|
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
|
||||||
import { RightSidebarIcon } from '@blocksuite/icons/rc';
|
import { RightSidebarIcon } from '@blocksuite/icons/rc';
|
||||||
|
|
||||||
import { WindowsAppControls } from '../../../components/pure/header/windows-app-controls';
|
import * as styles from './sidebar-header.css';
|
||||||
import type { RightSidebarView } from '../entities/right-sidebar-view';
|
|
||||||
import * as styles from './header.css';
|
|
||||||
|
|
||||||
export type HeaderProps = {
|
export type HeaderProps = {
|
||||||
floating: boolean;
|
floating: boolean;
|
||||||
onToggle?: () => void;
|
onToggle?: () => void;
|
||||||
view: RightSidebarView;
|
children?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
function Container({
|
function Container({
|
||||||
@@ -42,10 +41,10 @@ const ToggleButton = ({ onToggle }: { onToggle?: () => void }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Windows = ({ floating, onToggle, view }: HeaderProps) => {
|
const Windows = ({ floating, onToggle, children }: HeaderProps) => {
|
||||||
return (
|
return (
|
||||||
<Container className={styles.header} floating={floating}>
|
<Container className={styles.header} floating={floating}>
|
||||||
<view.header.Target></view.header.Target>
|
{children}
|
||||||
<div className={styles.spacer} />
|
<div className={styles.spacer} />
|
||||||
<ToggleButton onToggle={onToggle} />
|
<ToggleButton onToggle={onToggle} />
|
||||||
<div className={styles.windowsAppControlsContainer}>
|
<div className={styles.windowsAppControlsContainer}>
|
||||||
@@ -55,10 +54,10 @@ const Windows = ({ floating, onToggle, view }: HeaderProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const NonWindows = ({ floating, view, onToggle }: HeaderProps) => {
|
const NonWindows = ({ floating, children, onToggle }: HeaderProps) => {
|
||||||
return (
|
return (
|
||||||
<Container className={styles.header} floating={floating}>
|
<Container className={styles.header} floating={floating}>
|
||||||
<view.header.Target></view.header.Target>
|
{children}
|
||||||
<div className={styles.spacer} />
|
<div className={styles.spacer} />
|
||||||
<ToggleButton onToggle={onToggle} />
|
<ToggleButton onToggle={onToggle} />
|
||||||
</Container>
|
</Container>
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { useService } from '@toeverything/infra';
|
|
||||||
|
|
||||||
import { ViewService } from '../services/view';
|
|
||||||
|
|
||||||
export const ViewBodyIsland = ({ children }: React.PropsWithChildren) => {
|
|
||||||
const view = useService(ViewService).view;
|
|
||||||
return <view.body.Provider>{children}</view.body.Provider>;
|
|
||||||
};
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { useService } from '@toeverything/infra';
|
|
||||||
|
|
||||||
import { ViewService } from '../services/view';
|
|
||||||
|
|
||||||
export const ViewHeaderIsland = ({ children }: React.PropsWithChildren) => {
|
|
||||||
const view = useService(ViewService).view;
|
|
||||||
return <view.header.Provider>{children}</view.header.Provider>;
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
/**
|
||||||
|
* # View Islands
|
||||||
|
*
|
||||||
|
* This file defines some components that allow each UI area to be defined inside each View route as shown below,
|
||||||
|
* and the Workbench is responsible for rendering these areas into their containers.
|
||||||
|
*
|
||||||
|
* ```tsx
|
||||||
|
* const MyView = () => {
|
||||||
|
* return <>
|
||||||
|
* <ViewHeader>
|
||||||
|
* ...
|
||||||
|
* </ViewHeader>
|
||||||
|
* <ViewBody>
|
||||||
|
* ...
|
||||||
|
* </ViewBody>
|
||||||
|
* <ViewSidebarTab tabId="my-tab" icon={<MyIcon />}>
|
||||||
|
* ...
|
||||||
|
* </ViewSidebarTab>
|
||||||
|
* </>
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* const viewRoute = [
|
||||||
|
* {
|
||||||
|
* path: '/my-view',
|
||||||
|
* component: MyView,
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Each Island is divided into `Target` and `Provider`.
|
||||||
|
* The `Provider` wraps the content to be rendered, while the `Target` is placed where it needs to be rendered.
|
||||||
|
* Then you get a view portal.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createIsland, type Island } from '@affine/core/utils/island';
|
||||||
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
|
import type React from 'react';
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
forwardRef,
|
||||||
|
type Ref,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import { ViewService } from '../services/view';
|
||||||
|
|
||||||
|
interface ViewIslandRegistry {
|
||||||
|
[key: string]: Island | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A registry context will be placed at the top level of the workbench.
|
||||||
|
*
|
||||||
|
* The `View` will create islands and place them in the registry,
|
||||||
|
* while `Workbench` can use the KEY to retrieve and display the islands.
|
||||||
|
*/
|
||||||
|
const ViewIslandRegistryContext = createContext<ViewIslandRegistry>({});
|
||||||
|
const ViewIslandSetContext = createContext<React.Dispatch<
|
||||||
|
React.SetStateAction<ViewIslandRegistry>
|
||||||
|
> | null>(null);
|
||||||
|
|
||||||
|
const ViewIsland = ({
|
||||||
|
id,
|
||||||
|
children,
|
||||||
|
}: React.PropsWithChildren<{ id: string }>) => {
|
||||||
|
const setter = useContext(ViewIslandSetContext);
|
||||||
|
|
||||||
|
if (!setter) {
|
||||||
|
throw new Error(
|
||||||
|
'ViewIslandProvider must be used inside ViewIslandRegistryProvider'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [island] = useState<Island>(createIsland());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setter(prev => ({ ...prev, [id]: island }));
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setter(prev => {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[id];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, [id, island, setter]);
|
||||||
|
|
||||||
|
return <island.Provider>{children}</island.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ViewIslandTarget = forwardRef(function ViewIslandTarget(
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
children,
|
||||||
|
...otherProps
|
||||||
|
}: { id: string } & React.HTMLProps<HTMLDivElement>,
|
||||||
|
ref: Ref<HTMLDivElement>
|
||||||
|
) {
|
||||||
|
const island = useContext(ViewIslandRegistryContext)[id];
|
||||||
|
if (!island) {
|
||||||
|
return <div ref={ref} {...otherProps} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<island.Target ref={ref} {...otherProps}>
|
||||||
|
{children}
|
||||||
|
</island.Target>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ViewIslandRegistryProvider = ({
|
||||||
|
children,
|
||||||
|
}: React.PropsWithChildren) => {
|
||||||
|
const [contextValue, setContextValue] = useState<ViewIslandRegistry>({});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ViewIslandRegistryContext.Provider value={contextValue}>
|
||||||
|
<ViewIslandSetContext.Provider value={setContextValue}>
|
||||||
|
{children}
|
||||||
|
</ViewIslandSetContext.Provider>
|
||||||
|
</ViewIslandRegistryContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ViewBody = ({ children }: React.PropsWithChildren) => {
|
||||||
|
const view = useService(ViewService).view;
|
||||||
|
|
||||||
|
return <ViewIsland id={`${view.id}:body`}>{children}</ViewIsland>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ViewBodyTarget = forwardRef(function ViewBodyTarget(
|
||||||
|
{
|
||||||
|
viewId,
|
||||||
|
...otherProps
|
||||||
|
}: React.HTMLProps<HTMLDivElement> & { viewId: string },
|
||||||
|
ref: React.ForwardedRef<HTMLDivElement>
|
||||||
|
) {
|
||||||
|
return <ViewIslandTarget id={`${viewId}:body`} {...otherProps} ref={ref} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ViewHeader = ({ children }: React.PropsWithChildren) => {
|
||||||
|
const view = useService(ViewService).view;
|
||||||
|
|
||||||
|
return <ViewIsland id={`${view.id}:header`}>{children}</ViewIsland>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ViewHeaderTarget = forwardRef(function ViewHeaderTarget(
|
||||||
|
{
|
||||||
|
viewId,
|
||||||
|
...otherProps
|
||||||
|
}: React.HTMLProps<HTMLDivElement> & { viewId: string },
|
||||||
|
ref: React.ForwardedRef<HTMLDivElement>
|
||||||
|
) {
|
||||||
|
return <ViewIslandTarget id={`${viewId}:header`} {...otherProps} ref={ref} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ViewSidebarTab = ({
|
||||||
|
children,
|
||||||
|
tabId,
|
||||||
|
icon,
|
||||||
|
unmountOnInactive = true,
|
||||||
|
}: React.PropsWithChildren<{
|
||||||
|
tabId: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
unmountOnInactive?: boolean;
|
||||||
|
}>) => {
|
||||||
|
const view = useService(ViewService).view;
|
||||||
|
const activeTab = useLiveData(view.activeSidebarTab$);
|
||||||
|
useEffect(() => {
|
||||||
|
view.addSidebarTab(tabId);
|
||||||
|
return () => {
|
||||||
|
view.removeSidebarTab(tabId);
|
||||||
|
};
|
||||||
|
}, [tabId, view]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ViewIsland id={`${view.id}:sidebar:${tabId}:icon`}>{icon}</ViewIsland>
|
||||||
|
<ViewIsland id={`${view.id}:sidebar:${tabId}:body`}>
|
||||||
|
{unmountOnInactive && activeTab?.id !== tabId ? null : children}
|
||||||
|
</ViewIsland>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ViewSidebarTabIconTarget = forwardRef(
|
||||||
|
function ViewSidebarTabIconTarget({
|
||||||
|
viewId,
|
||||||
|
tabId,
|
||||||
|
...otherProps
|
||||||
|
}: React.HTMLProps<HTMLDivElement> & { tabId: string; viewId: string }) {
|
||||||
|
return (
|
||||||
|
<ViewIslandTarget
|
||||||
|
id={`${viewId}:sidebar:${tabId}:icon`}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ViewSidebarTabBodyTarget = forwardRef(
|
||||||
|
function ViewSidebarTabBodyTarget({
|
||||||
|
viewId,
|
||||||
|
tabId,
|
||||||
|
...otherProps
|
||||||
|
}: React.HTMLProps<HTMLDivElement> & {
|
||||||
|
tabId: string;
|
||||||
|
viewId: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ViewIslandTarget
|
||||||
|
id={`${viewId}:sidebar:${tabId}:body`}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { cssVar } from '@toeverything/theme';
|
||||||
import { style } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
export const workbenchRootContainer = style({
|
export const workbenchRootContainer = style({
|
||||||
@@ -12,3 +13,19 @@ export const workbenchViewContainer = style({
|
|||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const workbenchSidebar = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexShrink: 0,
|
||||||
|
height: '100%',
|
||||||
|
right: 0,
|
||||||
|
selectors: {
|
||||||
|
[`&[data-client-border=true]`]: {
|
||||||
|
paddingLeft: 8,
|
||||||
|
borderRadius: 6,
|
||||||
|
},
|
||||||
|
[`&[data-client-border=false]`]: {
|
||||||
|
borderLeft: `1px solid ${cssVar('borderColor')}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { ResizePanel } from '@affine/component/resize-panel';
|
||||||
import { memo, useCallback, useEffect, useRef } from 'react';
|
import { rightSidebarWidthAtom } from '@affine/core/atoms';
|
||||||
|
import {
|
||||||
|
appSettingAtom,
|
||||||
|
FrameworkScope,
|
||||||
|
useLiveData,
|
||||||
|
useService,
|
||||||
|
} from '@toeverything/infra';
|
||||||
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
|
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import type { View } from '../entities/view';
|
import type { View } from '../entities/view';
|
||||||
import { WorkbenchService } from '../services/workbench';
|
import { WorkbenchService } from '../services/workbench';
|
||||||
import { useBindWorkbenchToBrowserRouter } from './browser-adapter';
|
import { useBindWorkbenchToBrowserRouter } from './browser-adapter';
|
||||||
import { useBindWorkbenchToDesktopRouter } from './desktop-adapter';
|
import { useBindWorkbenchToDesktopRouter } from './desktop-adapter';
|
||||||
|
import { SidebarContainer } from './sidebar/sidebar-container';
|
||||||
import { SplitView } from './split-view/split-view';
|
import { SplitView } from './split-view/split-view';
|
||||||
|
import { ViewIslandRegistryProvider } from './view-islands';
|
||||||
import { ViewRoot } from './view-root';
|
import { ViewRoot } from './view-root';
|
||||||
import * as styles from './workbench-root.css';
|
import * as styles from './workbench-root.css';
|
||||||
|
|
||||||
@@ -43,12 +53,15 @@ export const WorkbenchRoot = memo(() => {
|
|||||||
}, [basename, workbench.basename$]);
|
}, [basename, workbench.basename$]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SplitView
|
<ViewIslandRegistryProvider>
|
||||||
className={styles.workbenchRootContainer}
|
<SplitView
|
||||||
views={views}
|
className={styles.workbenchRootContainer}
|
||||||
renderer={panelRenderer}
|
views={views}
|
||||||
onMove={onMove}
|
renderer={panelRenderer}
|
||||||
/>
|
onMove={onMove}
|
||||||
|
/>
|
||||||
|
<WorkbenchSidebar />
|
||||||
|
</ViewIslandRegistryProvider>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -84,3 +97,67 @@ const WorkbenchView = ({ view, index }: { view: View; index: number }) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MIN_SIDEBAR_WIDTH = 320;
|
||||||
|
const MAX_SIDEBAR_WIDTH = 800;
|
||||||
|
|
||||||
|
const WorkbenchSidebar = () => {
|
||||||
|
const { clientBorder } = useAtomValue(appSettingAtom);
|
||||||
|
|
||||||
|
const [width, setWidth] = useAtom(rightSidebarWidthAtom);
|
||||||
|
const [resizing, setResizing] = useState(false);
|
||||||
|
|
||||||
|
const workbench = useService(WorkbenchService).workbench;
|
||||||
|
|
||||||
|
const views = useLiveData(workbench.views$);
|
||||||
|
const activeView = useLiveData(workbench.activeView$);
|
||||||
|
const sidebarOpen = useLiveData(workbench.sidebarOpen$);
|
||||||
|
const [floating, setFloating] = useState(false);
|
||||||
|
|
||||||
|
const handleOpenChange = useCallback(
|
||||||
|
(open: boolean) => {
|
||||||
|
if (open) {
|
||||||
|
workbench.openSidebar();
|
||||||
|
} else {
|
||||||
|
workbench.closeSidebar();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[workbench]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onResize = () => setFloating(!!(window.innerWidth < 768));
|
||||||
|
onResize();
|
||||||
|
window.addEventListener('resize', onResize);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', onResize);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResizePanel
|
||||||
|
floating={floating}
|
||||||
|
resizeHandlePos="left"
|
||||||
|
resizeHandleOffset={clientBorder ? 3.5 : 0}
|
||||||
|
width={width}
|
||||||
|
resizing={resizing}
|
||||||
|
onResizing={setResizing}
|
||||||
|
className={styles.workbenchSidebar}
|
||||||
|
data-client-border={clientBorder && sidebarOpen}
|
||||||
|
open={sidebarOpen}
|
||||||
|
onOpen={handleOpenChange}
|
||||||
|
onWidthChange={setWidth}
|
||||||
|
minWidth={MIN_SIDEBAR_WIDTH}
|
||||||
|
maxWidth={MAX_SIDEBAR_WIDTH}
|
||||||
|
unmountOnExit={false}
|
||||||
|
>
|
||||||
|
{views.map(view => (
|
||||||
|
<FrameworkScope key={view.id} scope={view.scope}>
|
||||||
|
<SidebarContainer
|
||||||
|
style={{ display: activeView !== view ? 'none' : undefined }}
|
||||||
|
/>
|
||||||
|
</FrameworkScope>
|
||||||
|
))}
|
||||||
|
</ResizePanel>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { nanoid } from 'nanoid';
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { CollectionService } from '../../../modules/collection';
|
import { CollectionService } from '../../../modules/collection';
|
||||||
import { ViewBodyIsland, ViewHeaderIsland } from '../../../modules/workbench';
|
import { ViewBody, ViewHeader } from '../../../modules/workbench';
|
||||||
import { EmptyCollectionList } from '../page-list-empty';
|
import { EmptyCollectionList } from '../page-list-empty';
|
||||||
import { AllCollectionHeader } from './header';
|
import { AllCollectionHeader } from './header';
|
||||||
import * as styles from './index.css';
|
import * as styles from './index.css';
|
||||||
@@ -55,13 +55,13 @@ export const AllCollection = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewHeaderIsland>
|
<ViewHeader>
|
||||||
<AllCollectionHeader
|
<AllCollectionHeader
|
||||||
showCreateNew={!hideHeaderCreateNew}
|
showCreateNew={!hideHeaderCreateNew}
|
||||||
onCreateCollection={handleCreateCollection}
|
onCreateCollection={handleCreateCollection}
|
||||||
/>
|
/>
|
||||||
</ViewHeaderIsland>
|
</ViewHeader>
|
||||||
<ViewBodyIsland>
|
<ViewBody>
|
||||||
<div className={styles.body}>
|
<div className={styles.body}>
|
||||||
{collectionMetas.length > 0 ? (
|
{collectionMetas.length > 0 ? (
|
||||||
<VirtualizedCollectionList
|
<VirtualizedCollectionList
|
||||||
@@ -82,7 +82,7 @@ export const AllCollection = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ViewBodyIsland>
|
</ViewBody>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import type { Filter } from '@affine/env/filter';
|
|||||||
import { useService, WorkspaceService } from '@toeverything/infra';
|
import { useService, WorkspaceService } from '@toeverything/infra';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { ViewBodyIsland, ViewHeaderIsland } from '../../../modules/workbench';
|
import { ViewBody, ViewHeader } 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';
|
||||||
import { FilterContainer } from './all-page-filter';
|
import { FilterContainer } from './all-page-filter';
|
||||||
@@ -27,14 +27,14 @@ export const AllPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewHeaderIsland>
|
<ViewHeader>
|
||||||
<AllPageHeader
|
<AllPageHeader
|
||||||
showCreateNew={!hideHeaderCreateNew}
|
showCreateNew={!hideHeaderCreateNew}
|
||||||
filters={filters}
|
filters={filters}
|
||||||
onChangeFilters={setFilters}
|
onChangeFilters={setFilters}
|
||||||
/>
|
/>
|
||||||
</ViewHeaderIsland>
|
</ViewHeader>
|
||||||
<ViewBodyIsland>
|
<ViewBody>
|
||||||
<div className={styles.body}>
|
<div className={styles.body}>
|
||||||
<FilterContainer filters={filters} onChangeFilters={setFilters} />
|
<FilterContainer filters={filters} onChangeFilters={setFilters} />
|
||||||
{filteredPageMetas.length > 0 ? (
|
{filteredPageMetas.length > 0 ? (
|
||||||
@@ -50,7 +50,7 @@ export const AllPage = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ViewBodyIsland>
|
</ViewBody>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { DeleteTagConfirmModal, TagService } from '@affine/core/modules/tag';
|
|||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { ViewBodyIsland, ViewHeaderIsland } from '../../../modules/workbench';
|
import { ViewBody, ViewHeader } 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';
|
||||||
@@ -56,10 +56,10 @@ export const AllTag = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewHeaderIsland>
|
<ViewHeader>
|
||||||
<AllTagHeader />
|
<AllTagHeader />
|
||||||
</ViewHeaderIsland>
|
</ViewHeader>
|
||||||
<ViewBodyIsland>
|
<ViewBody>
|
||||||
<div className={styles.body}>
|
<div className={styles.body}>
|
||||||
{tags.length > 0 ? (
|
{tags.length > 0 ? (
|
||||||
<VirtualizedTagList
|
<VirtualizedTagList
|
||||||
@@ -71,7 +71,7 @@ export const AllTag = () => {
|
|||||||
<EmptyTagList heading={<EmptyTagListHeader />} />
|
<EmptyTagList heading={<EmptyTagListHeader />} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ViewBodyIsland>
|
</ViewBody>
|
||||||
<DeleteTagConfirmModal
|
<DeleteTagConfirmModal
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={handleCloseModal}
|
onOpenChange={handleCloseModal}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { useCallback, useEffect, useState } from 'react';
|
|||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
||||||
import { ViewBodyIsland, ViewHeaderIsland } from '../../../modules/workbench';
|
import { ViewBody, ViewHeader } from '../../../modules/workbench';
|
||||||
import { WorkspaceSubPath } from '../../../shared';
|
import { WorkspaceSubPath } from '../../../shared';
|
||||||
import * as styles from './collection.css';
|
import * as styles from './collection.css';
|
||||||
import { CollectionDetailHeader } from './header';
|
import { CollectionDetailHeader } from './header';
|
||||||
@@ -40,18 +40,18 @@ export const CollectionDetail = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewHeaderIsland>
|
<ViewHeader>
|
||||||
<CollectionDetailHeader
|
<CollectionDetailHeader
|
||||||
showCreateNew={!hideHeaderCreateNew}
|
showCreateNew={!hideHeaderCreateNew}
|
||||||
onCreate={handleEditCollection}
|
onCreate={handleEditCollection}
|
||||||
/>
|
/>
|
||||||
</ViewHeaderIsland>
|
</ViewHeader>
|
||||||
<ViewBodyIsland>
|
<ViewBody>
|
||||||
<VirtualizedPageList
|
<VirtualizedPageList
|
||||||
collection={collection}
|
collection={collection}
|
||||||
setHideHeaderCreateNewPage={setHideHeaderCreateNew}
|
setHideHeaderCreateNewPage={setHideHeaderCreateNew}
|
||||||
/>
|
/>
|
||||||
</ViewBodyIsland>
|
</ViewBody>
|
||||||
{node}
|
{node}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -127,7 +127,7 @@ const Placeholder = ({ collection }: { collection: Collection }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewHeaderIsland>
|
<ViewHeader>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -166,8 +166,8 @@ const Placeholder = ({ collection }: { collection: Collection }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1 }} />
|
<div style={{ flex: 1 }} />
|
||||||
</div>
|
</div>
|
||||||
</ViewHeaderIsland>
|
</ViewHeader>
|
||||||
<ViewBodyIsland>
|
<ViewBody>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -302,7 +302,7 @@ const Placeholder = ({ collection }: { collection: Collection }) => {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</ViewBodyIsland>
|
</ViewBody>
|
||||||
{node}
|
{node}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { Divider, type InlineEditHandle } from '@affine/component';
|
import {
|
||||||
|
Divider,
|
||||||
|
type InlineEditHandle,
|
||||||
|
observeResize,
|
||||||
|
} from '@affine/component';
|
||||||
import { openInfoModalAtom } from '@affine/core/atoms';
|
import { openInfoModalAtom } from '@affine/core/atoms';
|
||||||
import { InfoModal } from '@affine/core/components/affine/page-properties';
|
import { InfoModal } from '@affine/core/components/affine/page-properties';
|
||||||
import { FavoriteButton } from '@affine/core/components/blocksuite/block-suite-header/favorite';
|
import { FavoriteButton } from '@affine/core/components/blocksuite/block-suite-header/favorite';
|
||||||
@@ -13,7 +17,7 @@ import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
|
|||||||
import type { Doc } from '@blocksuite/store';
|
import type { Doc } from '@blocksuite/store';
|
||||||
import { type Workspace } from '@toeverything/infra';
|
import { type Workspace } from '@toeverything/infra';
|
||||||
import { useAtom, useAtomValue } from 'jotai';
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
import { useCallback, useRef } from 'react';
|
import { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { SharePageButton } from '../../../components/affine/share-page-modal';
|
import { SharePageButton } from '../../../components/affine/share-page-modal';
|
||||||
import { appSidebarFloatingAtom } from '../../../components/app-sidebar';
|
import { appSidebarFloatingAtom } from '../../../components/app-sidebar';
|
||||||
@@ -22,36 +26,50 @@ import { HeaderDivider } from '../../../components/pure/header';
|
|||||||
import * as styles from './detail-page-header.css';
|
import * as styles from './detail-page-header.css';
|
||||||
import { useDetailPageHeaderResponsive } from './use-header-responsive';
|
import { useDetailPageHeaderResponsive } from './use-header-responsive';
|
||||||
|
|
||||||
function Header({
|
const Header = forwardRef<
|
||||||
children,
|
HTMLDivElement,
|
||||||
style,
|
{
|
||||||
className,
|
children: React.ReactNode;
|
||||||
}: {
|
className?: string;
|
||||||
children: React.ReactNode;
|
style?: React.CSSProperties;
|
||||||
className?: string;
|
}
|
||||||
style?: React.CSSProperties;
|
>(({ children, style, className }, ref) => {
|
||||||
}) {
|
|
||||||
const appSidebarFloating = useAtomValue(appSidebarFloatingAtom);
|
const appSidebarFloating = useAtomValue(appSidebarFloatingAtom);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-testid="header"
|
data-testid="header"
|
||||||
style={style}
|
style={style}
|
||||||
className={className}
|
className={className}
|
||||||
|
ref={ref}
|
||||||
data-sidebar-floating={appSidebarFloating}
|
data-sidebar-floating={appSidebarFloating}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
Header.displayName = 'forwardRef(Header)';
|
||||||
|
|
||||||
interface PageHeaderProps {
|
interface PageHeaderProps {
|
||||||
page: Doc;
|
page: Doc;
|
||||||
workspace: Workspace;
|
workspace: Workspace;
|
||||||
}
|
}
|
||||||
export function JournalPageHeader({ page, workspace }: PageHeaderProps) {
|
export function JournalPageHeader({ page, workspace }: PageHeaderProps) {
|
||||||
const { hideShare, hideToday } = useDetailPageHeaderResponsive();
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [containerWidth, setContainerWidth] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
return observeResize(container, entry => {
|
||||||
|
setContainerWidth(entry.contentRect.width);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const { hideShare, hideToday } =
|
||||||
|
useDetailPageHeaderResponsive(containerWidth);
|
||||||
return (
|
return (
|
||||||
<Header className={styles.header}>
|
<Header className={styles.header} ref={containerRef}>
|
||||||
<EditorModeSwitch
|
<EditorModeSwitch
|
||||||
docCollection={workspace.docCollection}
|
docCollection={workspace.docCollection}
|
||||||
pageId={page?.id}
|
pageId={page?.id}
|
||||||
@@ -66,7 +84,11 @@ export function JournalPageHeader({ page, workspace }: PageHeaderProps) {
|
|||||||
<JournalTodayButton docCollection={workspace.docCollection} />
|
<JournalTodayButton docCollection={workspace.docCollection} />
|
||||||
)}
|
)}
|
||||||
<HeaderDivider />
|
<HeaderDivider />
|
||||||
<PageHeaderMenuButton isJournal page={page} />
|
<PageHeaderMenuButton
|
||||||
|
isJournal
|
||||||
|
page={page}
|
||||||
|
containerWidth={containerWidth}
|
||||||
|
/>
|
||||||
{page && !hideShare ? (
|
{page && !hideShare ? (
|
||||||
<SharePageButton workspace={workspace} page={page} />
|
<SharePageButton workspace={workspace} page={page} />
|
||||||
) : null}
|
) : null}
|
||||||
@@ -76,14 +98,25 @@ export function JournalPageHeader({ page, workspace }: PageHeaderProps) {
|
|||||||
|
|
||||||
export function NormalPageHeader({ page, workspace }: PageHeaderProps) {
|
export function NormalPageHeader({ page, workspace }: PageHeaderProps) {
|
||||||
const titleInputHandleRef = useRef<InlineEditHandle>(null);
|
const titleInputHandleRef = useRef<InlineEditHandle>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [containerWidth, setContainerWidth] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
return observeResize(container, entry => {
|
||||||
|
setContainerWidth(entry.contentRect.width);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const { hideCollect, hideShare, hidePresent, showDivider } =
|
const { hideCollect, hideShare, hidePresent, showDivider } =
|
||||||
useDetailPageHeaderResponsive();
|
useDetailPageHeaderResponsive(containerWidth);
|
||||||
|
|
||||||
const onRename = useCallback(() => {
|
const onRename = useCallback(() => {
|
||||||
setTimeout(() => titleInputHandleRef.current?.triggerEdit());
|
setTimeout(() => titleInputHandleRef.current?.triggerEdit());
|
||||||
}, []);
|
}, []);
|
||||||
return (
|
return (
|
||||||
<Header className={styles.header}>
|
<Header className={styles.header} ref={containerRef}>
|
||||||
<EditorModeSwitch
|
<EditorModeSwitch
|
||||||
docCollection={workspace.docCollection}
|
docCollection={workspace.docCollection}
|
||||||
pageId={page?.id}
|
pageId={page?.id}
|
||||||
@@ -100,7 +133,11 @@ export function NormalPageHeader({ page, workspace }: PageHeaderProps) {
|
|||||||
{runtimeConfig.enableInfoModal ? <InfoButton /> : null}
|
{runtimeConfig.enableInfoModal ? <InfoButton /> : null}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<PageHeaderMenuButton rename={onRename} page={page} />
|
<PageHeaderMenuButton
|
||||||
|
rename={onRename}
|
||||||
|
page={page}
|
||||||
|
containerWidth={containerWidth}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.spacer} />
|
<div className={styles.spacer} />
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Scrollable } from '@affine/component';
|
import { Scrollable } from '@affine/component';
|
||||||
import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton';
|
import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton';
|
||||||
|
import type { ChatPanel } from '@affine/core/blocksuite/presets/ai';
|
||||||
import { AIProvider } from '@affine/core/blocksuite/presets/ai';
|
import { AIProvider } from '@affine/core/blocksuite/presets/ai';
|
||||||
import { PageAIOnboarding } from '@affine/core/components/affine/ai-onboarding';
|
import { PageAIOnboarding } from '@affine/core/components/affine/ai-onboarding';
|
||||||
import { AIIsland } from '@affine/core/components/pure/ai-island';
|
|
||||||
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
|
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
|
||||||
import { RecentDocsService } from '@affine/core/modules/quicksearch';
|
import { RecentDocsService } from '@affine/core/modules/quicksearch';
|
||||||
|
import { ViewService } from '@affine/core/modules/workbench/services/view';
|
||||||
import type { PageRootService } from '@blocksuite/blocks';
|
import type { PageRootService } from '@blocksuite/blocks';
|
||||||
import {
|
import {
|
||||||
BookmarkBlockService,
|
BookmarkBlockService,
|
||||||
@@ -15,6 +16,7 @@ import {
|
|||||||
ImageBlockService,
|
ImageBlockService,
|
||||||
} from '@blocksuite/blocks';
|
} from '@blocksuite/blocks';
|
||||||
import { DisposableGroup } from '@blocksuite/global/utils';
|
import { DisposableGroup } from '@blocksuite/global/utils';
|
||||||
|
import { AiIcon, FrameIcon, TocIcon, TodayIcon } from '@blocksuite/icons/rc';
|
||||||
import { type AffineEditorContainer } from '@blocksuite/presets';
|
import { type AffineEditorContainer } from '@blocksuite/presets';
|
||||||
import type { Doc as BlockSuiteDoc } from '@blocksuite/store';
|
import type { Doc as BlockSuiteDoc } from '@blocksuite/store';
|
||||||
import type { Doc } from '@toeverything/infra';
|
import type { Doc } from '@toeverything/infra';
|
||||||
@@ -30,7 +32,14 @@ import {
|
|||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
import { memo, useCallback, useEffect, useLayoutEffect, useState } from 'react';
|
import {
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import type { Map as YMap } from 'yjs';
|
import type { Map as YMap } from 'yjs';
|
||||||
|
|
||||||
@@ -43,29 +52,26 @@ import { useRegisterBlocksuiteEditorCommands } from '../../../hooks/affine/use-r
|
|||||||
import { useActiveBlocksuiteEditor } from '../../../hooks/use-block-suite-editor';
|
import { useActiveBlocksuiteEditor } from '../../../hooks/use-block-suite-editor';
|
||||||
import { usePageDocumentTitle } from '../../../hooks/use-global-state';
|
import { usePageDocumentTitle } from '../../../hooks/use-global-state';
|
||||||
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
||||||
import {
|
|
||||||
MultiTabSidebarBody,
|
|
||||||
MultiTabSidebarHeaderSwitcher,
|
|
||||||
sidebarTabs,
|
|
||||||
type TabOnLoadFn,
|
|
||||||
} from '../../../modules/multi-tab-sidebar';
|
|
||||||
import {
|
|
||||||
RightSidebarService,
|
|
||||||
RightSidebarViewIsland,
|
|
||||||
} from '../../../modules/right-sidebar';
|
|
||||||
import {
|
import {
|
||||||
useIsActiveView,
|
useIsActiveView,
|
||||||
ViewBodyIsland,
|
ViewBody,
|
||||||
ViewHeaderIsland,
|
ViewHeader,
|
||||||
|
ViewSidebarTab,
|
||||||
|
WorkbenchService,
|
||||||
} from '../../../modules/workbench';
|
} from '../../../modules/workbench';
|
||||||
import { performanceRenderLogger } from '../../../shared';
|
import { performanceRenderLogger } from '../../../shared';
|
||||||
import { PageNotFound } from '../../404';
|
import { PageNotFound } from '../../404';
|
||||||
import * as styles from './detail-page.css';
|
import * as styles from './detail-page.css';
|
||||||
import { DetailPageHeader } from './detail-page-header';
|
import { DetailPageHeader } from './detail-page-header';
|
||||||
|
import { EditorChatPanel } from './tabs/chat';
|
||||||
|
import { EditorFramePanel } from './tabs/frame';
|
||||||
|
import { EditorJournalPanel } from './tabs/journal';
|
||||||
|
import { EditorOutline } from './tabs/outline';
|
||||||
|
|
||||||
const DetailPageImpl = memo(function DetailPageImpl() {
|
const DetailPageImpl = memo(function DetailPageImpl() {
|
||||||
const rightSidebar = useService(RightSidebarService).rightSidebar;
|
const workbench = useService(WorkbenchService).workbench;
|
||||||
const activeTabName = useLiveData(rightSidebar.activeTabName$);
|
const view = useService(ViewService).view;
|
||||||
|
const activeSidebarTab = useLiveData(view.activeSidebarTab$);
|
||||||
|
|
||||||
const doc = useService(DocService).doc;
|
const doc = useService(DocService).doc;
|
||||||
const { openPage, jumpToPageBlock, jumpToTag } = useNavigateHelper();
|
const { openPage, jumpToPageBlock, jumpToTag } = useNavigateHelper();
|
||||||
@@ -75,18 +81,12 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
|||||||
const docCollection = workspace.docCollection;
|
const docCollection = workspace.docCollection;
|
||||||
const mode = useLiveData(doc.mode$);
|
const mode = useLiveData(doc.mode$);
|
||||||
const { appSettings } = useAppSettingHelper();
|
const { appSettings } = useAppSettingHelper();
|
||||||
const [tabOnLoad, setTabOnLoad] = useState<TabOnLoadFn | null>(null);
|
const chatPanelRef = useRef<ChatPanel | null>(null);
|
||||||
|
|
||||||
const isActiveView = useIsActiveView();
|
const isActiveView = useIsActiveView();
|
||||||
// TODO(@eyhn): remove jotai here
|
// TODO(@eyhn): remove jotai here
|
||||||
const [_, setActiveBlockSuiteEditor] = useActiveBlocksuiteEditor();
|
const [_, setActiveBlockSuiteEditor] = useActiveBlocksuiteEditor();
|
||||||
|
|
||||||
const setActiveTabName = useCallback(
|
|
||||||
(...args: Parameters<typeof rightSidebar.setActiveTabName>) =>
|
|
||||||
rightSidebar.setActiveTabName(...args),
|
|
||||||
[rightSidebar]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isActiveView) {
|
if (isActiveView) {
|
||||||
setActiveBlockSuiteEditor(editor);
|
setActiveBlockSuiteEditor(editor);
|
||||||
@@ -95,28 +95,17 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const disposable = AIProvider.slots.requestOpenWithChat.on(params => {
|
const disposable = AIProvider.slots.requestOpenWithChat.on(params => {
|
||||||
const opened = rightSidebar.isOpen$.value;
|
console.log(params);
|
||||||
const actived = activeTabName === 'chat';
|
workbench.openSidebar();
|
||||||
|
view.activeSidebarTab('chat');
|
||||||
|
|
||||||
if (!opened) {
|
if (chatPanelRef.current) {
|
||||||
rightSidebar.open();
|
const chatCards = chatPanelRef.current.querySelector('chat-cards');
|
||||||
}
|
if (chatCards) chatCards.temporaryParams = params;
|
||||||
if (!actived) {
|
|
||||||
setActiveTabName('chat');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save chat parameters:
|
|
||||||
// * The right sidebar is not open
|
|
||||||
// * Chat panel is not activated
|
|
||||||
if (!opened || !actived) {
|
|
||||||
const callback = AIProvider.genRequestChatCardsFn(params);
|
|
||||||
setTabOnLoad(() => callback);
|
|
||||||
} else {
|
|
||||||
setTabOnLoad(null);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return () => disposable.dispose();
|
return () => disposable.dispose();
|
||||||
}, [activeTabName, rightSidebar, setActiveTabName]);
|
}, [activeSidebarTab, view, workbench]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isActiveView) {
|
if (isActiveView) {
|
||||||
@@ -224,16 +213,13 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const isWindowsDesktop = environment.isDesktop && environment.isWindows;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewHeaderIsland>
|
<ViewHeader>
|
||||||
<DetailPageHeader page={doc.blockSuiteDoc} workspace={workspace} />
|
<DetailPageHeader page={doc.blockSuiteDoc} workspace={workspace} />
|
||||||
</ViewHeaderIsland>
|
</ViewHeader>
|
||||||
<ViewBodyIsland>
|
<ViewBody>
|
||||||
<div className={styles.mainContainer}>
|
<div className={styles.mainContainer}>
|
||||||
<AIIsland />
|
|
||||||
{/* Add a key to force rerender when page changed, to avoid error boundary persisting. */}
|
{/* Add a key to force rerender when page changed, to avoid error boundary persisting. */}
|
||||||
<AffineErrorBoundary key={doc.id}>
|
<AffineErrorBoundary key={doc.id}>
|
||||||
<TopTip pageId={doc.id} workspace={workspace} />
|
<TopTip pageId={doc.id} workspace={workspace} />
|
||||||
@@ -260,39 +246,24 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
|||||||
</AffineErrorBoundary>
|
</AffineErrorBoundary>
|
||||||
{isInTrash ? <TrashPageFooter /> : null}
|
{isInTrash ? <TrashPageFooter /> : null}
|
||||||
</div>
|
</div>
|
||||||
</ViewBodyIsland>
|
</ViewBody>
|
||||||
|
|
||||||
|
<ViewSidebarTab tabId="chat" icon={<AiIcon />} unmountOnInactive={false}>
|
||||||
|
<EditorChatPanel editor={editor} ref={chatPanelRef} />
|
||||||
|
</ViewSidebarTab>
|
||||||
|
|
||||||
|
<ViewSidebarTab tabId="journal" icon={<TodayIcon />}>
|
||||||
|
<EditorJournalPanel />
|
||||||
|
</ViewSidebarTab>
|
||||||
|
|
||||||
|
<ViewSidebarTab tabId="outline" icon={<TocIcon />}>
|
||||||
|
<EditorOutline editor={editor} />
|
||||||
|
</ViewSidebarTab>
|
||||||
|
|
||||||
|
<ViewSidebarTab tabId="frame" icon={<FrameIcon />}>
|
||||||
|
<EditorFramePanel editor={editor} />
|
||||||
|
</ViewSidebarTab>
|
||||||
|
|
||||||
<RightSidebarViewIsland
|
|
||||||
active={isActiveView}
|
|
||||||
header={
|
|
||||||
!isWindowsDesktop ? (
|
|
||||||
<MultiTabSidebarHeaderSwitcher
|
|
||||||
activeTabName={activeTabName ?? sidebarTabs[0]?.name}
|
|
||||||
setActiveTabName={setActiveTabName}
|
|
||||||
tabs={sidebarTabs}
|
|
||||||
/>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
body={
|
|
||||||
<MultiTabSidebarBody
|
|
||||||
editor={editor}
|
|
||||||
tab={
|
|
||||||
sidebarTabs.find(ext => ext.name === activeTabName) ??
|
|
||||||
sidebarTabs[0]
|
|
||||||
}
|
|
||||||
onLoad={tabOnLoad}
|
|
||||||
>
|
|
||||||
{/* Show switcher in body for windows desktop */}
|
|
||||||
{isWindowsDesktop && (
|
|
||||||
<MultiTabSidebarHeaderSwitcher
|
|
||||||
activeTabName={activeTabName ?? sidebarTabs[0]?.name}
|
|
||||||
setActiveTabName={setActiveTabName}
|
|
||||||
tabs={sidebarTabs}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</MultiTabSidebarBody>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<GlobalPageHistoryModal />
|
<GlobalPageHistoryModal />
|
||||||
<PageAIOnboarding />
|
<PageAIOnboarding />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
import { ChatPanel } from '@affine/core/blocksuite/presets/ai';
|
import { ChatPanel } from '@affine/core/blocksuite/presets/ai';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
import { AiIcon } from '@blocksuite/icons/rc';
|
import type { AffineEditorContainer } from '@blocksuite/presets';
|
||||||
import { useCallback, useEffect, useRef } from 'react';
|
import { forwardRef, useCallback, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
import type { SidebarTab, SidebarTabProps } from '../sidebar-tab';
|
|
||||||
import * as styles from './chat.css';
|
import * as styles from './chat.css';
|
||||||
|
|
||||||
|
export interface SidebarTabProps {
|
||||||
|
editor: AffineEditorContainer | null;
|
||||||
|
onLoad?: ((component: HTMLElement) => void) | null;
|
||||||
|
}
|
||||||
|
|
||||||
// A wrapper for CopilotPanel
|
// A wrapper for CopilotPanel
|
||||||
const EditorChatPanel = ({ editor, onLoad }: SidebarTabProps) => {
|
export const EditorChatPanel = forwardRef(function EditorChatPanel(
|
||||||
|
{ editor, onLoad }: SidebarTabProps,
|
||||||
|
ref: React.ForwardedRef<ChatPanel>
|
||||||
|
) {
|
||||||
const chatPanelRef = useRef<ChatPanel | null>(null);
|
const chatPanelRef = useRef<ChatPanel | null>(null);
|
||||||
|
|
||||||
const onRefChange = useCallback((container: HTMLDivElement | null) => {
|
const onRefChange = useCallback((container: HTMLDivElement | null) => {
|
||||||
@@ -21,11 +28,17 @@ const EditorChatPanel = ({ editor, onLoad }: SidebarTabProps) => {
|
|||||||
if (onLoad && chatPanelRef.current) {
|
if (onLoad && chatPanelRef.current) {
|
||||||
(chatPanelRef.current as ChatPanel).updateComplete
|
(chatPanelRef.current as ChatPanel).updateComplete
|
||||||
.then(() => {
|
.then(() => {
|
||||||
onLoad(chatPanelRef.current as HTMLElement);
|
if (ref) {
|
||||||
|
if (typeof ref === 'function') {
|
||||||
|
ref(chatPanelRef.current);
|
||||||
|
} else {
|
||||||
|
ref.current = chatPanelRef.current;
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
}
|
}
|
||||||
}, [onLoad]);
|
}, [onLoad, ref]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editor) return;
|
if (!editor) return;
|
||||||
@@ -57,10 +70,4 @@ const EditorChatPanel = ({ editor, onLoad }: SidebarTabProps) => {
|
|||||||
// (copilotPanelRef.current as CopilotPanel).fitPadding = [20, 20, 20, 20];
|
// (copilotPanelRef.current as CopilotPanel).fitPadding = [20, 20, 20, 20];
|
||||||
|
|
||||||
return <div className={styles.root} ref={onRefChange} />;
|
return <div className={styles.root} ref={onRefChange} />;
|
||||||
};
|
});
|
||||||
|
|
||||||
export const chatTab: SidebarTab = {
|
|
||||||
name: 'chat',
|
|
||||||
icon: <AiIcon />,
|
|
||||||
Component: EditorChatPanel,
|
|
||||||
};
|
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
import { FrameIcon } from '@blocksuite/icons/rc';
|
import type { AffineEditorContainer } from '@blocksuite/presets';
|
||||||
import { FramePanel } from '@blocksuite/presets';
|
import { FramePanel } from '@blocksuite/presets';
|
||||||
import { useCallback, useRef } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
|
|
||||||
import type { SidebarTab, SidebarTabProps } from '../sidebar-tab';
|
|
||||||
import * as styles from './frame.css';
|
import * as styles from './frame.css';
|
||||||
|
|
||||||
// A wrapper for FramePanel
|
// A wrapper for FramePanel
|
||||||
const EditorFramePanel = ({ editor }: SidebarTabProps) => {
|
export const EditorFramePanel = ({
|
||||||
|
editor,
|
||||||
|
}: {
|
||||||
|
editor: AffineEditorContainer | null;
|
||||||
|
}) => {
|
||||||
const framePanelRef = useRef<FramePanel | null>(null);
|
const framePanelRef = useRef<FramePanel | null>(null);
|
||||||
|
|
||||||
const onRefChange = useCallback((container: HTMLDivElement | null) => {
|
const onRefChange = useCallback((container: HTMLDivElement | null) => {
|
||||||
@@ -32,9 +35,3 @@ const EditorFramePanel = ({ editor }: SidebarTabProps) => {
|
|||||||
|
|
||||||
return <div className={styles.root} ref={onRefChange} />;
|
return <div className={styles.root} ref={onRefChange} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const framePanelTab: SidebarTab = {
|
|
||||||
name: 'frame',
|
|
||||||
icon: <FrameIcon />,
|
|
||||||
Component: EditorFramePanel,
|
|
||||||
};
|
|
||||||
@@ -29,7 +29,6 @@ import dayjs from 'dayjs';
|
|||||||
import type { HTMLAttributes, PropsWithChildren, ReactNode } from 'react';
|
import type { HTMLAttributes, PropsWithChildren, ReactNode } from 'react';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import type { SidebarTab } from '../sidebar-tab';
|
|
||||||
import * as styles from './journal.css';
|
import * as styles from './journal.css';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -85,7 +84,7 @@ interface JournalBlockProps {
|
|||||||
date: dayjs.Dayjs;
|
date: dayjs.Dayjs;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditorJournalPanel = () => {
|
export const EditorJournalPanel = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const doc = useService(DocService).doc;
|
const doc = useService(DocService).doc;
|
||||||
const workspace = useService(WorkspaceService).workspace;
|
const workspace = useService(WorkspaceService).workspace;
|
||||||
@@ -381,9 +380,3 @@ const JournalConflictBlock = ({ date }: JournalBlockProps) => {
|
|||||||
</ConflictList>
|
</ConflictList>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const journalTab: SidebarTab = {
|
|
||||||
name: 'journal',
|
|
||||||
icon: <TodayIcon />,
|
|
||||||
Component: EditorJournalPanel,
|
|
||||||
};
|
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
import { TocIcon } from '@blocksuite/icons/rc';
|
import type { AffineEditorContainer } from '@blocksuite/presets';
|
||||||
import { OutlinePanel } from '@blocksuite/presets';
|
import { OutlinePanel } from '@blocksuite/presets';
|
||||||
import { useCallback, useRef } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
|
|
||||||
import type { SidebarTab, SidebarTabProps } from '../sidebar-tab';
|
|
||||||
import * as styles from './outline.css';
|
import * as styles from './outline.css';
|
||||||
|
|
||||||
// A wrapper for TOCNotesPanel
|
// A wrapper for TOCNotesPanel
|
||||||
const EditorOutline = ({ editor }: SidebarTabProps) => {
|
export const EditorOutline = ({
|
||||||
|
editor,
|
||||||
|
}: {
|
||||||
|
editor: AffineEditorContainer | null;
|
||||||
|
}) => {
|
||||||
const outlinePanelRef = useRef<OutlinePanel | null>(null);
|
const outlinePanelRef = useRef<OutlinePanel | null>(null);
|
||||||
|
|
||||||
const onRefChange = useCallback((container: HTMLDivElement | null) => {
|
const onRefChange = useCallback((container: HTMLDivElement | null) => {
|
||||||
@@ -32,9 +35,3 @@ const EditorOutline = ({ editor }: SidebarTabProps) => {
|
|||||||
|
|
||||||
return <div className={styles.root} ref={onRefChange} />;
|
return <div className={styles.root} ref={onRefChange} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const outlineTab: SidebarTab = {
|
|
||||||
name: 'outline',
|
|
||||||
icon: <TocIcon />,
|
|
||||||
Component: EditorOutline,
|
|
||||||
};
|
|
||||||
@@ -1,21 +1,16 @@
|
|||||||
import { RightSidebarService } from '@affine/core/modules/right-sidebar';
|
|
||||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
import { ViewService } from '@affine/core/modules/workbench/services/view';
|
|
||||||
import { useViewPosition } from '@affine/core/modules/workbench/view/use-view-position';
|
import { useViewPosition } from '@affine/core/modules/workbench/view/use-view-position';
|
||||||
import { DocService, useLiveData, useService } from '@toeverything/infra';
|
import { DocService, useLiveData, useService } from '@toeverything/infra';
|
||||||
|
|
||||||
export const useDetailPageHeaderResponsive = () => {
|
export const useDetailPageHeaderResponsive = (availableWidth: number) => {
|
||||||
const mode = useLiveData(useService(DocService).doc.mode$);
|
const mode = useLiveData(useService(DocService).doc.mode$);
|
||||||
|
|
||||||
const view = useService(ViewService).view;
|
|
||||||
const workbench = useService(WorkbenchService).workbench;
|
const workbench = useService(WorkbenchService).workbench;
|
||||||
const availableWidth = useLiveData(view.headerContentWidth$);
|
|
||||||
const viewPosition = useViewPosition();
|
const viewPosition = useViewPosition();
|
||||||
const workbenchViewsCount = useLiveData(
|
const workbenchViewsCount = useLiveData(
|
||||||
workbench.views$.map(views => views.length)
|
workbench.views$.map(views => views.length)
|
||||||
);
|
);
|
||||||
const rightSidebar = useService(RightSidebarService).rightSidebar;
|
const rightSidebarOpen = useLiveData(workbench.sidebarOpen$);
|
||||||
const rightSidebarOpen = useLiveData(rightSidebar.isOpen$);
|
|
||||||
|
|
||||||
// share button should be hidden once split-view is enabled
|
// share button should be hidden once split-view is enabled
|
||||||
const hideShare = availableWidth < 500 || workbenchViewsCount > 1;
|
const hideShare = availableWidth < 500 || workbenchViewsCount > 1;
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import { useParams } from 'react-router-dom';
|
|||||||
|
|
||||||
import { AffineErrorBoundary } from '../../components/affine/affine-error-boundary';
|
import { AffineErrorBoundary } from '../../components/affine/affine-error-boundary';
|
||||||
import { WorkspaceLayout } from '../../layouts/workspace-layout';
|
import { WorkspaceLayout } from '../../layouts/workspace-layout';
|
||||||
import { RightSidebarContainer } from '../../modules/right-sidebar';
|
|
||||||
import { WorkbenchRoot } from '../../modules/workbench';
|
import { WorkbenchRoot } from '../../modules/workbench';
|
||||||
import { AllWorkspaceModals } from '../../providers/modal-provider';
|
import { AllWorkspaceModals } from '../../providers/modal-provider';
|
||||||
import { performanceRenderLogger } from '../../shared';
|
import { performanceRenderLogger } from '../../shared';
|
||||||
@@ -163,7 +162,6 @@ export const Component = (): ReactElement => {
|
|||||||
<AffineErrorBoundary height="100vh">
|
<AffineErrorBoundary height="100vh">
|
||||||
<WorkspaceLayout>
|
<WorkspaceLayout>
|
||||||
<WorkbenchRoot />
|
<WorkbenchRoot />
|
||||||
<RightSidebarContainer />
|
|
||||||
</WorkspaceLayout>
|
</WorkspaceLayout>
|
||||||
</AffineErrorBoundary>
|
</AffineErrorBoundary>
|
||||||
</FrameworkScope>
|
</FrameworkScope>
|
||||||
|
|||||||
@@ -4,10 +4,7 @@ import {
|
|||||||
} from '@affine/core/components/page-list';
|
} from '@affine/core/components/page-list';
|
||||||
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||||
import { TagService } from '@affine/core/modules/tag';
|
import { TagService } from '@affine/core/modules/tag';
|
||||||
import {
|
import { ViewBody, ViewHeader } from '@affine/core/modules/workbench';
|
||||||
ViewBodyIsland,
|
|
||||||
ViewHeaderIsland,
|
|
||||||
} from '@affine/core/modules/workbench';
|
|
||||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
@@ -37,10 +34,10 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewHeaderIsland>
|
<ViewHeader>
|
||||||
<TagDetailHeader />
|
<TagDetailHeader />
|
||||||
</ViewHeaderIsland>
|
</ViewHeader>
|
||||||
<ViewBodyIsland>
|
<ViewBody>
|
||||||
<div className={styles.body}>
|
<div className={styles.body}>
|
||||||
{filteredPageMetas.length > 0 ? (
|
{filteredPageMetas.length > 0 ? (
|
||||||
<VirtualizedPageList
|
<VirtualizedPageList
|
||||||
@@ -60,7 +57,7 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ViewBodyIsland>
|
</ViewBody>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { assertExists } from '@blocksuite/global/utils';
|
|||||||
import { DeleteIcon } from '@blocksuite/icons/rc';
|
import { DeleteIcon } from '@blocksuite/icons/rc';
|
||||||
import { useService, WorkspaceService } from '@toeverything/infra';
|
import { useService, WorkspaceService } from '@toeverything/infra';
|
||||||
|
|
||||||
import { ViewBodyIsland, ViewHeaderIsland } from '../../modules/workbench';
|
import { ViewBody, ViewHeader } 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';
|
||||||
|
|
||||||
@@ -39,10 +39,10 @@ export const TrashPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewHeaderIsland>
|
<ViewHeader>
|
||||||
<TrashHeader />
|
<TrashHeader />
|
||||||
</ViewHeaderIsland>
|
</ViewHeader>
|
||||||
<ViewBodyIsland>
|
<ViewBody>
|
||||||
<div className={styles.body}>
|
<div className={styles.body}>
|
||||||
{filteredPageMetas.length > 0 ? (
|
{filteredPageMetas.length > 0 ? (
|
||||||
<VirtualizedTrashList />
|
<VirtualizedTrashList />
|
||||||
@@ -53,7 +53,7 @@ export const TrashPage = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ViewBodyIsland>
|
</ViewBody>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user