mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-22 12:36:24 +08:00
refactor(core): optimize abstraction of sidebar doc tree structure (#7455)
This commit is contained in:
+82
-77
@@ -29,7 +29,6 @@ import {
|
|||||||
import type { DocCollection } from '@blocksuite/store';
|
import type { DocCollection } from '@blocksuite/store';
|
||||||
import { type AnimateLayoutChanges, useSortable } from '@dnd-kit/sortable';
|
import { type AnimateLayoutChanges, useSortable } from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
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 { useBlockSuiteDocMeta } from '../../../../hooks/use-block-suite-page-meta';
|
||||||
import { WorkbenchService } from '../../../../modules/workbench';
|
import { WorkbenchService } from '../../../../modules/workbench';
|
||||||
import { WorkbenchLink } from '../../../../modules/workbench/view/workbench-link';
|
import { WorkbenchLink } from '../../../../modules/workbench/view/workbench-link';
|
||||||
import { MenuLinkItem as SidebarMenuLinkItem } from '../../../app-sidebar';
|
|
||||||
import { DragMenuItemOverlay } from '../components/drag-menu-item-overlay';
|
import { DragMenuItemOverlay } from '../components/drag-menu-item-overlay';
|
||||||
import * as draggableMenuItemStyles from '../components/draggable-menu-item.css';
|
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 type { CollectionsListProps } from '../index';
|
||||||
import { Doc } from './doc';
|
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
const animateLayoutChanges: AnimateLayoutChanges = ({
|
const animateLayoutChanges: AnimateLayoutChanges = ({
|
||||||
@@ -60,7 +59,6 @@ export const CollectionSidebarNavItem = ({
|
|||||||
dndId: DNDIdentifier;
|
dndId: DNDIdentifier;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const [collapsed, setCollapsed] = useState(true);
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const collectionService = useService(CollectionService);
|
const collectionService = useService(CollectionService);
|
||||||
const { createPage } = usePageHelper(docCollection);
|
const { createPage } = usePageHelper(docCollection);
|
||||||
@@ -139,79 +137,78 @@ export const CollectionSidebarNavItem = ({
|
|||||||
});
|
});
|
||||||
}, [createAndAddDocument, openConfirmModal, t]);
|
}, [createAndAddDocument, openConfirmModal, t]);
|
||||||
|
|
||||||
return (
|
const postfix = (
|
||||||
<Collapsible.Root
|
<div
|
||||||
open={!collapsed}
|
onClick={stopPropagation}
|
||||||
className={className}
|
onMouseDown={e => {
|
||||||
style={style}
|
// prevent drag
|
||||||
ref={setNodeRef}
|
e.stopPropagation();
|
||||||
{...attributes}
|
}}
|
||||||
|
style={{ display: 'flex', alignItems: 'center' }}
|
||||||
>
|
>
|
||||||
<SidebarMenuLinkItem
|
<IconButton onClick={onConfirmAddDocToCollection} size="small">
|
||||||
{...listeners}
|
<PlusIcon />
|
||||||
data-draggable={true}
|
</IconButton>
|
||||||
data-dragging={isDragging}
|
<CollectionOperations
|
||||||
className={draggableMenuItemStyles.draggableMenuItem}
|
collection={collection}
|
||||||
data-testid="collection-item"
|
openRenameModal={handleOpen}
|
||||||
data-collection-id={collection.id}
|
onAddDocToCollection={onConfirmAddDocToCollection}
|
||||||
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}
|
|
||||||
>
|
>
|
||||||
<span>{collection.name}</span>
|
<IconButton
|
||||||
</SidebarMenuLinkItem>
|
data-testid="collection-options"
|
||||||
<Collapsible.Content className={styles.collapsibleContent}>
|
type="plain"
|
||||||
{!collapsed && (
|
size="small"
|
||||||
<CollectionSidebarNavItemContent
|
style={{ marginLeft: 4 }}
|
||||||
collection={collection}
|
>
|
||||||
docCollection={docCollection}
|
<MoreHorizontalIcon />
|
||||||
dndId={dndId}
|
</IconButton>
|
||||||
/>
|
</CollectionOperations>
|
||||||
)}
|
<RenameModal
|
||||||
</Collapsible.Content>
|
open={open}
|
||||||
</Collapsible.Root>
|
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,
|
collection,
|
||||||
docCollection,
|
docCollection,
|
||||||
dndId,
|
dndId,
|
||||||
@@ -254,12 +251,20 @@ export const CollectionSidebarNavItemContent = ({
|
|||||||
{filtered.length > 0 ? (
|
{filtered.length > 0 ? (
|
||||||
filtered.map(page => {
|
filtered.map(page => {
|
||||||
return (
|
return (
|
||||||
<Doc
|
<SidebarDocItem
|
||||||
docId={page.id}
|
|
||||||
parentId={dndId}
|
|
||||||
inAllowList={allowList.has(page.id)}
|
|
||||||
removeFromAllowList={removeFromAllowList}
|
|
||||||
key={page.id}
|
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',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,2 +1 @@
|
|||||||
export * from './collections-list';
|
export * from './collections-list';
|
||||||
export { Doc } from './doc';
|
|
||||||
|
|||||||
+2
-51
@@ -3,7 +3,7 @@ import { globalStyle, style } from '@vanilla-extract/css';
|
|||||||
export const wrapper = style({
|
export const wrapper = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: '4px',
|
gap: 2,
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
// marginLeft:8,
|
// marginLeft:8,
|
||||||
});
|
});
|
||||||
@@ -23,37 +23,6 @@ export const viewTitle = style({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
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({
|
export const more = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
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({
|
export const emptyCollectionWrapper = style({
|
||||||
padding: '9px 0',
|
padding: '9px 0',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -146,10 +98,9 @@ export const emptyCollectionNewButton = style({
|
|||||||
fontSize: cssVar('fontXs'),
|
fontSize: cssVar('fontXs'),
|
||||||
});
|
});
|
||||||
export const docsListContainer = style({
|
export const docsListContainer = style({
|
||||||
marginLeft: 20,
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: 4,
|
gap: 2,
|
||||||
});
|
});
|
||||||
export const noReferences = style({
|
export const noReferences = style({
|
||||||
fontSize: cssVar('fontSm'),
|
fontSize: cssVar('fontSm'),
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ import { AddFavouriteButton } from '../favorite/add-favourite-button';
|
|||||||
import * as styles from '../favorite/styles.css';
|
import * as styles from '../favorite/styles.css';
|
||||||
import { OperationMenuButton } from './operation-menu-button';
|
import { OperationMenuButton } from './operation-menu-button';
|
||||||
|
|
||||||
type PostfixItemProps = {
|
export type PostfixItemProps = {
|
||||||
pageId: string;
|
pageId: string;
|
||||||
pageTitle: string;
|
pageTitle: string;
|
||||||
inFavorites?: boolean;
|
inFavorites?: boolean;
|
||||||
|
|||||||
-127
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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',
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const collapseContent = style({
|
||||||
|
paddingTop: 2,
|
||||||
|
paddingLeft: 20,
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
});
|
||||||
+1
-1
@@ -84,7 +84,7 @@ const FavoriteListInner = ({ docCollection: workspace }: FavoriteListProps) => {
|
|||||||
} else if (item.type === 'doc') {
|
} else if (item.type === 'doc') {
|
||||||
return (
|
return (
|
||||||
<FavouriteDocSidebarNavItem
|
<FavouriteDocSidebarNavItem
|
||||||
pageId={item.id}
|
docId={item.id}
|
||||||
// memo?
|
// memo?
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
+20
-113
@@ -1,32 +1,16 @@
|
|||||||
import { Loading, Tooltip } from '@affine/component';
|
|
||||||
import {
|
import {
|
||||||
getDNDId,
|
getDNDId,
|
||||||
parseDNDId,
|
parseDNDId,
|
||||||
} from '@affine/core/hooks/affine/use-global-dnd-helper';
|
} 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 { useI18n } from '@affine/i18n';
|
||||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons/rc';
|
import { EdgelessIcon, PageIcon } from '@blocksuite/icons/rc';
|
||||||
import { type AnimateLayoutChanges, useSortable } from '@dnd-kit/sortable';
|
import { type AnimateLayoutChanges, useSortable } from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
import { DocsService, useLiveData, useService } from '@toeverything/infra';
|
||||||
import {
|
import { useMemo } from 'react';
|
||||||
DocsService,
|
|
||||||
LiveData,
|
|
||||||
useLiveData,
|
|
||||||
useServices,
|
|
||||||
} from '@toeverything/infra';
|
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
|
||||||
|
|
||||||
import { MenuLinkItem } from '../../../app-sidebar';
|
|
||||||
import { DragMenuItemOverlay } from '../components/drag-menu-item-overlay';
|
import { DragMenuItemOverlay } from '../components/drag-menu-item-overlay';
|
||||||
import * as draggableMenuItemStyles from '../components/draggable-menu-item.css';
|
import { SidebarDocItem } from '../doc-tree/doc';
|
||||||
import { PostfixItem } from '../components/postfix-item';
|
|
||||||
import type { ReferencePageProps } from '../components/reference-page';
|
|
||||||
import { ReferencePage } from '../components/reference-page';
|
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
const animateLayoutChanges: AnimateLayoutChanges = ({
|
const animateLayoutChanges: AnimateLayoutChanges = ({
|
||||||
@@ -34,44 +18,12 @@ const animateLayoutChanges: AnimateLayoutChanges = ({
|
|||||||
wasDragging,
|
wasDragging,
|
||||||
}) => (isSorting || wasDragging ? false : true);
|
}) => (isSorting || wasDragging ? false : true);
|
||||||
|
|
||||||
export const FavouriteDocSidebarNavItem = ({
|
export const FavouriteDocSidebarNavItem = ({ docId }: { docId: string }) => {
|
||||||
pageId,
|
|
||||||
}: ReferencePageProps & {
|
|
||||||
sortable?: boolean;
|
|
||||||
}) => {
|
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const { docsSearchService, workbenchService, docsService } = useServices({
|
const docsService = useService(DocsService);
|
||||||
DocsSearchService,
|
const docRecord = useLiveData(docsService.list.doc$(docId));
|
||||||
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 docMode = useLiveData(docRecord?.mode$);
|
||||||
const docTitle = useLiveData(docRecord?.title$);
|
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 pageTitle = docTitle || t['Untitled']();
|
||||||
|
|
||||||
const icon = useMemo(() => {
|
const icon = useMemo(() => {
|
||||||
@@ -82,7 +34,7 @@ export const FavouriteDocSidebarNavItem = ({
|
|||||||
return <DragMenuItemOverlay icon={icon} title={pageTitle} />;
|
return <DragMenuItemOverlay icon={icon} title={pageTitle} />;
|
||||||
}, [icon, pageTitle]);
|
}, [icon, pageTitle]);
|
||||||
|
|
||||||
const dragItemId = getDNDId('sidebar-pin', 'doc', pageId);
|
const dragItemId = getDNDId('sidebar-pin', 'doc', docId);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setNodeRef,
|
setNodeRef,
|
||||||
@@ -107,68 +59,23 @@ export const FavouriteDocSidebarNavItem = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapsible.Root
|
<div
|
||||||
className={styles.favItemWrapper}
|
className={styles.favItemWrapper}
|
||||||
open={!collapsed}
|
|
||||||
style={style}
|
style={style}
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
|
data-draggable={true}
|
||||||
|
data-dragging={isDragging}
|
||||||
|
data-testid={`favourite-page-${docId}`}
|
||||||
|
data-favourite-page-item
|
||||||
{...attributes}
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
>
|
>
|
||||||
<MenuLinkItem
|
<SidebarDocItem
|
||||||
{...listeners}
|
docId={docId}
|
||||||
data-testid={`favourite-page-${pageId}`}
|
postfixConfig={{
|
||||||
data-favourite-page-item
|
inFavorites: true,
|
||||||
icon={icon}
|
}}
|
||||||
data-draggable={true}
|
/>
|
||||||
data-dragging={isDragging}
|
</div>
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+4
-10
@@ -22,15 +22,6 @@ export const favItemWrapper = style({
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
selectors: {
|
|
||||||
'&[data-nested="true"]': {
|
|
||||||
marginLeft: '20px',
|
|
||||||
width: 'calc(100% - 20px)',
|
|
||||||
},
|
|
||||||
'&:not(:first-of-type)': {
|
|
||||||
marginTop: '4px',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
export const collapsibleContent = style({
|
export const collapsibleContent = style({
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
@@ -71,10 +62,13 @@ globalStyle(`${dragPageItemOverlay} span`, {
|
|||||||
});
|
});
|
||||||
export const favoriteList = style({
|
export const favoriteList = style({
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
borderRadius: '4px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 2,
|
||||||
selectors: {
|
selectors: {
|
||||||
'&[data-over="true"]': {
|
'&[data-over="true"]': {
|
||||||
background: cssVar('hoverColorFilled'),
|
background: cssVar('hoverColorFilled'),
|
||||||
borderRadius: '4px',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user