mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-25 06:18:45 +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:
@@ -12,7 +12,7 @@ import { nanoid } from 'nanoid';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { CollectionService } from '../../../modules/collection';
|
||||
import { ViewBodyIsland, ViewHeaderIsland } from '../../../modules/workbench';
|
||||
import { ViewBody, ViewHeader } from '../../../modules/workbench';
|
||||
import { EmptyCollectionList } from '../page-list-empty';
|
||||
import { AllCollectionHeader } from './header';
|
||||
import * as styles from './index.css';
|
||||
@@ -55,13 +55,13 @@ export const AllCollection = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewHeaderIsland>
|
||||
<ViewHeader>
|
||||
<AllCollectionHeader
|
||||
showCreateNew={!hideHeaderCreateNew}
|
||||
onCreateCollection={handleCreateCollection}
|
||||
/>
|
||||
</ViewHeaderIsland>
|
||||
<ViewBodyIsland>
|
||||
</ViewHeader>
|
||||
<ViewBody>
|
||||
<div className={styles.body}>
|
||||
{collectionMetas.length > 0 ? (
|
||||
<VirtualizedCollectionList
|
||||
@@ -82,7 +82,7 @@ export const AllCollection = () => {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ViewBodyIsland>
|
||||
</ViewBody>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { Filter } from '@affine/env/filter';
|
||||
import { useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { ViewBodyIsland, ViewHeaderIsland } from '../../../modules/workbench';
|
||||
import { ViewBody, ViewHeader } from '../../../modules/workbench';
|
||||
import { EmptyPageList } from '../page-list-empty';
|
||||
import * as styles from './all-page.css';
|
||||
import { FilterContainer } from './all-page-filter';
|
||||
@@ -27,14 +27,14 @@ export const AllPage = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewHeaderIsland>
|
||||
<ViewHeader>
|
||||
<AllPageHeader
|
||||
showCreateNew={!hideHeaderCreateNew}
|
||||
filters={filters}
|
||||
onChangeFilters={setFilters}
|
||||
/>
|
||||
</ViewHeaderIsland>
|
||||
<ViewBodyIsland>
|
||||
</ViewHeader>
|
||||
<ViewBody>
|
||||
<div className={styles.body}>
|
||||
<FilterContainer filters={filters} onChangeFilters={setFilters} />
|
||||
{filteredPageMetas.length > 0 ? (
|
||||
@@ -50,7 +50,7 @@ export const AllPage = () => {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ViewBodyIsland>
|
||||
</ViewBody>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ import { DeleteTagConfirmModal, TagService } from '@affine/core/modules/tag';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { ViewBodyIsland, ViewHeaderIsland } from '../../../modules/workbench';
|
||||
import { ViewBody, ViewHeader } from '../../../modules/workbench';
|
||||
import { EmptyTagList } from '../page-list-empty';
|
||||
import * as styles from './all-tag.css';
|
||||
import { AllTagHeader } from './header';
|
||||
@@ -56,10 +56,10 @@ export const AllTag = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewHeaderIsland>
|
||||
<ViewHeader>
|
||||
<AllTagHeader />
|
||||
</ViewHeaderIsland>
|
||||
<ViewBodyIsland>
|
||||
</ViewHeader>
|
||||
<ViewBody>
|
||||
<div className={styles.body}>
|
||||
{tags.length > 0 ? (
|
||||
<VirtualizedTagList
|
||||
@@ -71,7 +71,7 @@ export const AllTag = () => {
|
||||
<EmptyTagList heading={<EmptyTagListHeader />} />
|
||||
)}
|
||||
</div>
|
||||
</ViewBodyIsland>
|
||||
</ViewBody>
|
||||
<DeleteTagConfirmModal
|
||||
open={open}
|
||||
onOpenChange={handleCloseModal}
|
||||
|
||||
@@ -19,7 +19,7 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
||||
import { ViewBodyIsland, ViewHeaderIsland } from '../../../modules/workbench';
|
||||
import { ViewBody, ViewHeader } from '../../../modules/workbench';
|
||||
import { WorkspaceSubPath } from '../../../shared';
|
||||
import * as styles from './collection.css';
|
||||
import { CollectionDetailHeader } from './header';
|
||||
@@ -40,18 +40,18 @@ export const CollectionDetail = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewHeaderIsland>
|
||||
<ViewHeader>
|
||||
<CollectionDetailHeader
|
||||
showCreateNew={!hideHeaderCreateNew}
|
||||
onCreate={handleEditCollection}
|
||||
/>
|
||||
</ViewHeaderIsland>
|
||||
<ViewBodyIsland>
|
||||
</ViewHeader>
|
||||
<ViewBody>
|
||||
<VirtualizedPageList
|
||||
collection={collection}
|
||||
setHideHeaderCreateNewPage={setHideHeaderCreateNew}
|
||||
/>
|
||||
</ViewBodyIsland>
|
||||
</ViewBody>
|
||||
{node}
|
||||
</>
|
||||
);
|
||||
@@ -127,7 +127,7 @@ const Placeholder = ({ collection }: { collection: Collection }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewHeaderIsland>
|
||||
<ViewHeader>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
@@ -166,8 +166,8 @@ const Placeholder = ({ collection }: { collection: Collection }) => {
|
||||
</div>
|
||||
<div style={{ flex: 1 }} />
|
||||
</div>
|
||||
</ViewHeaderIsland>
|
||||
<ViewBodyIsland>
|
||||
</ViewHeader>
|
||||
<ViewBody>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
@@ -302,7 +302,7 @@ const Placeholder = ({ collection }: { collection: Collection }) => {
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</ViewBodyIsland>
|
||||
</ViewBody>
|
||||
{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 { InfoModal } from '@affine/core/components/affine/page-properties';
|
||||
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 Workspace } from '@toeverything/infra';
|
||||
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 { appSidebarFloatingAtom } from '../../../components/app-sidebar';
|
||||
@@ -22,36 +26,50 @@ import { HeaderDivider } from '../../../components/pure/header';
|
||||
import * as styles from './detail-page-header.css';
|
||||
import { useDetailPageHeaderResponsive } from './use-header-responsive';
|
||||
|
||||
function Header({
|
||||
children,
|
||||
style,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}) {
|
||||
const Header = forwardRef<
|
||||
HTMLDivElement,
|
||||
{
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
>(({ children, style, className }, ref) => {
|
||||
const appSidebarFloating = useAtomValue(appSidebarFloatingAtom);
|
||||
return (
|
||||
<div
|
||||
data-testid="header"
|
||||
style={style}
|
||||
className={className}
|
||||
ref={ref}
|
||||
data-sidebar-floating={appSidebarFloating}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Header.displayName = 'forwardRef(Header)';
|
||||
|
||||
interface PageHeaderProps {
|
||||
page: Doc;
|
||||
workspace: Workspace;
|
||||
}
|
||||
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 (
|
||||
<Header className={styles.header}>
|
||||
<Header className={styles.header} ref={containerRef}>
|
||||
<EditorModeSwitch
|
||||
docCollection={workspace.docCollection}
|
||||
pageId={page?.id}
|
||||
@@ -66,7 +84,11 @@ export function JournalPageHeader({ page, workspace }: PageHeaderProps) {
|
||||
<JournalTodayButton docCollection={workspace.docCollection} />
|
||||
)}
|
||||
<HeaderDivider />
|
||||
<PageHeaderMenuButton isJournal page={page} />
|
||||
<PageHeaderMenuButton
|
||||
isJournal
|
||||
page={page}
|
||||
containerWidth={containerWidth}
|
||||
/>
|
||||
{page && !hideShare ? (
|
||||
<SharePageButton workspace={workspace} page={page} />
|
||||
) : null}
|
||||
@@ -76,14 +98,25 @@ export function JournalPageHeader({ page, workspace }: PageHeaderProps) {
|
||||
|
||||
export function NormalPageHeader({ page, workspace }: PageHeaderProps) {
|
||||
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 } =
|
||||
useDetailPageHeaderResponsive();
|
||||
useDetailPageHeaderResponsive(containerWidth);
|
||||
|
||||
const onRename = useCallback(() => {
|
||||
setTimeout(() => titleInputHandleRef.current?.triggerEdit());
|
||||
}, []);
|
||||
return (
|
||||
<Header className={styles.header}>
|
||||
<Header className={styles.header} ref={containerRef}>
|
||||
<EditorModeSwitch
|
||||
docCollection={workspace.docCollection}
|
||||
pageId={page?.id}
|
||||
@@ -100,7 +133,11 @@ export function NormalPageHeader({ page, workspace }: PageHeaderProps) {
|
||||
{runtimeConfig.enableInfoModal ? <InfoButton /> : null}
|
||||
</>
|
||||
)}
|
||||
<PageHeaderMenuButton rename={onRename} page={page} />
|
||||
<PageHeaderMenuButton
|
||||
rename={onRename}
|
||||
page={page}
|
||||
containerWidth={containerWidth}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.spacer} />
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Scrollable } from '@affine/component';
|
||||
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 { 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 { RecentDocsService } from '@affine/core/modules/quicksearch';
|
||||
import { ViewService } from '@affine/core/modules/workbench/services/view';
|
||||
import type { PageRootService } from '@blocksuite/blocks';
|
||||
import {
|
||||
BookmarkBlockService,
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
ImageBlockService,
|
||||
} from '@blocksuite/blocks';
|
||||
import { DisposableGroup } from '@blocksuite/global/utils';
|
||||
import { AiIcon, FrameIcon, TocIcon, TodayIcon } from '@blocksuite/icons/rc';
|
||||
import { type AffineEditorContainer } from '@blocksuite/presets';
|
||||
import type { Doc as BlockSuiteDoc } from '@blocksuite/store';
|
||||
import type { Doc } from '@toeverything/infra';
|
||||
@@ -30,7 +32,14 @@ import {
|
||||
} from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
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 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 { usePageDocumentTitle } from '../../../hooks/use-global-state';
|
||||
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 {
|
||||
useIsActiveView,
|
||||
ViewBodyIsland,
|
||||
ViewHeaderIsland,
|
||||
ViewBody,
|
||||
ViewHeader,
|
||||
ViewSidebarTab,
|
||||
WorkbenchService,
|
||||
} from '../../../modules/workbench';
|
||||
import { performanceRenderLogger } from '../../../shared';
|
||||
import { PageNotFound } from '../../404';
|
||||
import * as styles from './detail-page.css';
|
||||
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 rightSidebar = useService(RightSidebarService).rightSidebar;
|
||||
const activeTabName = useLiveData(rightSidebar.activeTabName$);
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
const view = useService(ViewService).view;
|
||||
const activeSidebarTab = useLiveData(view.activeSidebarTab$);
|
||||
|
||||
const doc = useService(DocService).doc;
|
||||
const { openPage, jumpToPageBlock, jumpToTag } = useNavigateHelper();
|
||||
@@ -75,18 +81,12 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
const docCollection = workspace.docCollection;
|
||||
const mode = useLiveData(doc.mode$);
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
const [tabOnLoad, setTabOnLoad] = useState<TabOnLoadFn | null>(null);
|
||||
const chatPanelRef = useRef<ChatPanel | null>(null);
|
||||
|
||||
const isActiveView = useIsActiveView();
|
||||
// TODO(@eyhn): remove jotai here
|
||||
const [_, setActiveBlockSuiteEditor] = useActiveBlocksuiteEditor();
|
||||
|
||||
const setActiveTabName = useCallback(
|
||||
(...args: Parameters<typeof rightSidebar.setActiveTabName>) =>
|
||||
rightSidebar.setActiveTabName(...args),
|
||||
[rightSidebar]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isActiveView) {
|
||||
setActiveBlockSuiteEditor(editor);
|
||||
@@ -95,28 +95,17 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
|
||||
useEffect(() => {
|
||||
const disposable = AIProvider.slots.requestOpenWithChat.on(params => {
|
||||
const opened = rightSidebar.isOpen$.value;
|
||||
const actived = activeTabName === 'chat';
|
||||
console.log(params);
|
||||
workbench.openSidebar();
|
||||
view.activeSidebarTab('chat');
|
||||
|
||||
if (!opened) {
|
||||
rightSidebar.open();
|
||||
}
|
||||
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);
|
||||
if (chatPanelRef.current) {
|
||||
const chatCards = chatPanelRef.current.querySelector('chat-cards');
|
||||
if (chatCards) chatCards.temporaryParams = params;
|
||||
}
|
||||
});
|
||||
return () => disposable.dispose();
|
||||
}, [activeTabName, rightSidebar, setActiveTabName]);
|
||||
}, [activeSidebarTab, view, workbench]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isActiveView) {
|
||||
@@ -224,16 +213,13 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
]
|
||||
);
|
||||
|
||||
const isWindowsDesktop = environment.isDesktop && environment.isWindows;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewHeaderIsland>
|
||||
<ViewHeader>
|
||||
<DetailPageHeader page={doc.blockSuiteDoc} workspace={workspace} />
|
||||
</ViewHeaderIsland>
|
||||
<ViewBodyIsland>
|
||||
</ViewHeader>
|
||||
<ViewBody>
|
||||
<div className={styles.mainContainer}>
|
||||
<AIIsland />
|
||||
{/* Add a key to force rerender when page changed, to avoid error boundary persisting. */}
|
||||
<AffineErrorBoundary key={doc.id}>
|
||||
<TopTip pageId={doc.id} workspace={workspace} />
|
||||
@@ -260,39 +246,24 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
</AffineErrorBoundary>
|
||||
{isInTrash ? <TrashPageFooter /> : null}
|
||||
</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 />
|
||||
<PageAIOnboarding />
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const root = style({
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
import { ChatPanel } from '@affine/core/blocksuite/presets/ai';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { AffineEditorContainer } from '@blocksuite/presets';
|
||||
import { forwardRef, useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
import * as styles from './chat.css';
|
||||
|
||||
export interface SidebarTabProps {
|
||||
editor: AffineEditorContainer | null;
|
||||
onLoad?: ((component: HTMLElement) => void) | null;
|
||||
}
|
||||
|
||||
// A wrapper for CopilotPanel
|
||||
export const EditorChatPanel = forwardRef(function EditorChatPanel(
|
||||
{ editor, onLoad }: SidebarTabProps,
|
||||
ref: React.ForwardedRef<ChatPanel>
|
||||
) {
|
||||
const chatPanelRef = useRef<ChatPanel | null>(null);
|
||||
|
||||
const onRefChange = useCallback((container: HTMLDivElement | null) => {
|
||||
if (container) {
|
||||
assertExists(chatPanelRef.current, 'chat panel should be initialized');
|
||||
container.append(chatPanelRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (onLoad && chatPanelRef.current) {
|
||||
(chatPanelRef.current as ChatPanel).updateComplete
|
||||
.then(() => {
|
||||
if (ref) {
|
||||
if (typeof ref === 'function') {
|
||||
ref(chatPanelRef.current);
|
||||
} else {
|
||||
ref.current = chatPanelRef.current;
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
}, [onLoad, ref]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return;
|
||||
const pageService = editor.host.spec.getService('affine:page');
|
||||
|
||||
const disposable = [
|
||||
pageService.slots.docLinkClicked.on(() => {
|
||||
(chatPanelRef.current as ChatPanel).doc = editor.doc;
|
||||
}),
|
||||
pageService.docModeService.onModeChange(() => {
|
||||
if (!editor.host) return;
|
||||
(chatPanelRef.current as ChatPanel).host = editor.host;
|
||||
}),
|
||||
];
|
||||
|
||||
return () => disposable.forEach(d => d.dispose());
|
||||
}, [editor]);
|
||||
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!chatPanelRef.current) {
|
||||
chatPanelRef.current = new ChatPanel();
|
||||
}
|
||||
|
||||
(chatPanelRef.current as ChatPanel).host = editor.host;
|
||||
(chatPanelRef.current as ChatPanel).doc = editor.doc;
|
||||
// (copilotPanelRef.current as CopilotPanel).fitPadding = [20, 20, 20, 20];
|
||||
|
||||
return <div className={styles.root} ref={onRefChange} />;
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const root = style({
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { AffineEditorContainer } from '@blocksuite/presets';
|
||||
import { FramePanel } from '@blocksuite/presets';
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
import * as styles from './frame.css';
|
||||
|
||||
// A wrapper for FramePanel
|
||||
export const EditorFramePanel = ({
|
||||
editor,
|
||||
}: {
|
||||
editor: AffineEditorContainer | null;
|
||||
}) => {
|
||||
const framePanelRef = useRef<FramePanel | null>(null);
|
||||
|
||||
const onRefChange = useCallback((container: HTMLDivElement | null) => {
|
||||
if (container) {
|
||||
assertExists(framePanelRef.current, 'frame panel should be initialized');
|
||||
container.append(framePanelRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!framePanelRef.current) {
|
||||
framePanelRef.current = new FramePanel();
|
||||
}
|
||||
|
||||
if (editor !== framePanelRef.current?.editor) {
|
||||
(framePanelRef.current as FramePanel).editor = editor;
|
||||
(framePanelRef.current as FramePanel).fitPadding = [20, 20, 20, 20];
|
||||
}
|
||||
|
||||
return <div className={styles.root} ref={onRefChange} />;
|
||||
};
|
||||
@@ -0,0 +1,227 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
const interactive = style({
|
||||
position: 'relative',
|
||||
cursor: 'pointer',
|
||||
selectors: {
|
||||
'&:hover': {
|
||||
backgroundColor: cssVar('hoverColor'),
|
||||
},
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
opacity: 0,
|
||||
borderRadius: 'inherit',
|
||||
boxShadow: `0 0 0 3px ${cssVar('primaryColor')}`,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
'&::after': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
borderRadius: 'inherit',
|
||||
boxShadow: `0 0 0 0px ${cssVar('primaryColor')}`,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
'&:focus-visible::before': {
|
||||
opacity: 0.2,
|
||||
},
|
||||
'&:focus-visible::after': {
|
||||
boxShadow: `0 0 0 1px ${cssVar('primaryColor')}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
export const calendar = style({
|
||||
padding: '16px',
|
||||
});
|
||||
export const journalPanel = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'stretch',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
export const dailyCount = style({
|
||||
height: 0,
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
});
|
||||
export const dailyCountHeader = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '0 16px',
|
||||
gap: 8,
|
||||
});
|
||||
export const dailyCountNav = style([
|
||||
interactive,
|
||||
{
|
||||
height: 28,
|
||||
width: 0,
|
||||
flex: 1,
|
||||
fontWeight: 500,
|
||||
fontSize: 14,
|
||||
padding: '4px 8px',
|
||||
whiteSpace: 'nowrap',
|
||||
borderRadius: 8,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: cssVar('textSecondaryColor'),
|
||||
transition: 'all .3s',
|
||||
selectors: {
|
||||
'&[aria-selected="true"]': {
|
||||
backgroundColor: cssVar('backgroundTertiaryColor'),
|
||||
color: cssVar('textPrimaryColor'),
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
export const dailyCountContainer = style({
|
||||
height: 0,
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
width: `calc(var(--item-count) * 100%)`,
|
||||
transition: 'transform .15s ease',
|
||||
transform:
|
||||
'translateX(calc(var(--active-index) * 100% / var(--item-count) * -1))',
|
||||
});
|
||||
export const dailyCountItem = style({
|
||||
width: 'calc(100% / var(--item-count))',
|
||||
height: '100%',
|
||||
});
|
||||
export const dailyCountContent = style({
|
||||
padding: '8px 16px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
});
|
||||
export const dailyCountEmpty = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
maxHeight: 220,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
lineHeight: '24px',
|
||||
fontSize: 15,
|
||||
color: cssVar('textSecondaryColor'),
|
||||
textAlign: 'center',
|
||||
padding: '0 70px',
|
||||
fontWeight: 400,
|
||||
});
|
||||
|
||||
// page item
|
||||
export const pageItem = style([
|
||||
interactive,
|
||||
{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
borderRadius: 4,
|
||||
padding: '0 4px',
|
||||
gap: 8,
|
||||
height: 30,
|
||||
selectors: {
|
||||
'&[aria-selected="true"]': {
|
||||
backgroundColor: cssVar('hoverColor'),
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
export const pageItemIcon = style({
|
||||
width: 20,
|
||||
height: 20,
|
||||
color: cssVar('iconColor'),
|
||||
});
|
||||
export const pageItemLabel = style({
|
||||
width: 0,
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
fontWeight: 500,
|
||||
fontSize: cssVar('fontSm'),
|
||||
color: cssVar('textPrimaryColor'),
|
||||
textAlign: 'left',
|
||||
selectors: {
|
||||
'[aria-selected="true"] &': {
|
||||
// TODO(@catsjuice): wait for design
|
||||
color: cssVar('primaryColor'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// conflict
|
||||
export const journalConflictBlock = style({
|
||||
padding: '0 16px 16px 16px',
|
||||
});
|
||||
export const journalConflictWrapper = style({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(170px, 1fr))',
|
||||
rowGap: 4,
|
||||
columnGap: 8,
|
||||
});
|
||||
export const journalConflictMoreTrigger = style([
|
||||
interactive,
|
||||
{
|
||||
color: cssVar('textSecondaryColor'),
|
||||
height: 30,
|
||||
borderRadius: 4,
|
||||
padding: '0px 8px',
|
||||
fontSize: cssVar('fontSm'),
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
]);
|
||||
|
||||
// customize date-picker cell
|
||||
export const journalDateCell = style([
|
||||
interactive,
|
||||
{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: 8,
|
||||
fontSize: cssVar('fontSm'),
|
||||
color: cssVar('textPrimaryColor'),
|
||||
fontWeight: 400,
|
||||
position: 'relative',
|
||||
|
||||
selectors: {
|
||||
'&[data-is-today="true"]': {
|
||||
fontWeight: 600,
|
||||
color: cssVar('brandColor'),
|
||||
},
|
||||
'&[data-not-current-month="true"]': {
|
||||
color: cssVar('black10'),
|
||||
},
|
||||
'&[data-selected="true"]': {
|
||||
backgroundColor: cssVar('brandColor'),
|
||||
fontWeight: 500,
|
||||
color: cssVar('pureWhite'),
|
||||
},
|
||||
'&[data-is-journal="false"][data-selected="true"]': {
|
||||
backgroundColor: 'transparent',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
fontWeight: 500,
|
||||
border: `1px solid ${cssVar('primaryColor')}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
export const journalDateCellDot = style({
|
||||
width: 4,
|
||||
height: 4,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: cssVar('primaryColor'),
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
});
|
||||
@@ -0,0 +1,382 @@
|
||||
import type { DateCell } from '@affine/component';
|
||||
import { DatePicker, IconButton, Menu, Scrollable } from '@affine/component';
|
||||
import { MoveToTrash } from '@affine/core/components/page-list';
|
||||
import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
|
||||
import {
|
||||
useJournalHelper,
|
||||
useJournalInfoHelper,
|
||||
useJournalRouteHelper,
|
||||
} from '@affine/core/hooks/use-journal';
|
||||
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import {
|
||||
EdgelessIcon,
|
||||
MoreHorizontalIcon,
|
||||
PageIcon,
|
||||
TodayIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import type { DocRecord } from '@toeverything/infra';
|
||||
import {
|
||||
DocService,
|
||||
DocsService,
|
||||
useLiveData,
|
||||
useService,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||
import clsx from 'clsx';
|
||||
import dayjs from 'dayjs';
|
||||
import type { HTMLAttributes, PropsWithChildren, ReactNode } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import * as styles from './journal.css';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
const CountDisplay = ({
|
||||
count,
|
||||
max = 99,
|
||||
...attrs
|
||||
}: { count: number; max?: number } & HTMLAttributes<HTMLSpanElement>) => {
|
||||
return <span {...attrs}>{count > max ? `${max}+` : count}</span>;
|
||||
};
|
||||
interface PageItemProps extends HTMLAttributes<HTMLDivElement> {
|
||||
docRecord: DocRecord;
|
||||
right?: ReactNode;
|
||||
}
|
||||
const PageItem = ({ docRecord, right, className, ...attrs }: PageItemProps) => {
|
||||
const title = useLiveData(docRecord.title$);
|
||||
const mode = useLiveData(docRecord.mode$);
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const { isJournal } = useJournalInfoHelper(
|
||||
workspace.docCollection,
|
||||
docRecord.id
|
||||
);
|
||||
|
||||
const Icon = isJournal
|
||||
? TodayIcon
|
||||
: mode === 'edgeless'
|
||||
? EdgelessIcon
|
||||
: PageIcon;
|
||||
return (
|
||||
<div
|
||||
aria-label={title}
|
||||
className={clsx(className, styles.pageItem)}
|
||||
{...attrs}
|
||||
>
|
||||
<div className={styles.pageItemIcon}>
|
||||
<Icon width={20} height={20} />
|
||||
</div>
|
||||
<span className={styles.pageItemLabel}>{title}</span>
|
||||
{right}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type NavItemName = 'createdToday' | 'updatedToday';
|
||||
interface NavItem {
|
||||
name: NavItemName;
|
||||
label: string;
|
||||
count: number;
|
||||
}
|
||||
interface JournalBlockProps {
|
||||
date: dayjs.Dayjs;
|
||||
}
|
||||
|
||||
export const EditorJournalPanel = () => {
|
||||
const t = useI18n();
|
||||
const doc = useService(DocService).doc;
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const { journalDate, isJournal } = useJournalInfoHelper(
|
||||
workspace.docCollection,
|
||||
doc.id
|
||||
);
|
||||
const { openJournal } = useJournalRouteHelper(workspace.docCollection);
|
||||
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
|
||||
|
||||
useEffect(() => {
|
||||
journalDate && setDate(journalDate.format('YYYY-MM-DD'));
|
||||
}, [journalDate]);
|
||||
|
||||
const onDateSelect = useCallback(
|
||||
(date: string) => {
|
||||
if (journalDate && dayjs(date).isSame(dayjs(journalDate))) return;
|
||||
openJournal(date);
|
||||
},
|
||||
[journalDate, openJournal]
|
||||
);
|
||||
|
||||
const customDayRenderer = useCallback(
|
||||
(cell: DateCell) => {
|
||||
// TODO(@catsjuice): add a dot to indicate journal
|
||||
// has performance issue for now, better to calculate it in advance
|
||||
// const hasJournal = !!getJournalsByDate(cell.date.format('YYYY-MM-DD'))?.length;
|
||||
const hasJournal = false;
|
||||
return (
|
||||
<button
|
||||
className={styles.journalDateCell}
|
||||
data-is-date-cell
|
||||
tabIndex={cell.focused ? 0 : -1}
|
||||
data-is-today={cell.isToday}
|
||||
data-not-current-month={cell.notCurrentMonth}
|
||||
data-selected={cell.selected}
|
||||
data-is-journal={isJournal}
|
||||
data-has-journal={hasJournal}
|
||||
>
|
||||
{cell.label}
|
||||
{hasJournal && !cell.selected ? (
|
||||
<div className={styles.journalDateCellDot} />
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
[isJournal]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.journalPanel} data-is-journal={isJournal}>
|
||||
<div className={styles.calendar}>
|
||||
<DatePicker
|
||||
weekDays={t['com.affine.calendar-date-picker.week-days']()}
|
||||
monthNames={t['com.affine.calendar-date-picker.month-names']()}
|
||||
todayLabel={t['com.affine.calendar-date-picker.today']()}
|
||||
customDayRenderer={customDayRenderer}
|
||||
value={date}
|
||||
onChange={onDateSelect}
|
||||
/>
|
||||
</div>
|
||||
<JournalConflictBlock date={dayjs(date)} />
|
||||
<JournalDailyCountBlock date={dayjs(date)} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const sortPagesByDate = (
|
||||
docs: DocRecord[],
|
||||
field: 'updatedDate' | 'createDate',
|
||||
order: 'asc' | 'desc' = 'desc'
|
||||
) => {
|
||||
return [...docs].sort((a, b) => {
|
||||
return (
|
||||
(order === 'asc' ? 1 : -1) *
|
||||
dayjs(b.meta$.value[field]).diff(dayjs(a.meta$.value[field]))
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const DailyCountEmptyFallback = ({ name }: { name: NavItemName }) => {
|
||||
const t = useI18n();
|
||||
|
||||
return (
|
||||
<div className={styles.dailyCountEmpty}>
|
||||
{name === 'createdToday'
|
||||
? t['com.affine.journal.daily-count-created-empty-tips']()
|
||||
: t['com.affine.journal.daily-count-updated-empty-tips']()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const JournalDailyCountBlock = ({ date }: JournalBlockProps) => {
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const nodeRef = useRef<HTMLDivElement>(null);
|
||||
const t = useI18n();
|
||||
const [activeItem, setActiveItem] = useState<NavItemName>('createdToday');
|
||||
const docRecords = useLiveData(useService(DocsService).list.docs$);
|
||||
|
||||
const navigateHelper = useNavigateHelper();
|
||||
|
||||
const getTodaysPages = useCallback(
|
||||
(field: 'createDate' | 'updatedDate') => {
|
||||
return sortPagesByDate(
|
||||
docRecords.filter(docRecord => {
|
||||
const meta = docRecord.meta$.value;
|
||||
if (meta.trash) return false;
|
||||
return meta[field] && dayjs(meta[field]).isSame(date, 'day');
|
||||
}),
|
||||
field
|
||||
);
|
||||
},
|
||||
[date, docRecords]
|
||||
);
|
||||
|
||||
const createdToday = useMemo(
|
||||
() => getTodaysPages('createDate'),
|
||||
[getTodaysPages]
|
||||
);
|
||||
const updatedToday = useMemo(
|
||||
() => getTodaysPages('updatedDate'),
|
||||
[getTodaysPages]
|
||||
);
|
||||
|
||||
const headerItems = useMemo<NavItem[]>(
|
||||
() => [
|
||||
{
|
||||
name: 'createdToday',
|
||||
label: t['com.affine.journal.created-today'](),
|
||||
count: createdToday.length,
|
||||
},
|
||||
{
|
||||
name: 'updatedToday',
|
||||
label: t['com.affine.journal.updated-today'](),
|
||||
count: updatedToday.length,
|
||||
},
|
||||
],
|
||||
[createdToday.length, t, updatedToday.length]
|
||||
);
|
||||
|
||||
const activeIndex = headerItems.findIndex(({ name }) => name === activeItem);
|
||||
|
||||
const vars = assignInlineVars({
|
||||
'--active-index': String(activeIndex),
|
||||
'--item-count': String(headerItems.length),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.dailyCount} style={vars}>
|
||||
<header className={styles.dailyCountHeader}>
|
||||
{headerItems.map(({ label, count, name }, index) => {
|
||||
return (
|
||||
<button
|
||||
onClick={() => setActiveItem(name)}
|
||||
aria-selected={activeItem === name}
|
||||
className={styles.dailyCountNav}
|
||||
key={index}
|
||||
>
|
||||
{label}
|
||||
|
||||
<CountDisplay count={count} />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</header>
|
||||
|
||||
<main className={styles.dailyCountContainer} data-active={activeItem}>
|
||||
{headerItems.map(({ name }) => {
|
||||
const renderList =
|
||||
name === 'createdToday' ? createdToday : updatedToday;
|
||||
if (renderList.length === 0)
|
||||
return (
|
||||
<div key={name} className={styles.dailyCountItem}>
|
||||
<DailyCountEmptyFallback name={name} />
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<Scrollable.Root key={name} className={styles.dailyCountItem}>
|
||||
<Scrollable.Scrollbar />
|
||||
<Scrollable.Viewport>
|
||||
<div className={styles.dailyCountContent} ref={nodeRef}>
|
||||
{renderList.map((pageRecord, index) => (
|
||||
<PageItem
|
||||
onClick={() =>
|
||||
navigateHelper.openPage(workspace.id, pageRecord.id)
|
||||
}
|
||||
tabIndex={name === activeItem ? 0 : -1}
|
||||
key={index}
|
||||
docRecord={pageRecord}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Scrollable.Viewport>
|
||||
</Scrollable.Root>
|
||||
);
|
||||
})}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MAX_CONFLICT_COUNT = 5;
|
||||
interface ConflictListProps
|
||||
extends PropsWithChildren,
|
||||
HTMLAttributes<HTMLDivElement> {
|
||||
docRecords: DocRecord[];
|
||||
}
|
||||
const ConflictList = ({
|
||||
docRecords,
|
||||
children,
|
||||
className,
|
||||
...attrs
|
||||
}: ConflictListProps) => {
|
||||
const navigateHelper = useNavigateHelper();
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const currentDoc = useService(DocService).doc;
|
||||
const { setTrashModal } = useTrashModalHelper(workspace.docCollection);
|
||||
|
||||
const handleOpenTrashModal = useCallback(
|
||||
(docRecord: DocRecord) => {
|
||||
setTrashModal({
|
||||
open: true,
|
||||
pageIds: [docRecord.id],
|
||||
pageTitles: [docRecord.title$.value],
|
||||
});
|
||||
},
|
||||
[setTrashModal]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={clsx(styles.journalConflictWrapper, className)} {...attrs}>
|
||||
{docRecords.map(docRecord => {
|
||||
const isCurrent = docRecord.id === currentDoc.id;
|
||||
return (
|
||||
<PageItem
|
||||
aria-selected={isCurrent}
|
||||
docRecord={docRecord}
|
||||
key={docRecord.id}
|
||||
right={
|
||||
<Menu
|
||||
items={
|
||||
<MoveToTrash
|
||||
onSelect={() => handleOpenTrashModal(docRecord)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<IconButton type="plain">
|
||||
<MoreHorizontalIcon />
|
||||
</IconButton>
|
||||
</Menu>
|
||||
}
|
||||
onClick={() => navigateHelper.openPage(workspace.id, docRecord.id)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const JournalConflictBlock = ({ date }: JournalBlockProps) => {
|
||||
const t = useI18n();
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const docRecordList = useService(DocsService).list;
|
||||
const journalHelper = useJournalHelper(workspace.docCollection);
|
||||
const docs = journalHelper.getJournalsByDate(date.format('YYYY-MM-DD'));
|
||||
const docRecords = useLiveData(
|
||||
docRecordList.docs$.map(records =>
|
||||
records.filter(v => {
|
||||
return docs.some(doc => doc.id === v.id);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
if (docs.length <= 1) return null;
|
||||
|
||||
return (
|
||||
<ConflictList
|
||||
className={styles.journalConflictBlock}
|
||||
docRecords={docRecords.slice(0, MAX_CONFLICT_COUNT)}
|
||||
>
|
||||
{docs.length > MAX_CONFLICT_COUNT ? (
|
||||
<Menu
|
||||
items={
|
||||
<ConflictList docRecords={docRecords.slice(MAX_CONFLICT_COUNT)} />
|
||||
}
|
||||
>
|
||||
<div className={styles.journalConflictMoreTrigger}>
|
||||
{t['com.affine.journal.conflict-show-more']({
|
||||
count: (docRecords.length - MAX_CONFLICT_COUNT).toFixed(0),
|
||||
})}
|
||||
</div>
|
||||
</Menu>
|
||||
) : null}
|
||||
</ConflictList>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const root = style({
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { AffineEditorContainer } from '@blocksuite/presets';
|
||||
import { OutlinePanel } from '@blocksuite/presets';
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
import * as styles from './outline.css';
|
||||
|
||||
// A wrapper for TOCNotesPanel
|
||||
export const EditorOutline = ({
|
||||
editor,
|
||||
}: {
|
||||
editor: AffineEditorContainer | null;
|
||||
}) => {
|
||||
const outlinePanelRef = useRef<OutlinePanel | null>(null);
|
||||
|
||||
const onRefChange = useCallback((container: HTMLDivElement | null) => {
|
||||
if (container) {
|
||||
assertExists(outlinePanelRef.current, 'toc panel should be initialized');
|
||||
container.append(outlinePanelRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!outlinePanelRef.current) {
|
||||
outlinePanelRef.current = new OutlinePanel();
|
||||
}
|
||||
|
||||
if (editor !== outlinePanelRef.current?.editor) {
|
||||
(outlinePanelRef.current as OutlinePanel).editor = editor;
|
||||
(outlinePanelRef.current as OutlinePanel).fitPadding = [20, 20, 20, 20];
|
||||
}
|
||||
|
||||
return <div className={styles.root} ref={onRefChange} />;
|
||||
};
|
||||
@@ -1,21 +1,16 @@
|
||||
import { RightSidebarService } from '@affine/core/modules/right-sidebar';
|
||||
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 { DocService, useLiveData, useService } from '@toeverything/infra';
|
||||
|
||||
export const useDetailPageHeaderResponsive = () => {
|
||||
export const useDetailPageHeaderResponsive = (availableWidth: number) => {
|
||||
const mode = useLiveData(useService(DocService).doc.mode$);
|
||||
|
||||
const view = useService(ViewService).view;
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
const availableWidth = useLiveData(view.headerContentWidth$);
|
||||
const viewPosition = useViewPosition();
|
||||
const workbenchViewsCount = useLiveData(
|
||||
workbench.views$.map(views => views.length)
|
||||
);
|
||||
const rightSidebar = useService(RightSidebarService).rightSidebar;
|
||||
const rightSidebarOpen = useLiveData(rightSidebar.isOpen$);
|
||||
const rightSidebarOpen = useLiveData(workbench.sidebarOpen$);
|
||||
|
||||
// share button should be hidden once split-view is enabled
|
||||
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 { WorkspaceLayout } from '../../layouts/workspace-layout';
|
||||
import { RightSidebarContainer } from '../../modules/right-sidebar';
|
||||
import { WorkbenchRoot } from '../../modules/workbench';
|
||||
import { AllWorkspaceModals } from '../../providers/modal-provider';
|
||||
import { performanceRenderLogger } from '../../shared';
|
||||
@@ -163,7 +162,6 @@ export const Component = (): ReactElement => {
|
||||
<AffineErrorBoundary height="100vh">
|
||||
<WorkspaceLayout>
|
||||
<WorkbenchRoot />
|
||||
<RightSidebarContainer />
|
||||
</WorkspaceLayout>
|
||||
</AffineErrorBoundary>
|
||||
</FrameworkScope>
|
||||
|
||||
@@ -4,10 +4,7 @@ import {
|
||||
} from '@affine/core/components/page-list';
|
||||
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||
import { TagService } from '@affine/core/modules/tag';
|
||||
import {
|
||||
ViewBodyIsland,
|
||||
ViewHeaderIsland,
|
||||
} from '@affine/core/modules/workbench';
|
||||
import { ViewBody, ViewHeader } from '@affine/core/modules/workbench';
|
||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
@@ -37,10 +34,10 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewHeaderIsland>
|
||||
<ViewHeader>
|
||||
<TagDetailHeader />
|
||||
</ViewHeaderIsland>
|
||||
<ViewBodyIsland>
|
||||
</ViewHeader>
|
||||
<ViewBody>
|
||||
<div className={styles.body}>
|
||||
{filteredPageMetas.length > 0 ? (
|
||||
<VirtualizedPageList
|
||||
@@ -60,7 +57,7 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ViewBodyIsland>
|
||||
</ViewBody>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ import { assertExists } from '@blocksuite/global/utils';
|
||||
import { DeleteIcon } from '@blocksuite/icons/rc';
|
||||
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 * as styles from './trash-page.css';
|
||||
|
||||
@@ -39,10 +39,10 @@ export const TrashPage = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewHeaderIsland>
|
||||
<ViewHeader>
|
||||
<TrashHeader />
|
||||
</ViewHeaderIsland>
|
||||
<ViewBodyIsland>
|
||||
</ViewHeader>
|
||||
<ViewBody>
|
||||
<div className={styles.body}>
|
||||
{filteredPageMetas.length > 0 ? (
|
||||
<VirtualizedTrashList />
|
||||
@@ -53,7 +53,7 @@ export const TrashPage = () => {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ViewBodyIsland>
|
||||
</ViewBody>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user