mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-19 11:06:25 +08:00
refactor(core): use new backlink indexer (#7296)
This commit is contained in:
@@ -8,7 +8,7 @@ import {
|
||||
Tooltip,
|
||||
} from '@affine/component';
|
||||
import { useCurrentWorkspacePropertiesAdapter } from '@affine/core/hooks/use-affine-adapter';
|
||||
import { useBlockSuitePageBacklinks } from '@affine/core/hooks/use-block-suite-page-backlinks';
|
||||
import { DocLinksService } from '@affine/core/modules/doc-link';
|
||||
import type {
|
||||
PageInfoCustomProperty,
|
||||
PageInfoCustomPropertyMeta,
|
||||
@@ -380,7 +380,7 @@ export const PagePropertiesSettingsPopup = ({
|
||||
};
|
||||
|
||||
type PageBacklinksPopupProps = PropsWithChildren<{
|
||||
backlinks: string[];
|
||||
backlinks: { docId: string; blockId: string; title: string }[];
|
||||
}>;
|
||||
|
||||
export const PageBacklinksPopup = ({
|
||||
@@ -398,11 +398,11 @@ export const PageBacklinksPopup = ({
|
||||
}}
|
||||
items={
|
||||
<div className={styles.backlinksList}>
|
||||
{backlinks.map(pageId => (
|
||||
{backlinks.map(link => (
|
||||
<AffinePageReference
|
||||
key={pageId}
|
||||
key={link.docId + ':' + link.blockId}
|
||||
wrapper={MenuItem}
|
||||
pageId={pageId}
|
||||
pageId={link.docId}
|
||||
docCollection={manager.workspace.docCollection}
|
||||
/>
|
||||
))}
|
||||
@@ -597,10 +597,11 @@ export const PagePropertiesTableHeader = ({
|
||||
const manager = useContext(managerContext);
|
||||
|
||||
const t = useI18n();
|
||||
const backlinks = useBlockSuitePageBacklinks(
|
||||
manager.workspace.docCollection,
|
||||
manager.pageId
|
||||
);
|
||||
const { docLinksServices } = useServices({
|
||||
DocLinksServices: DocLinksService,
|
||||
});
|
||||
const docBacklinks = docLinksServices.backlinks;
|
||||
const backlinks = useLiveData(docBacklinks.backlinks$);
|
||||
|
||||
const { docService, workspaceService } = useServices({
|
||||
DocService,
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const container = style({
|
||||
width: '100%',
|
||||
maxWidth: cssVar('--affine-editor-width'),
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
paddingLeft: cssVar('--affine-editor-side-padding', '24'),
|
||||
paddingRight: cssVar('--affine-editor-side-padding', '24'),
|
||||
fontSize: cssVar('--affine-font-base'),
|
||||
});
|
||||
|
||||
export const dividerContainer = style({
|
||||
height: '16px',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const divider = style({
|
||||
background: cssVar('--affine-border-color'),
|
||||
height: '0.5px',
|
||||
width: '100%',
|
||||
});
|
||||
|
||||
export const titleLine = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const title = style({
|
||||
fontWeight: 500,
|
||||
fontSize: '15px',
|
||||
lineHeight: '24px',
|
||||
color: cssVar('--affine-text-primary-color'),
|
||||
});
|
||||
|
||||
export const showButton = style({
|
||||
width: '56px',
|
||||
height: '28px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid ' + cssVar('--affine-border-color'),
|
||||
backgroundColor: cssVar('--affine-white'),
|
||||
textAlign: 'center',
|
||||
fontSize: '12px',
|
||||
lineHeight: '28px',
|
||||
fontWeight: '500',
|
||||
color: cssVar('--affine-text-primary-color'),
|
||||
cursor: 'pointer',
|
||||
});
|
||||
|
||||
export const linksContainer = style({
|
||||
marginBottom: '16px',
|
||||
});
|
||||
|
||||
export const linksTitles = style({
|
||||
color: cssVar('--affine-text-secondary-color'),
|
||||
height: '32px',
|
||||
lineHeight: '32px',
|
||||
});
|
||||
|
||||
export const link = style({
|
||||
width: '100%',
|
||||
height: '32px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
whiteSpace: 'nowrap',
|
||||
});
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
import { DocLinksService } from '@affine/core/modules/doc-link';
|
||||
import {
|
||||
useLiveData,
|
||||
useServices,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { AffinePageReference } from '../../affine/reference-link';
|
||||
import * as styles from './bi-directional-link-panel.css';
|
||||
|
||||
export const BiDirectionalLinkPanel = () => {
|
||||
const [show, setShow] = useState(false);
|
||||
const { docLinksService, workspaceService } = useServices({
|
||||
DocLinksService,
|
||||
WorkspaceService,
|
||||
});
|
||||
|
||||
const links = useLiveData(docLinksService.links.links$);
|
||||
const backlinks = useLiveData(docLinksService.backlinks.backlinks$);
|
||||
|
||||
const handleClickShow = useCallback(() => {
|
||||
setShow(!show);
|
||||
}, [show]);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{!show && (
|
||||
<div className={styles.dividerContainer}>
|
||||
<div className={styles.divider}></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.titleLine}>
|
||||
<div className={styles.title}>Bi-Directional Links</div>
|
||||
<div className={styles.showButton} onClick={handleClickShow}>
|
||||
{show ? 'Hide' : 'Show'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{show && (
|
||||
<>
|
||||
<div className={styles.dividerContainer}>
|
||||
<div className={styles.divider}></div>
|
||||
</div>
|
||||
<div className={styles.linksContainer}>
|
||||
<div className={styles.linksTitles}>
|
||||
Backlinks · {backlinks.length}
|
||||
</div>
|
||||
{backlinks.map(link => (
|
||||
<div key={link.docId} className={styles.link}>
|
||||
<AffinePageReference
|
||||
key={link.docId}
|
||||
pageId={link.docId}
|
||||
docCollection={workspaceService.workspace.docCollection}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.linksContainer}>
|
||||
<div className={styles.linksTitles}>
|
||||
Outgoing links · {links.length}
|
||||
</div>
|
||||
{links.map(link => (
|
||||
<div key={link.docId} className={styles.link}>
|
||||
<AffinePageReference
|
||||
pageId={link.docId}
|
||||
docCollection={workspaceService.workspace.docCollection}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -7,7 +7,6 @@ import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
|
||||
import { PeekViewService } from '@affine/core/modules/peek-view';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import {
|
||||
BiDirectionalLinkPanel,
|
||||
DocMetaTags,
|
||||
DocTitle,
|
||||
EdgelessEditor,
|
||||
@@ -34,6 +33,7 @@ import React, {
|
||||
|
||||
import { PagePropertiesTable } from '../../affine/page-properties';
|
||||
import { AffinePageReference } from '../../affine/reference-link';
|
||||
import { BiDirectionalLinkPanel } from './bi-directional-link-panel';
|
||||
import { BlocksuiteEditorJournalDocTitle } from './journal-doc-title';
|
||||
import {
|
||||
patchDocModeService,
|
||||
@@ -65,10 +65,6 @@ const adapted = {
|
||||
react: React,
|
||||
elementClass: EdgelessEditor,
|
||||
}),
|
||||
BiDirectionalLinkPanel: createReactComponentFromLit({
|
||||
react: React,
|
||||
elementClass: BiDirectionalLinkPanel,
|
||||
}),
|
||||
};
|
||||
|
||||
interface BlocksuiteEditorProps {
|
||||
@@ -211,9 +207,7 @@ export const BlocksuiteDocEditor = forwardRef<
|
||||
}}
|
||||
></div>
|
||||
) : null}
|
||||
{docPage && !page.readonly ? (
|
||||
<adapted.BiDirectionalLinkPanel doc={page} pageRoot={docPage} />
|
||||
) : null}
|
||||
{!page.readonly ? <BiDirectionalLinkPanel /> : null}
|
||||
</div>
|
||||
{portals}
|
||||
</>
|
||||
|
||||
@@ -183,7 +183,10 @@ export const ItemGroup = <T extends ListItem>({
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<Collapsible.Content className={styles.collapsibleContent}>
|
||||
<Collapsible.Content
|
||||
className={styles.collapsibleContent}
|
||||
data-state={!collapsed ? 'open' : 'closed'}
|
||||
>
|
||||
<div className={styles.collapsibleContentInner}>
|
||||
{items.map(item => (
|
||||
<PageListItemRenderer key={item.id} {...item} />
|
||||
|
||||
+2
-8
@@ -231,10 +231,6 @@ export const CollectionSidebarNavItemContent = ({
|
||||
() => new Set(collection.allowList),
|
||||
[collection.allowList]
|
||||
);
|
||||
const allPagesMeta = useMemo(
|
||||
() => Object.fromEntries(pages.map(v => [v.id, v])),
|
||||
[pages]
|
||||
);
|
||||
const removeFromAllowList = useCallback(
|
||||
(id: string) => {
|
||||
collectionService.deletePageFromCollection(collection.id, id);
|
||||
@@ -259,18 +255,16 @@ export const CollectionSidebarNavItemContent = ({
|
||||
filtered.map(page => {
|
||||
return (
|
||||
<Doc
|
||||
docId={page.id}
|
||||
parentId={dndId}
|
||||
inAllowList={allowList.has(page.id)}
|
||||
removeFromAllowList={removeFromAllowList}
|
||||
allPageMeta={allPagesMeta}
|
||||
doc={page}
|
||||
key={page.id}
|
||||
docCollection={docCollection}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className={styles.emptyCollection}>
|
||||
<div className={styles.noReferences}>
|
||||
{t['com.affine.collection.emptyCollection']()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
+76
-38
@@ -1,15 +1,21 @@
|
||||
import { useBlockSuitePageReferences } from '@affine/core/hooks/use-block-suite-page-references';
|
||||
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 type { DocCollection, DocMeta } from '@blocksuite/store';
|
||||
import { useDraggable } from '@dnd-kit/core';
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import { DocsService, useLiveData, useService } from '@toeverything/infra';
|
||||
import React, { useMemo } from 'react';
|
||||
import {
|
||||
DocsService,
|
||||
LiveData,
|
||||
useLiveData,
|
||||
useService,
|
||||
useServices,
|
||||
} from '@toeverything/infra';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import {
|
||||
type DNDIdentifier,
|
||||
@@ -22,42 +28,56 @@ import { ReferencePage } from '../components/reference-page';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export const Doc = ({
|
||||
doc,
|
||||
docId,
|
||||
parentId,
|
||||
docCollection,
|
||||
allPageMeta,
|
||||
inAllowList,
|
||||
removeFromAllowList,
|
||||
}: {
|
||||
parentId: DNDIdentifier;
|
||||
doc: DocMeta;
|
||||
docId: string;
|
||||
inAllowList: boolean;
|
||||
removeFromAllowList: (id: string) => void;
|
||||
docCollection: DocCollection;
|
||||
allPageMeta: Record<string, DocMeta>;
|
||||
}) => {
|
||||
const [collapsed, setCollapsed] = React.useState(true);
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
const location = useLiveData(workbench.location$);
|
||||
|
||||
const { docsSearchService, workbenchService } = useServices({
|
||||
DocsSearchService,
|
||||
WorkbenchService,
|
||||
DocsService,
|
||||
});
|
||||
const t = useI18n();
|
||||
|
||||
const docId = doc.id;
|
||||
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 dragItemId = getDNDId('collection-list', 'doc', docId, parentId);
|
||||
|
||||
const docTitle = useLiveData(docRecord?.title$);
|
||||
const icon = useMemo(() => {
|
||||
return docMode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />;
|
||||
}, [docMode]);
|
||||
|
||||
const references = useBlockSuitePageReferences(docCollection, docId);
|
||||
const referencesToRender = references.filter(
|
||||
id => allPageMeta[id] && !allPageMeta[id]?.trash
|
||||
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 docTitle = doc.title || t['Untitled']();
|
||||
const dragItemId = getDNDId('collection-list', 'doc', docId, parentId);
|
||||
|
||||
const title = docTitle || t['Untitled']();
|
||||
const docTitleElement = useMemo(() => {
|
||||
return <DragMenuItemOverlay icon={icon} title={docTitle} />;
|
||||
}, [icon, docTitle]);
|
||||
@@ -83,13 +103,12 @@ export const Doc = ({
|
||||
linkComponent={WorkbenchLink}
|
||||
className={styles.title}
|
||||
active={active}
|
||||
collapsed={referencesToRender.length > 0 ? collapsed : undefined}
|
||||
collapsed={collapsed}
|
||||
onCollapsedChange={setCollapsed}
|
||||
postfix={
|
||||
<PostfixItem
|
||||
docCollection={docCollection}
|
||||
pageId={docId}
|
||||
pageTitle={docTitle}
|
||||
pageTitle={title}
|
||||
removeFromAllowList={removeFromAllowList}
|
||||
inAllowList={inAllowList}
|
||||
/>
|
||||
@@ -98,20 +117,39 @@ export const Doc = ({
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
>
|
||||
{doc.title || t['Untitled']()}
|
||||
<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}>
|
||||
{referencesToRender.map(id => {
|
||||
return (
|
||||
<ReferencePage
|
||||
key={id}
|
||||
docCollection={docCollection}
|
||||
pageId={id}
|
||||
metaMapping={allPageMeta}
|
||||
parentIds={new Set([docId])}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{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>
|
||||
);
|
||||
|
||||
+18
-23
@@ -1,5 +1,5 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { globalStyle, keyframes, style } from '@vanilla-extract/css';
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
export const wrapper = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
@@ -82,22 +82,6 @@ export const menuDividerStyle = style({
|
||||
height: '1px',
|
||||
background: cssVar('borderColor'),
|
||||
});
|
||||
const slideDown = keyframes({
|
||||
'0%': {
|
||||
height: '0px',
|
||||
},
|
||||
'100%': {
|
||||
height: 'var(--radix-collapsible-content-height)',
|
||||
},
|
||||
});
|
||||
const slideUp = keyframes({
|
||||
'0%': {
|
||||
height: 'var(--radix-collapsible-content-height)',
|
||||
},
|
||||
'100%': {
|
||||
height: '0px',
|
||||
},
|
||||
});
|
||||
export const collapsibleContent = style({
|
||||
overflow: 'hidden',
|
||||
marginTop: '4px',
|
||||
@@ -105,14 +89,25 @@ export const collapsibleContent = style({
|
||||
'&[data-hidden="true"]': {
|
||||
display: 'none',
|
||||
},
|
||||
'&[data-state="open"]': {
|
||||
animation: `${slideDown} 0.2s ease-in-out`,
|
||||
},
|
||||
'&[data-state="closed"]': {
|
||||
animation: `${slideUp} 0.2s ease-in-out`,
|
||||
},
|
||||
});
|
||||
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',
|
||||
@@ -156,7 +151,7 @@ export const docsListContainer = style({
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
});
|
||||
export const emptyCollection = style({
|
||||
export const noReferences = style({
|
||||
fontSize: cssVar('fontSm'),
|
||||
textAlign: 'left',
|
||||
paddingLeft: '32px',
|
||||
|
||||
+10
-6
@@ -5,8 +5,7 @@ import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { MoreHorizontalIcon } from '@blocksuite/icons/rc';
|
||||
import type { DocCollection } from '@blocksuite/store';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { useService, useServices, WorkspaceService } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useTrashModalHelper } from '../../../../hooks/affine/use-trash-modal-helper';
|
||||
@@ -15,7 +14,6 @@ import { OperationItems } from './operation-item';
|
||||
|
||||
export type OperationMenuButtonProps = {
|
||||
pageId: string;
|
||||
docCollection: DocCollection;
|
||||
pageTitle: string;
|
||||
setRenameModalOpen: () => void;
|
||||
inFavorites?: boolean;
|
||||
@@ -26,7 +24,6 @@ export type OperationMenuButtonProps = {
|
||||
|
||||
export const OperationMenuButton = ({ ...props }: OperationMenuButtonProps) => {
|
||||
const {
|
||||
docCollection,
|
||||
pageId,
|
||||
pageTitle,
|
||||
setRenameModalOpen,
|
||||
@@ -36,8 +33,15 @@ export const OperationMenuButton = ({ ...props }: OperationMenuButtonProps) => {
|
||||
isReferencePage,
|
||||
} = props;
|
||||
const t = useI18n();
|
||||
const { createLinkedPage } = usePageHelper(docCollection);
|
||||
const { setTrashModal } = useTrashModalHelper(docCollection);
|
||||
const { workspaceService } = useServices({
|
||||
WorkspaceService,
|
||||
});
|
||||
const { createLinkedPage } = usePageHelper(
|
||||
workspaceService.workspace.docCollection
|
||||
);
|
||||
const { setTrashModal } = useTrashModalHelper(
|
||||
workspaceService.workspace.docCollection
|
||||
);
|
||||
|
||||
const favAdapter = useService(FavoriteItemsAdapter);
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
|
||||
+8
-4
@@ -2,7 +2,7 @@ import { toast } from '@affine/component';
|
||||
import { RenameModal } from '@affine/component/rename-modal';
|
||||
import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import type { DocCollection } from '@blocksuite/store';
|
||||
import { useServices, WorkspaceService } from '@toeverything/infra';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { AddFavouriteButton } from '../favorite/add-favourite-button';
|
||||
@@ -10,7 +10,6 @@ import * as styles from '../favorite/styles.css';
|
||||
import { OperationMenuButton } from './operation-menu-button';
|
||||
|
||||
type PostfixItemProps = {
|
||||
docCollection: DocCollection;
|
||||
pageId: string;
|
||||
pageTitle: string;
|
||||
inFavorites?: boolean;
|
||||
@@ -20,10 +19,15 @@ type PostfixItemProps = {
|
||||
};
|
||||
|
||||
export const PostfixItem = ({ ...props }: PostfixItemProps) => {
|
||||
const { docCollection, pageId, pageTitle } = props;
|
||||
const { pageId, pageTitle } = props;
|
||||
const t = useI18n();
|
||||
const [open, setOpen] = useState(false);
|
||||
const { setDocTitle } = useDocMetaHelper(docCollection);
|
||||
const { workspaceService } = useServices({
|
||||
WorkspaceService,
|
||||
});
|
||||
const { setDocTitle } = useDocMetaHelper(
|
||||
workspaceService.workspace.docCollection
|
||||
);
|
||||
|
||||
const handleRename = useCallback(
|
||||
(newName: string) => {
|
||||
|
||||
+80
-56
@@ -1,57 +1,67 @@
|
||||
import { useBlockSuitePageReferences } from '@affine/core/hooks/use-block-suite-page-references';
|
||||
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 type { DocCollection, DocMeta } from '@blocksuite/store';
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import { DocsService, useLiveData, useService } from '@toeverything/infra';
|
||||
import { useMemo, useState } from 'react';
|
||||
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 {
|
||||
docCollection: DocCollection;
|
||||
pageId: string;
|
||||
metaMapping: Record<string, DocMeta>;
|
||||
parentIds?: Set<string>;
|
||||
}
|
||||
|
||||
export const ReferencePage = ({
|
||||
docCollection,
|
||||
pageId,
|
||||
metaMapping,
|
||||
parentIds,
|
||||
}: ReferencePageProps) => {
|
||||
export const ReferencePage = ({ pageId, parentIds }: ReferencePageProps) => {
|
||||
const t = useI18n();
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
const { docsSearchService, workbenchService, docsService } = useServices({
|
||||
DocsSearchService,
|
||||
WorkbenchService,
|
||||
DocsService,
|
||||
});
|
||||
const workbench = workbenchService.workbench;
|
||||
const location = useLiveData(workbench.location$);
|
||||
const active = location.pathname === '/' + pageId;
|
||||
|
||||
const pageRecord = useLiveData(useService(DocsService).list.doc$(pageId));
|
||||
const pageMode = useLiveData(pageRecord?.mode$);
|
||||
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 pageMode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />;
|
||||
}, [pageMode]);
|
||||
|
||||
const references = useBlockSuitePageReferences(docCollection, pageId);
|
||||
const referencesToShow = useMemo(() => {
|
||||
return [
|
||||
...new Set(
|
||||
references.filter(ref => metaMapping[ref] && !metaMapping[ref]?.trash)
|
||||
),
|
||||
];
|
||||
}, [references, metaMapping]);
|
||||
|
||||
return docMode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />;
|
||||
}, [docMode]);
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const collapsible = referencesToShow.length > 0;
|
||||
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 = !metaMapping[pageId]?.title;
|
||||
const pageTitle = metaMapping[pageId]?.title || t['Untitled']();
|
||||
const untitled = !docTitle;
|
||||
const pageTitle = docTitle || t['Untitled']();
|
||||
|
||||
return (
|
||||
<Collapsible.Root
|
||||
@@ -62,42 +72,56 @@ export const ReferencePage = ({
|
||||
<MenuLinkItem
|
||||
data-type="reference-page"
|
||||
data-testid={`reference-page-${pageId}`}
|
||||
active={active}
|
||||
active={linkActive}
|
||||
to={`/${pageId}`}
|
||||
icon={icon}
|
||||
collapsed={collapsible ? collapsed : undefined}
|
||||
collapsed={collapsed}
|
||||
onCollapsedChange={setCollapsed}
|
||||
linkComponent={WorkbenchLink}
|
||||
postfix={
|
||||
<PostfixItem
|
||||
docCollection={docCollection}
|
||||
pageId={pageId}
|
||||
pageTitle={pageTitle}
|
||||
isReferencePage={true}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<span className={styles.label} data-untitled={untitled}>
|
||||
{pageTitle}
|
||||
</span>
|
||||
<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 && (
|
||||
<Collapsible.Content className={styles.collapsibleContent}>
|
||||
<div className={styles.collapsibleContentInner}>
|
||||
{referencesToShow.map(ref => {
|
||||
return (
|
||||
<ReferencePage
|
||||
key={ref}
|
||||
docCollection={docCollection}
|
||||
pageId={ref}
|
||||
metaMapping={metaMapping}
|
||||
parentIds={new Set([...(parentIds ?? []), pageId])}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
)}
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
+8
-8
@@ -3,21 +3,21 @@ import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { mixpanel } from '@affine/core/utils';
|
||||
import { PlusIcon } from '@blocksuite/icons/rc';
|
||||
import type { DocCollection } from '@blocksuite/store';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { useService, useServices, WorkspaceService } from '@toeverything/infra';
|
||||
|
||||
import { usePageHelper } from '../../../blocksuite/block-suite-page-list/utils';
|
||||
|
||||
type AddFavouriteButtonProps = {
|
||||
docCollection: DocCollection;
|
||||
pageId?: string;
|
||||
};
|
||||
|
||||
export const AddFavouriteButton = ({
|
||||
docCollection,
|
||||
pageId,
|
||||
}: AddFavouriteButtonProps) => {
|
||||
const { createPage, createLinkedPage } = usePageHelper(docCollection);
|
||||
export const AddFavouriteButton = ({ pageId }: AddFavouriteButtonProps) => {
|
||||
const { workspaceService } = useServices({
|
||||
WorkspaceService,
|
||||
});
|
||||
const { createPage, createLinkedPage } = usePageHelper(
|
||||
workspaceService.workspace.docCollection
|
||||
);
|
||||
const favAdapter = useService(FavoriteItemsAdapter);
|
||||
const handleAddFavorite = useAsyncCallback(
|
||||
async e => {
|
||||
|
||||
+22
-25
@@ -3,18 +3,21 @@ import {
|
||||
getDNDId,
|
||||
resolveDragEndIntent,
|
||||
} from '@affine/core/hooks/affine/use-global-dnd-helper';
|
||||
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import type { WorkspaceFavoriteItem } from '@affine/core/modules/properties/services/schema';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import type { DocMeta } from '@blocksuite/store';
|
||||
import { useDndContext, useDroppable } from '@dnd-kit/core';
|
||||
import {
|
||||
SortableContext,
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import {
|
||||
DocsService,
|
||||
useLiveData,
|
||||
useService,
|
||||
useServices,
|
||||
} from '@toeverything/infra';
|
||||
import { Fragment, useCallback, useMemo } from 'react';
|
||||
|
||||
import { CollectionSidebarNavItem } from '../collections';
|
||||
@@ -25,28 +28,24 @@ import { FavouriteDocSidebarNavItem } from './favourite-nav-item';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
const FavoriteListInner = ({ docCollection: workspace }: FavoriteListProps) => {
|
||||
const metas = useBlockSuiteDocMeta(workspace);
|
||||
const favAdapter = useService(FavoriteItemsAdapter);
|
||||
const collections = useLiveData(useService(CollectionService).collections$);
|
||||
const { favoriteItemsAdapter, docsService, collectionService } = useServices({
|
||||
FavoriteItemsAdapter,
|
||||
DocsService,
|
||||
CollectionService,
|
||||
});
|
||||
const collections = useLiveData(collectionService.collections$);
|
||||
const docs = useLiveData(docsService.list.docs$);
|
||||
const trashDocs = useLiveData(docsService.list.trashDocs$);
|
||||
const dropItemId = getDNDId('sidebar-pin', 'container', workspace.id);
|
||||
|
||||
const docMetaMapping = useMemo(
|
||||
() =>
|
||||
metas.reduce(
|
||||
(acc, meta) => {
|
||||
acc[meta.id] = meta;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, DocMeta>
|
||||
),
|
||||
[metas]
|
||||
);
|
||||
|
||||
const favourites = useLiveData(
|
||||
favAdapter.orderedFavorites$.map(favs => {
|
||||
favoriteItemsAdapter.orderedFavorites$.map(favs => {
|
||||
return favs.filter(fav => {
|
||||
if (fav.type === 'doc') {
|
||||
return !!docMetaMapping[fav.id] && !docMetaMapping[fav.id].trash;
|
||||
return (
|
||||
docs.some(doc => doc.id === fav.id) &&
|
||||
!trashDocs.some(doc => doc.id === fav.id)
|
||||
);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
@@ -82,19 +81,17 @@ const FavoriteListInner = ({ docCollection: workspace }: FavoriteListProps) => {
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else if (item.type === 'doc' && !docMetaMapping[item.id].trash) {
|
||||
} else if (item.type === 'doc') {
|
||||
return (
|
||||
<FavouriteDocSidebarNavItem
|
||||
metaMapping={docMetaMapping}
|
||||
pageId={item.id}
|
||||
// memo?
|
||||
docCollection={workspace}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[collections, docMetaMapping, workspace]
|
||||
[collections, workspace]
|
||||
);
|
||||
|
||||
const t = useI18n();
|
||||
@@ -107,7 +104,7 @@ const FavoriteListInner = ({ docCollection: workspace }: FavoriteListProps) => {
|
||||
data-over={shouldRenderDragOver}
|
||||
>
|
||||
<CategoryDivider label={t['com.affine.rootAppSidebar.favorites']()}>
|
||||
<AddFavouriteButton docCollection={workspace} />
|
||||
<AddFavouriteButton />
|
||||
</CategoryDivider>
|
||||
{favourites.map(item => {
|
||||
return <Fragment key={item.id}>{renderFavItem(item)}</Fragment>;
|
||||
|
||||
+71
-38
@@ -1,8 +1,9 @@
|
||||
import { Loading, Tooltip } from '@affine/component';
|
||||
import {
|
||||
getDNDId,
|
||||
parseDNDId,
|
||||
} from '@affine/core/hooks/affine/use-global-dnd-helper';
|
||||
import { useBlockSuitePageReferences } from '@affine/core/hooks/use-block-suite-page-references';
|
||||
import { DocsSearchService } from '@affine/core/modules/docs-search';
|
||||
import {
|
||||
WorkbenchLink,
|
||||
WorkbenchService,
|
||||
@@ -12,8 +13,13 @@ 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, useLiveData, useService } from '@toeverything/infra';
|
||||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
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';
|
||||
@@ -29,38 +35,49 @@ const animateLayoutChanges: AnimateLayoutChanges = ({
|
||||
}) => (isSorting || wasDragging ? false : true);
|
||||
|
||||
export const FavouriteDocSidebarNavItem = ({
|
||||
docCollection: workspace,
|
||||
pageId,
|
||||
metaMapping,
|
||||
}: ReferencePageProps & {
|
||||
sortable?: boolean;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
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(useService(DocsService).list.doc$(pageId));
|
||||
const docRecord = useLiveData(docsService.list.doc$(pageId));
|
||||
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(() => {
|
||||
return docMode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />;
|
||||
}, [docMode]);
|
||||
|
||||
const references = useBlockSuitePageReferences(workspace, pageId);
|
||||
const referencesToShow = useMemo(() => {
|
||||
return [
|
||||
...new Set(
|
||||
references.filter(ref => metaMapping[ref] && !metaMapping[ref]?.trash)
|
||||
),
|
||||
];
|
||||
}, [references, metaMapping]);
|
||||
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const collapsible = referencesToShow.length > 0;
|
||||
|
||||
const untitled = !metaMapping[pageId]?.title;
|
||||
const pageTitle = metaMapping[pageId]?.title || t['Untitled']();
|
||||
|
||||
const overlayPreview = useMemo(() => {
|
||||
return <DragMenuItemOverlay icon={icon} title={pageTitle} />;
|
||||
}, [icon, pageTitle]);
|
||||
@@ -108,33 +125,49 @@ export const FavouriteDocSidebarNavItem = ({
|
||||
active={linkActive}
|
||||
to={`/${pageId}`}
|
||||
linkComponent={WorkbenchLink}
|
||||
collapsed={collapsible ? collapsed : undefined}
|
||||
collapsed={collapsed}
|
||||
onCollapsedChange={setCollapsed}
|
||||
postfix={
|
||||
<PostfixItem
|
||||
docCollection={workspace}
|
||||
pageId={pageId}
|
||||
pageTitle={pageTitle}
|
||||
inFavorites={true}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<span className={styles.label} data-untitled={untitled}>
|
||||
{pageTitle}
|
||||
</span>
|
||||
<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}>
|
||||
{referencesToShow.map(id => {
|
||||
return (
|
||||
<ReferencePage
|
||||
key={id}
|
||||
docCollection={workspace}
|
||||
pageId={id}
|
||||
metaMapping={metaMapping}
|
||||
parentIds={new Set([pageId])}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{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>
|
||||
);
|
||||
|
||||
+19
-23
@@ -1,5 +1,5 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { globalStyle, keyframes, style } from '@vanilla-extract/css';
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
export const label = style({
|
||||
selectors: {
|
||||
'&[data-untitled="true"]': {
|
||||
@@ -7,6 +7,16 @@ export const label = style({
|
||||
},
|
||||
},
|
||||
});
|
||||
export const labelContainer = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
});
|
||||
export const labelTooltipContainer = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
});
|
||||
export const favItemWrapper = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
@@ -22,22 +32,6 @@ export const favItemWrapper = style({
|
||||
},
|
||||
},
|
||||
});
|
||||
const slideDown = keyframes({
|
||||
'0%': {
|
||||
height: '0px',
|
||||
},
|
||||
'100%': {
|
||||
height: 'var(--radix-collapsible-content-height)',
|
||||
},
|
||||
});
|
||||
const slideUp = keyframes({
|
||||
'0%': {
|
||||
height: 'var(--radix-collapsible-content-height)',
|
||||
},
|
||||
'100%': {
|
||||
height: '0px',
|
||||
},
|
||||
});
|
||||
export const collapsibleContent = style({
|
||||
overflow: 'hidden',
|
||||
marginTop: '4px',
|
||||
@@ -45,12 +39,6 @@ export const collapsibleContent = style({
|
||||
'&[data-hidden="true"]': {
|
||||
display: 'none',
|
||||
},
|
||||
'&[data-state="open"]': {
|
||||
animation: `${slideDown} 0.2s ease-out`,
|
||||
},
|
||||
'&[data-state="closed"]': {
|
||||
animation: `${slideUp} 0.2s ease-out`,
|
||||
},
|
||||
},
|
||||
});
|
||||
export const collapsibleContentInner = style({
|
||||
@@ -131,3 +119,11 @@ export const emptyFavouritesMessage = style({
|
||||
color: cssVar('black30'),
|
||||
userSelect: 'none',
|
||||
});
|
||||
export const noReferences = style({
|
||||
fontSize: cssVar('fontSm'),
|
||||
textAlign: 'left',
|
||||
paddingLeft: '32px',
|
||||
color: cssVar('black30'),
|
||||
lineHeight: '30px',
|
||||
userSelect: 'none',
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user