refactor(core): optimize abstraction of sidebar doc tree structure (#7455)

This commit is contained in:
CatsJuice
2024-07-11 06:24:44 +00:00
parent 7a35b78772
commit c850dbb2b7
13 changed files with 454 additions and 537 deletions

View File

@@ -29,7 +29,6 @@ import {
import type { DocCollection } from '@blocksuite/store';
import { type AnimateLayoutChanges, useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import * as Collapsible from '@radix-ui/react-collapsible';
import { useLiveData, useService } from '@toeverything/infra';
import { useCallback, useMemo, useState } from 'react';
@@ -37,11 +36,11 @@ import { useAllPageListConfig } from '../../../../hooks/affine/use-all-page-list
import { useBlockSuiteDocMeta } from '../../../../hooks/use-block-suite-page-meta';
import { WorkbenchService } from '../../../../modules/workbench';
import { WorkbenchLink } from '../../../../modules/workbench/view/workbench-link';
import { MenuLinkItem as SidebarMenuLinkItem } from '../../../app-sidebar';
import { DragMenuItemOverlay } from '../components/drag-menu-item-overlay';
import * as draggableMenuItemStyles from '../components/draggable-menu-item.css';
import { SidebarDocItem } from '../doc-tree/doc';
import { SidebarDocTreeNode } from '../doc-tree/node';
import type { CollectionsListProps } from '../index';
import { Doc } from './doc';
import * as styles from './styles.css';
const animateLayoutChanges: AnimateLayoutChanges = ({
@@ -60,7 +59,6 @@ export const CollectionSidebarNavItem = ({
dndId: DNDIdentifier;
className?: string;
}) => {
const [collapsed, setCollapsed] = useState(true);
const [open, setOpen] = useState(false);
const collectionService = useService(CollectionService);
const { createPage } = usePageHelper(docCollection);
@@ -139,79 +137,78 @@ export const CollectionSidebarNavItem = ({
});
}, [createAndAddDocument, openConfirmModal, t]);
return (
<Collapsible.Root
open={!collapsed}
className={className}
style={style}
ref={setNodeRef}
{...attributes}
const postfix = (
<div
onClick={stopPropagation}
onMouseDown={e => {
// prevent drag
e.stopPropagation();
}}
style={{ display: 'flex', alignItems: 'center' }}
>
<SidebarMenuLinkItem
{...listeners}
data-draggable={true}
data-dragging={isDragging}
className={draggableMenuItemStyles.draggableMenuItem}
data-testid="collection-item"
data-collection-id={collection.id}
data-type="collection-list-item"
onCollapsedChange={setCollapsed}
active={isOver || currentPath === path}
icon={<AnimatedCollectionsIcon closed={isOver} />}
to={path}
linkComponent={WorkbenchLink}
postfix={
<div
onClick={stopPropagation}
onMouseDown={e => {
// prevent drag
e.stopPropagation();
}}
style={{ display: 'flex', alignItems: 'center' }}
>
<IconButton onClick={onConfirmAddDocToCollection} size="small">
<PlusIcon />
</IconButton>
<CollectionOperations
collection={collection}
openRenameModal={handleOpen}
onAddDocToCollection={onConfirmAddDocToCollection}
>
<IconButton
data-testid="collection-options"
type="plain"
size="small"
style={{ marginLeft: 4 }}
>
<MoreHorizontalIcon />
</IconButton>
</CollectionOperations>
<RenameModal
open={open}
onOpenChange={setOpen}
onRename={onRename}
currentName={collection.name}
/>
</div>
}
collapsed={collapsed}
<IconButton onClick={onConfirmAddDocToCollection} size="small">
<PlusIcon />
</IconButton>
<CollectionOperations
collection={collection}
openRenameModal={handleOpen}
onAddDocToCollection={onConfirmAddDocToCollection}
>
<span>{collection.name}</span>
</SidebarMenuLinkItem>
<Collapsible.Content className={styles.collapsibleContent}>
{!collapsed && (
<CollectionSidebarNavItemContent
collection={collection}
docCollection={docCollection}
dndId={dndId}
/>
)}
</Collapsible.Content>
</Collapsible.Root>
<IconButton
data-testid="collection-options"
type="plain"
size="small"
style={{ marginLeft: 4 }}
>
<MoreHorizontalIcon />
</IconButton>
</CollectionOperations>
<RenameModal
open={open}
onOpenChange={setOpen}
onRename={onRename}
currentName={collection.name}
/>
</div>
);
return (
<SidebarDocTreeNode
ref={setNodeRef}
node={{ type: 'collection', data: collection }}
to={path}
linkComponent={WorkbenchLink}
subTree={
<CollectionSidebarNavItemContent
collection={collection}
docCollection={docCollection}
dndId={dndId}
/>
}
rootProps={{
className,
style,
...attributes,
}}
menuItemProps={{
...listeners,
'data-draggable': true,
'data-dragging': isDragging,
'data-testid': 'collection-item',
'data-collection-id': collection.id,
'data-type': 'collection-list-item',
className: draggableMenuItemStyles.draggableMenuItem,
active: isOver || currentPath === path,
icon: <AnimatedCollectionsIcon closed={isOver} />,
postfix,
}}
>
<span>{collection.name}</span>
</SidebarDocTreeNode>
);
};
export const CollectionSidebarNavItemContent = ({
const CollectionSidebarNavItemContent = ({
collection,
docCollection,
dndId,
@@ -254,12 +251,20 @@ export const CollectionSidebarNavItemContent = ({
{filtered.length > 0 ? (
filtered.map(page => {
return (
<Doc
docId={page.id}
parentId={dndId}
inAllowList={allowList.has(page.id)}
removeFromAllowList={removeFromAllowList}
<SidebarDocItem
key={page.id}
docId={page.id}
postfixConfig={{
inAllowList: allowList.has(page.id),
removeFromAllowList: removeFromAllowList,
}}
dragConfig={{
parentId: dndId,
where: 'collection-list',
}}
menuItemProps={{
'data-testid': 'collection-page',
}}
/>
);
})

View File

@@ -1,156 +0,0 @@
import { Loading, Tooltip } from '@affine/component';
import { DocsSearchService } from '@affine/core/modules/docs-search';
import {
WorkbenchLink,
WorkbenchService,
} from '@affine/core/modules/workbench';
import { useI18n } from '@affine/i18n';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons/rc';
import { useDraggable } from '@dnd-kit/core';
import * as Collapsible from '@radix-ui/react-collapsible';
import {
DocsService,
LiveData,
useLiveData,
useService,
useServices,
} from '@toeverything/infra';
import React, { useEffect, useMemo, useState } from 'react';
import {
type DNDIdentifier,
getDNDId,
} from '../../../../hooks/affine/use-global-dnd-helper';
import { MenuLinkItem } from '../../../app-sidebar';
import { DragMenuItemOverlay } from '../components/drag-menu-item-overlay';
import { PostfixItem } from '../components/postfix-item';
import { ReferencePage } from '../components/reference-page';
import * as styles from './styles.css';
export const Doc = ({
docId,
parentId,
inAllowList,
removeFromAllowList,
}: {
parentId: DNDIdentifier;
docId: string;
inAllowList: boolean;
removeFromAllowList: (id: string) => void;
}) => {
const { docsSearchService, workbenchService } = useServices({
DocsSearchService,
WorkbenchService,
DocsService,
});
const t = useI18n();
const location = useLiveData(workbenchService.workbench.location$);
const active = location.pathname === '/' + docId;
const [collapsed, setCollapsed] = React.useState(true);
const docRecord = useLiveData(useService(DocsService).list.doc$(docId));
const docMode = useLiveData(docRecord?.mode$);
const docTitle = useLiveData(docRecord?.title$);
const icon = useMemo(() => {
return docMode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />;
}, [docMode]);
const references = useLiveData(
useMemo(
() => LiveData.from(docsSearchService.watchRefsFrom(docId), null),
[docsSearchService, docId]
)
);
const indexerLoading = useLiveData(
docsSearchService.indexer.status$.map(
v => v.remaining === undefined || v.remaining > 0
)
);
const [referencesLoading, setReferencesLoading] = useState(true);
useEffect(() => {
setReferencesLoading(
prev =>
prev &&
indexerLoading /* after loading becomes false, it never becomes true */
);
}, [indexerLoading]);
const untitled = !docTitle;
const dragItemId = getDNDId('collection-list', 'doc', docId, parentId);
const title = docTitle || t['Untitled']();
const docTitleElement = useMemo(() => {
return <DragMenuItemOverlay icon={icon} title={docTitle} />;
}, [icon, docTitle]);
const { setNodeRef, attributes, listeners, isDragging } = useDraggable({
id: dragItemId,
data: {
preview: docTitleElement,
},
});
return (
<Collapsible.Root
open={!collapsed}
data-draggable={true}
data-dragging={isDragging}
>
<MenuLinkItem
data-testid="collection-page"
data-type="collection-list-item"
icon={icon}
to={`/${docId}`}
linkComponent={WorkbenchLink}
className={styles.title}
active={active}
collapsed={collapsed}
onCollapsedChange={setCollapsed}
postfix={
<PostfixItem
pageId={docId}
pageTitle={title}
removeFromAllowList={removeFromAllowList}
inAllowList={inAllowList}
/>
}
ref={setNodeRef}
{...attributes}
{...listeners}
>
<div className={styles.labelContainer}>
<span className={styles.label} data-untitled={untitled}>
{title || t['Untitled']()}
</span>
{!collapsed && referencesLoading && (
<Tooltip
content={t['com.affine.rootAppSidebar.docs.references-loading']()}
>
<div className={styles.labelTooltipContainer}>
<Loading />
</div>
</Tooltip>
)}
</div>
</MenuLinkItem>
<Collapsible.Content className={styles.collapsibleContent}>
{references ? (
references.length > 0 ? (
references.map(({ docId: childDocId }) => {
return (
<ReferencePage
key={childDocId}
pageId={childDocId}
parentIds={new Set([docId])}
/>
);
})
) : (
<div className={styles.noReferences}>
{t['com.affine.rootAppSidebar.docs.no-subdoc']()}
</div>
)
) : null}
</Collapsible.Content>
</Collapsible.Root>
);
};

View File

@@ -1,2 +1 @@
export * from './collections-list';
export { Doc } from './doc';

View File

@@ -3,7 +3,7 @@ import { globalStyle, style } from '@vanilla-extract/css';
export const wrapper = style({
display: 'flex',
flexDirection: 'column',
gap: '4px',
gap: 2,
userSelect: 'none',
// marginLeft:8,
});
@@ -23,37 +23,6 @@ export const viewTitle = style({
display: 'flex',
alignItems: 'center',
});
export const title = style({
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
});
globalStyle(`[data-draggable=true] ${title}:before`, {
content: '""',
position: 'absolute',
top: '50%',
transform: 'translateY(-50%)',
left: 0,
width: 4,
height: 4,
transition: 'height 0.2s, opacity 0.2s',
backgroundColor: cssVar('placeholderColor'),
borderRadius: '2px',
opacity: 0,
willChange: 'height, opacity',
});
globalStyle(`[data-draggable=true] ${title}:hover:before`, {
height: 12,
opacity: 1,
});
globalStyle(`[data-draggable=true][data-dragging=true] ${title}`, {
opacity: 0.5,
});
globalStyle(`[data-draggable=true][data-dragging=true] ${title}:before`, {
height: 32,
width: 2,
opacity: 1,
});
export const more = style({
display: 'flex',
alignItems: 'center',
@@ -91,23 +60,6 @@ export const collapsibleContent = style({
},
},
});
export const label = style({
selectors: {
'&[data-untitled="true"]': {
opacity: 0.6,
},
},
});
export const labelContainer = style({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
});
export const labelTooltipContainer = style({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
});
export const emptyCollectionWrapper = style({
padding: '9px 0',
display: 'flex',
@@ -146,10 +98,9 @@ export const emptyCollectionNewButton = style({
fontSize: cssVar('fontXs'),
});
export const docsListContainer = style({
marginLeft: 20,
display: 'flex',
flexDirection: 'column',
gap: 4,
gap: 2,
});
export const noReferences = style({
fontSize: cssVar('fontSm'),

View File

@@ -9,7 +9,7 @@ import { AddFavouriteButton } from '../favorite/add-favourite-button';
import * as styles from '../favorite/styles.css';
import { OperationMenuButton } from './operation-menu-button';
type PostfixItemProps = {
export type PostfixItemProps = {
pageId: string;
pageTitle: string;
inFavorites?: boolean;

View File

@@ -1,127 +0,0 @@
import { Loading, Tooltip } from '@affine/component';
import { DocsSearchService } from '@affine/core/modules/docs-search';
import {
WorkbenchLink,
WorkbenchService,
} from '@affine/core/modules/workbench';
import { useI18n } from '@affine/i18n';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons/rc';
import * as Collapsible from '@radix-ui/react-collapsible';
import {
DocsService,
LiveData,
useLiveData,
useServices,
} from '@toeverything/infra';
import { useEffect, useMemo, useState } from 'react';
import { MenuLinkItem } from '../../../app-sidebar';
import * as styles from '../favorite/styles.css';
import { PostfixItem } from './postfix-item';
export interface ReferencePageProps {
pageId: string;
parentIds?: Set<string>;
}
export const ReferencePage = ({ pageId, parentIds }: ReferencePageProps) => {
const t = useI18n();
const { docsSearchService, workbenchService, docsService } = useServices({
DocsSearchService,
WorkbenchService,
DocsService,
});
const workbench = workbenchService.workbench;
const location = useLiveData(workbench.location$);
const linkActive = location.pathname === '/' + pageId;
const docRecord = useLiveData(docsService.list.doc$(pageId));
const docMode = useLiveData(docRecord?.mode$);
const docTitle = useLiveData(docRecord?.title$);
const icon = useMemo(() => {
return docMode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />;
}, [docMode]);
const [collapsed, setCollapsed] = useState(true);
const references = useLiveData(
useMemo(
() => LiveData.from(docsSearchService.watchRefsFrom(pageId), null),
[docsSearchService, pageId]
)
);
const indexerLoading = useLiveData(
docsSearchService.indexer.status$.map(
v => v.remaining === undefined || v.remaining > 0
)
);
const [referencesLoading, setReferencesLoading] = useState(true);
useEffect(() => {
setReferencesLoading(
prev =>
prev &&
indexerLoading /* after loading becomes false, it never becomes true */
);
}, [indexerLoading]);
const nestedItem = parentIds && parentIds.size > 0;
const untitled = !docTitle;
const pageTitle = docTitle || t['Untitled']();
return (
<Collapsible.Root
className={styles.favItemWrapper}
data-nested={nestedItem}
open={!collapsed}
>
<MenuLinkItem
data-type="reference-page"
data-testid={`reference-page-${pageId}`}
active={linkActive}
to={`/${pageId}`}
icon={icon}
collapsed={collapsed}
onCollapsedChange={setCollapsed}
linkComponent={WorkbenchLink}
postfix={
<PostfixItem
pageId={pageId}
pageTitle={pageTitle}
isReferencePage={true}
/>
}
>
<div className={styles.labelContainer}>
<span className={styles.label} data-untitled={untitled}>
{pageTitle}
</span>
{!collapsed && referencesLoading && (
<Tooltip
content={t['com.affine.rootAppSidebar.docs.references-loading']()}
>
<div className={styles.labelTooltipContainer}>
<Loading />
</div>
</Tooltip>
)}
</div>
</MenuLinkItem>
<Collapsible.Content className={styles.collapsibleContent}>
<div className={styles.collapsibleContentInner}>
{references ? (
references.length > 0 ? (
references.map(({ docId }) => {
return (
<ReferencePage
key={docId}
pageId={docId}
parentIds={new Set([pageId])}
/>
);
})
) : (
<div className={styles.noReferences}>
{t['com.affine.rootAppSidebar.docs.no-subdoc']()}
</div>
)
) : null}
</div>
</Collapsible.Content>
</Collapsible.Root>
);
};

View File

@@ -0,0 +1,61 @@
import { cssVar } from '@toeverything/theme';
import { globalStyle, style } from '@vanilla-extract/css';
export const title = style({
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
});
globalStyle(`[data-draggable=true] ${title}:before`, {
content: '""',
position: 'absolute',
top: '50%',
transform: 'translateY(-50%)',
left: 0,
width: 4,
height: 4,
transition: 'height 0.2s, opacity 0.2s',
backgroundColor: cssVar('placeholderColor'),
borderRadius: '2px',
opacity: 0,
willChange: 'height, opacity',
});
globalStyle(`[data-draggable=true] ${title}:hover:before`, {
height: 12,
opacity: 1,
});
globalStyle(`[data-draggable=true][data-dragging=true] ${title}`, {
opacity: 0.5,
});
globalStyle(`[data-draggable=true][data-dragging=true] ${title}:before`, {
height: 32,
width: 2,
opacity: 1,
});
export const label = style({
selectors: {
'&[data-untitled="true"]': {
opacity: 0.6,
},
},
});
export const labelContainer = style({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
});
export const labelTooltipContainer = style({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
});
export const noReferences = style({
fontSize: cssVar('fontSm'),
textAlign: 'left',
paddingLeft: '32px',
color: cssVar('black30'),
userSelect: 'none',
});

View File

@@ -0,0 +1,174 @@
import { Loading, Tooltip } from '@affine/component';
import type { MenuItemProps } from '@affine/core/components/app-sidebar';
import {
type DNDIdentifier,
type DndWhere,
getDNDId,
} from '@affine/core/hooks/affine/use-global-dnd-helper';
import { DocsSearchService } from '@affine/core/modules/docs-search';
import {
WorkbenchLink,
WorkbenchService,
} from '@affine/core/modules/workbench';
import { useI18n } from '@affine/i18n';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons/rc';
import { useDraggable } from '@dnd-kit/core';
import {
DocsService,
LiveData,
useLiveData,
useServices,
} from '@toeverything/infra';
import { nanoid } from 'nanoid';
import { useEffect, useMemo, useState } from 'react';
import { DragMenuItemOverlay } from '../components/drag-menu-item-overlay';
import { PostfixItem, type PostfixItemProps } from '../components/postfix-item';
import * as styles from './doc.css';
import { SidebarDocTreeNode } from './node';
export type SidebarDocItemProps = {
docId: string;
postfixConfig?: Omit<
PostfixItemProps,
'pageId' | 'pageTitle' | 'isReferencePage'
>;
isReference?: boolean;
dragConfig?: {
parentId?: DNDIdentifier;
where: DndWhere;
};
menuItemProps?: Partial<MenuItemProps> & Record<`data-${string}`, string>;
};
export const SidebarDocItem = function SidebarDocItem({
docId,
postfixConfig,
isReference,
dragConfig,
menuItemProps,
}: SidebarDocItemProps) {
const { docsSearchService, workbenchService, docsService } = useServices({
DocsSearchService,
WorkbenchService,
DocsService,
});
const t = useI18n();
const location = useLiveData(workbenchService.workbench.location$);
const active = location.pathname === '/' + docId;
const docRecord = useLiveData(docsService.list.doc$(docId));
const docMode = useLiveData(docRecord?.mode$);
const docTitle = useLiveData(docRecord?.title$);
const icon = useMemo(() => {
return docMode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />;
}, [docMode]);
const references = useLiveData(
useMemo(
() => LiveData.from(docsSearchService.watchRefsFrom(docId), null),
[docsSearchService, docId]
)
);
const indexerLoading = useLiveData(
docsSearchService.indexer.status$.map(
v => v.remaining === undefined || v.remaining > 0
)
);
const [referencesLoading, setReferencesLoading] = useState(true);
useEffect(() => {
setReferencesLoading(
prev =>
prev &&
indexerLoading /* after loading becomes false, it never becomes true */
);
}, [indexerLoading]);
const untitled = !docTitle;
const title = docTitle || t['Untitled']();
// drag (not available for sub-docs)
const dragItemId = dragConfig
? getDNDId(dragConfig.where, 'doc', docId, dragConfig.parentId)
: nanoid();
const docTitleElement = useMemo(() => {
return <DragMenuItemOverlay icon={icon} title={docTitle} />;
}, [icon, docTitle]);
const { setNodeRef, attributes, listeners, isDragging } = useDraggable({
id: dragItemId,
data: { preview: docTitleElement },
disabled: !dragConfig || isReference,
});
const dragAttrs: Partial<MenuItemProps> = isReference
? {
// prevent dragging parent node
onMouseDown: e => e.stopPropagation(),
}
: { ...attributes, ...listeners };
// workaround to avoid invisible in playwright caused by nested drag
delete dragAttrs['aria-disabled'];
return (
<SidebarDocTreeNode
ref={setNodeRef}
rootProps={{ 'data-dragging': isDragging }}
node={{ type: 'doc', data: docId }}
to={`/${docId}`}
linkComponent={WorkbenchLink}
menuItemProps={{
'data-type': isReference ? 'reference-page' : undefined,
icon,
active,
className: styles.title,
postfix: (
<PostfixItem
pageId={docId}
pageTitle={title}
isReferencePage={isReference}
{...postfixConfig}
/>
),
...dragAttrs,
...menuItemProps,
}}
subTree={
references ? (
references.length > 0 ? (
references.map(({ docId: childDocId }) => {
return (
<SidebarDocItem
key={childDocId}
docId={childDocId}
isReference={true}
menuItemProps={{
'data-testid': `reference-page-${childDocId}`,
}}
/>
);
})
) : (
<div className={styles.noReferences}>
{t['com.affine.rootAppSidebar.docs.no-subdoc']()}
</div>
)
) : null
}
>
<div className={styles.labelContainer}>
<span className={styles.label} data-untitled={untitled}>
{title || t['Untitled']()}
</span>
{referencesLoading && (
<Tooltip
content={t['com.affine.rootAppSidebar.docs.references-loading']()}
>
<div className={styles.labelTooltipContainer}>
<Loading />
</div>
</Tooltip>
)}
</div>
</SidebarDocTreeNode>
);
};

View File

@@ -0,0 +1,6 @@
import { style } from '@vanilla-extract/css';
export const collapseContent = style({
paddingTop: 2,
paddingLeft: 20,
});

View File

@@ -0,0 +1,103 @@
import {
MenuItem,
type MenuItemProps,
MenuLinkItem,
} from '@affine/core/components/app-sidebar';
import type { Collection } from '@affine/env/filter';
import * as Collapsible from '@radix-ui/react-collapsible';
import {
createContext,
forwardRef,
type PropsWithChildren,
type ReactNode,
useContext,
useState,
} from 'react';
import { Link, type To } from 'react-router-dom';
import * as styles from './node.css';
type SidebarDocTreeNode =
| {
type: 'collection';
data: Collection;
}
// | { type: 'tag' }
// | { type: 'folder' }
| {
type: 'doc';
data: string;
};
export type SidebarDocTreeNodeProps = PropsWithChildren<{
node: SidebarDocTreeNode;
subTree?: ReactNode;
to?: To;
linkComponent?: React.ComponentType<{ to: To; className?: string }>;
menuItemProps?: MenuItemProps & Record<`data-${string}`, unknown>;
rootProps?: Collapsible.CollapsibleProps & Record<`data-${string}`, unknown>;
}>;
type SidebarDocTreeNodeContext = {
ancestors: SidebarDocTreeNode[];
};
export const sidebarDocTreeContext =
createContext<SidebarDocTreeNodeContext | null>(null);
/**
* Tree node for the sidebar doc/folder/tag/collection tree.
* This component is used to manage:
* - Collapsing state
* - Ancestors context
* - Link/Menu item rendering
* - Subtree indentation (left/top)
*/
export const SidebarDocTreeNode = forwardRef(function SidebarDocTreeNode(
{
node,
children,
subTree,
to,
linkComponent: LinkComponent = Link,
menuItemProps,
rootProps,
}: SidebarDocTreeNodeProps,
ref: React.Ref<HTMLDivElement>
) {
const [collapsed, setCollapsed] = useState(true);
const { ancestors } = useContext(sidebarDocTreeContext) ?? { ancestors: [] };
const finalMenuItemProps: SidebarDocTreeNodeProps['menuItemProps'] = {
...menuItemProps,
collapsed,
onCollapsedChange: setCollapsed,
};
return (
<sidebarDocTreeContext.Provider value={{ ancestors: [...ancestors, node] }}>
<Collapsible.Root
{...rootProps}
ref={ref}
open={!collapsed}
onOpenChange={setCollapsed}
>
{to ? (
<MenuLinkItem
to={to}
linkComponent={LinkComponent}
{...finalMenuItemProps}
>
{children}
</MenuLinkItem>
) : (
<MenuItem {...finalMenuItemProps}>{children}</MenuItem>
)}
<Collapsible.Content className={styles.collapseContent}>
{collapsed ? null : subTree}
</Collapsible.Content>
</Collapsible.Root>
</sidebarDocTreeContext.Provider>
);
});

View File

@@ -84,7 +84,7 @@ const FavoriteListInner = ({ docCollection: workspace }: FavoriteListProps) => {
} else if (item.type === 'doc') {
return (
<FavouriteDocSidebarNavItem
pageId={item.id}
docId={item.id}
// memo?
/>
);

View File

@@ -1,32 +1,16 @@
import { Loading, Tooltip } from '@affine/component';
import {
getDNDId,
parseDNDId,
} from '@affine/core/hooks/affine/use-global-dnd-helper';
import { DocsSearchService } from '@affine/core/modules/docs-search';
import {
WorkbenchLink,
WorkbenchService,
} from '@affine/core/modules/workbench';
import { useI18n } from '@affine/i18n';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons/rc';
import { type AnimateLayoutChanges, useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import * as Collapsible from '@radix-ui/react-collapsible';
import {
DocsService,
LiveData,
useLiveData,
useServices,
} from '@toeverything/infra';
import { useEffect, useMemo, useState } from 'react';
import { DocsService, useLiveData, useService } from '@toeverything/infra';
import { useMemo } from 'react';
import { MenuLinkItem } from '../../../app-sidebar';
import { DragMenuItemOverlay } from '../components/drag-menu-item-overlay';
import * as draggableMenuItemStyles from '../components/draggable-menu-item.css';
import { PostfixItem } from '../components/postfix-item';
import type { ReferencePageProps } from '../components/reference-page';
import { ReferencePage } from '../components/reference-page';
import { SidebarDocItem } from '../doc-tree/doc';
import * as styles from './styles.css';
const animateLayoutChanges: AnimateLayoutChanges = ({
@@ -34,44 +18,12 @@ const animateLayoutChanges: AnimateLayoutChanges = ({
wasDragging,
}) => (isSorting || wasDragging ? false : true);
export const FavouriteDocSidebarNavItem = ({
pageId,
}: ReferencePageProps & {
sortable?: boolean;
}) => {
export const FavouriteDocSidebarNavItem = ({ docId }: { docId: string }) => {
const t = useI18n();
const { docsSearchService, workbenchService, docsService } = useServices({
DocsSearchService,
WorkbenchService,
DocsService,
});
const workbench = workbenchService.workbench;
const location = useLiveData(workbench.location$);
const linkActive = location.pathname === '/' + pageId;
const docRecord = useLiveData(docsService.list.doc$(pageId));
const docsService = useService(DocsService);
const docRecord = useLiveData(docsService.list.doc$(docId));
const docMode = useLiveData(docRecord?.mode$);
const docTitle = useLiveData(docRecord?.title$);
const references = useLiveData(
useMemo(
() => LiveData.from(docsSearchService.watchRefsFrom(pageId), null),
[docsSearchService, pageId]
)
);
const indexerLoading = useLiveData(
docsSearchService.indexer.status$.map(
v => v.remaining === undefined || v.remaining > 0
)
);
const [referencesLoading, setReferencesLoading] = useState(true);
useEffect(() => {
setReferencesLoading(
prev =>
prev &&
indexerLoading /* after loading becomes false, it never becomes true */
);
}, [indexerLoading]);
const [collapsed, setCollapsed] = useState(true);
const untitled = !docTitle;
const pageTitle = docTitle || t['Untitled']();
const icon = useMemo(() => {
@@ -82,7 +34,7 @@ export const FavouriteDocSidebarNavItem = ({
return <DragMenuItemOverlay icon={icon} title={pageTitle} />;
}, [icon, pageTitle]);
const dragItemId = getDNDId('sidebar-pin', 'doc', pageId);
const dragItemId = getDNDId('sidebar-pin', 'doc', docId);
const {
setNodeRef,
@@ -107,68 +59,23 @@ export const FavouriteDocSidebarNavItem = ({
};
return (
<Collapsible.Root
<div
className={styles.favItemWrapper}
open={!collapsed}
style={style}
ref={setNodeRef}
data-draggable={true}
data-dragging={isDragging}
data-testid={`favourite-page-${docId}`}
data-favourite-page-item
{...attributes}
{...listeners}
>
<MenuLinkItem
{...listeners}
data-testid={`favourite-page-${pageId}`}
data-favourite-page-item
icon={icon}
data-draggable={true}
data-dragging={isDragging}
className={draggableMenuItemStyles.draggableMenuItem}
active={linkActive}
to={`/${pageId}`}
linkComponent={WorkbenchLink}
collapsed={collapsed}
onCollapsedChange={setCollapsed}
postfix={
<PostfixItem
pageId={pageId}
pageTitle={pageTitle}
inFavorites={true}
/>
}
>
<div className={styles.labelContainer}>
<span className={styles.label} data-untitled={untitled}>
{pageTitle}
</span>
{!collapsed && referencesLoading && (
<Tooltip
content={t['com.affine.rootAppSidebar.docs.references-loading']()}
>
<div className={styles.labelTooltipContainer}>
<Loading />
</div>
</Tooltip>
)}
</div>
</MenuLinkItem>
<Collapsible.Content className={styles.collapsibleContent}>
{references ? (
references.length > 0 ? (
references.map(({ docId }) => {
return (
<ReferencePage
key={docId}
pageId={docId}
parentIds={new Set([pageId])}
/>
);
})
) : (
<div className={styles.noReferences}>
{t['com.affine.rootAppSidebar.docs.no-subdoc']()}
</div>
)
) : null}
</Collapsible.Content>
</Collapsible.Root>
<SidebarDocItem
docId={docId}
postfixConfig={{
inFavorites: true,
}}
/>
</div>
);
};

View File

@@ -22,15 +22,6 @@ export const favItemWrapper = style({
flexDirection: 'column',
flexShrink: 0,
userSelect: 'none',
selectors: {
'&[data-nested="true"]': {
marginLeft: '20px',
width: 'calc(100% - 20px)',
},
'&:not(:first-of-type)': {
marginTop: '4px',
},
},
});
export const collapsibleContent = style({
overflow: 'hidden',
@@ -71,10 +62,13 @@ globalStyle(`${dragPageItemOverlay} span`, {
});
export const favoriteList = style({
overflow: 'hidden',
borderRadius: '4px',
display: 'flex',
flexDirection: 'column',
gap: 2,
selectors: {
'&[data-over="true"]': {
background: cssVar('hoverColorFilled'),
borderRadius: '4px',
},
},
});