diff --git a/packages/common/infra/src/modules/doc/entities/record-list.ts b/packages/common/infra/src/modules/doc/entities/record-list.ts index 6c861b1e93..e2db0963bc 100644 --- a/packages/common/infra/src/modules/doc/entities/record-list.ts +++ b/packages/common/infra/src/modules/doc/entities/record-list.ts @@ -29,6 +29,23 @@ export class DocRecordList extends Entity { [] ); + public readonly trashDocs$ = LiveData.from( + this.store.watchTrashDocIds().pipe( + map(ids => + ids.map(id => { + const exists = this.pool.get(id); + if (exists) { + return exists; + } + const record = this.framework.createEntity(DocRecord, { id }); + this.pool.set(id, record); + return record; + }) + ) + ), + [] + ); + public readonly isReady$ = LiveData.from( this.store.watchDocListReady(), false diff --git a/packages/common/infra/src/modules/doc/entities/record.ts b/packages/common/infra/src/modules/doc/entities/record.ts index bf25e6e439..589f02e44a 100644 --- a/packages/common/infra/src/modules/doc/entities/record.ts +++ b/packages/common/infra/src/modules/doc/entities/record.ts @@ -58,5 +58,6 @@ export class DocRecord extends Entity<{ id: string }> { } title$ = this.meta$.map(meta => meta.title ?? ''); + trash$ = this.meta$.map(meta => meta.trash ?? false); } diff --git a/packages/common/infra/src/modules/doc/stores/docs.ts b/packages/common/infra/src/modules/doc/stores/docs.ts index e6262d278d..e5818ee629 100644 --- a/packages/common/infra/src/modules/doc/stores/docs.ts +++ b/packages/common/infra/src/modules/doc/stores/docs.ts @@ -41,7 +41,29 @@ export class DocsStore extends Store { return () => { dispose(); }; - }).pipe(distinctUntilChanged((p, c) => isEqual(p, c))); + }); + } + + watchTrashDocIds() { + return new Observable(subscriber => { + const emit = () => { + subscriber.next( + this.workspaceService.workspace.docCollection.meta.docMetas + .map(v => (v.trash ? v.id : null)) + .filter(Boolean) as string[] + ); + }; + + emit(); + + const dispose = + this.workspaceService.workspace.docCollection.meta.docMetaUpdated.on( + emit + ).dispose; + return () => { + dispose(); + }; + }); } watchDocMeta(id: string) { diff --git a/packages/common/infra/src/sync/job/impl/indexeddb/index.ts b/packages/common/infra/src/sync/job/impl/indexeddb/index.ts index d99ccfd55f..3708bdf311 100644 --- a/packages/common/infra/src/sync/job/impl/indexeddb/index.ts +++ b/packages/common/infra/src/sync/job/impl/indexeddb/index.ts @@ -233,7 +233,6 @@ export class IndexedDBJobQueue implements JobQueue { fromPromise(async () => { const trx = this.database.transaction(['jobs'], 'readonly'); const remaining = await trx.objectStore('jobs').count(); - console.log(remaining); return { remaining }; }) ) diff --git a/packages/frontend/core/src/components/affine/page-properties/table.tsx b/packages/frontend/core/src/components/affine/page-properties/table.tsx index e9b15e8708..59ce753ec8 100644 --- a/packages/frontend/core/src/components/affine/page-properties/table.tsx +++ b/packages/frontend/core/src/components/affine/page-properties/table.tsx @@ -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={
- {backlinks.map(pageId => ( + {backlinks.map(link => ( ))} @@ -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, diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/bi-directional-link-panel.css.ts b/packages/frontend/core/src/components/blocksuite/block-suite-editor/bi-directional-link-panel.css.ts new file mode 100644 index 0000000000..3d58d0d0a6 --- /dev/null +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/bi-directional-link-panel.css.ts @@ -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', +}); diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/bi-directional-link-panel.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-editor/bi-directional-link-panel.tsx new file mode 100644 index 0000000000..b6c4d36d64 --- /dev/null +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/bi-directional-link-panel.tsx @@ -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 ( +
+ {!show && ( +
+
+
+ )} + +
+
Bi-Directional Links
+
+ {show ? 'Hide' : 'Show'} +
+
+ + {show && ( + <> +
+
+
+
+
+ Backlinks · {backlinks.length} +
+ {backlinks.map(link => ( +
+ +
+ ))} +
+
+
+ Outgoing links · {links.length} +
+ {links.map(link => ( +
+ +
+ ))} +
+ + )} +
+ ); +}; diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx index 391788e3ec..7f602b57bc 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx @@ -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< }} >
) : null} - {docPage && !page.readonly ? ( - - ) : null} + {!page.readonly ? : null} {portals} diff --git a/packages/frontend/core/src/components/page-list/page-group.tsx b/packages/frontend/core/src/components/page-list/page-group.tsx index 60901ad493..fb85b6a9ec 100644 --- a/packages/frontend/core/src/components/page-list/page-group.tsx +++ b/packages/frontend/core/src/components/page-list/page-group.tsx @@ -183,7 +183,10 @@ export const ItemGroup = ({ ) : null} ) : null} - +
{items.map(item => ( diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/collections/collections-list.tsx b/packages/frontend/core/src/components/pure/workspace-slider-bar/collections/collections-list.tsx index f3e658b4e4..22d695f635 100644 --- a/packages/frontend/core/src/components/pure/workspace-slider-bar/collections/collections-list.tsx +++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/collections/collections-list.tsx @@ -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 ( ); }) ) : ( -
+
{t['com.affine.collection.emptyCollection']()}
)} diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/collections/doc.tsx b/packages/frontend/core/src/components/pure/workspace-slider-bar/collections/doc.tsx index e96c7b785b..592b285301 100644 --- a/packages/frontend/core/src/components/pure/workspace-slider-bar/collections/doc.tsx +++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/collections/doc.tsx @@ -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; }) => { - 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' ? : ; }, [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 ; }, [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={ @@ -98,20 +117,39 @@ export const Doc = ({ {...attributes} {...listeners} > - {doc.title || t['Untitled']()} +
+ + {title || t['Untitled']()} + + {!collapsed && referencesLoading && ( + +
+ +
+
+ )} +
- {referencesToRender.map(id => { - return ( - - ); - })} + {references ? ( + references.length > 0 ? ( + references.map(({ docId: childDocId }) => { + return ( + + ); + }) + ) : ( +
+ {t['com.affine.rootAppSidebar.docs.no-subdoc']()} +
+ ) + ) : null}
); diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/collections/styles.css.ts b/packages/frontend/core/src/components/pure/workspace-slider-bar/collections/styles.css.ts index 3810a90e7d..6c9e8bdd61 100644 --- a/packages/frontend/core/src/components/pure/workspace-slider-bar/collections/styles.css.ts +++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/collections/styles.css.ts @@ -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', diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/components/operation-menu-button.tsx b/packages/frontend/core/src/components/pure/workspace-slider-bar/components/operation-menu-button.tsx index 75a6e1fbf4..846fba7c9a 100644 --- a/packages/frontend/core/src/components/pure/workspace-slider-bar/components/operation-menu-button.tsx +++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/components/operation-menu-button.tsx @@ -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; diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/components/postfix-item.tsx b/packages/frontend/core/src/components/pure/workspace-slider-bar/components/postfix-item.tsx index d435542b84..594bd32dc1 100644 --- a/packages/frontend/core/src/components/pure/workspace-slider-bar/components/postfix-item.tsx +++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/components/postfix-item.tsx @@ -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) => { diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/components/reference-page.tsx b/packages/frontend/core/src/components/pure/workspace-slider-bar/components/reference-page.tsx index c0b93037a0..cd5ea8f830 100644 --- a/packages/frontend/core/src/components/pure/workspace-slider-bar/components/reference-page.tsx +++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/components/reference-page.tsx @@ -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; parentIds?: Set; } -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' ? : ; - }, [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' ? : ; + }, [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 ( } > - - {pageTitle} - +
+ + {pageTitle} + + {!collapsed && referencesLoading && ( + +
+ +
+
+ )} +
- {collapsible && ( - -
- {referencesToShow.map(ref => { - return ( - - ); - })} -
-
- )} + +
+ {references ? ( + references.length > 0 ? ( + references.map(({ docId }) => { + return ( + + ); + }) + ) : ( +
+ {t['com.affine.rootAppSidebar.docs.no-subdoc']()} +
+ ) + ) : null} +
+
); }; diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/add-favourite-button.tsx b/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/add-favourite-button.tsx index 67cb92666e..4f52084a93 100644 --- a/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/add-favourite-button.tsx +++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/add-favourite-button.tsx @@ -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 => { diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/favorite-list.tsx b/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/favorite-list.tsx index 1e69ed469b..8ba9c4c328 100644 --- a/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/favorite-list.tsx +++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/favorite-list.tsx @@ -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 - ), - [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 ( ); } return null; }, - [collections, docMetaMapping, workspace] + [collections, workspace] ); const t = useI18n(); @@ -107,7 +104,7 @@ const FavoriteListInner = ({ docCollection: workspace }: FavoriteListProps) => { data-over={shouldRenderDragOver} > - + {favourites.map(item => { return {renderFavItem(item)}; diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/favourite-nav-item.tsx b/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/favourite-nav-item.tsx index 6368093c48..eba6c4ad61 100644 --- a/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/favourite-nav-item.tsx +++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/favourite-nav-item.tsx @@ -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' ? : ; }, [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 ; }, [icon, pageTitle]); @@ -108,33 +125,49 @@ export const FavouriteDocSidebarNavItem = ({ active={linkActive} to={`/${pageId}`} linkComponent={WorkbenchLink} - collapsed={collapsible ? collapsed : undefined} + collapsed={collapsed} onCollapsedChange={setCollapsed} postfix={ } > - - {pageTitle} - +
+ + {pageTitle} + + {!collapsed && referencesLoading && ( + +
+ +
+
+ )} +
- {referencesToShow.map(id => { - return ( - - ); - })} + {references ? ( + references.length > 0 ? ( + references.map(({ docId }) => { + return ( + + ); + }) + ) : ( +
+ {t['com.affine.rootAppSidebar.docs.no-subdoc']()} +
+ ) + ) : null}
); diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/styles.css.ts b/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/styles.css.ts index e3ee572602..131ea402fb 100644 --- a/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/styles.css.ts +++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/styles.css.ts @@ -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', +}); diff --git a/packages/frontend/core/src/hooks/use-block-suite-page-backlinks.ts b/packages/frontend/core/src/hooks/use-block-suite-page-backlinks.ts deleted file mode 100644 index 37b6bdd786..0000000000 --- a/packages/frontend/core/src/hooks/use-block-suite-page-backlinks.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { Doc, DocCollection } from '@blocksuite/store'; -import type { Atom } from 'jotai'; -import { atom, useAtomValue } from 'jotai'; - -import { useDocCollectionPage } from './use-block-suite-workspace-page'; - -const weakMap = new WeakMap>(); -function getPageBacklinks(page: Doc): string[] { - return ( - page.collection.indexer.backlink - ?.getBacklink(page.id) - .map(linkNode => linkNode.pageId) - .filter(id => id !== page.id) ?? [] - ); -} - -const getPageBacklinksAtom = (page: Doc | null) => { - if (!page) { - return atom([]); - } - - if (!weakMap.has(page)) { - const baseAtom = atom([]); - baseAtom.onMount = set => { - const disposables = [ - page.slots.ready.on(() => { - set(getPageBacklinks(page)); - }), - page.collection.indexer.backlink?.slots.indexUpdated.on(() => { - set(getPageBacklinks(page)); - }), - ]; - set(getPageBacklinks(page)); - return () => { - disposables.forEach(disposable => disposable?.dispose()); - }; - }; - weakMap.set(page, baseAtom); - } - return weakMap.get(page) as Atom; -}; - -export function useBlockSuitePageBacklinks( - docCollection: DocCollection, - docId: string -): string[] { - const doc = useDocCollectionPage(docCollection, docId); - return useAtomValue(getPageBacklinksAtom(doc)); -} diff --git a/packages/frontend/core/src/hooks/use-block-suite-page-references.ts b/packages/frontend/core/src/hooks/use-block-suite-page-references.ts deleted file mode 100644 index 3b655695bb..0000000000 --- a/packages/frontend/core/src/hooks/use-block-suite-page-references.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { Doc, DocCollection } from '@blocksuite/store'; -import type { Atom } from 'jotai'; -import { atom, useAtomValue } from 'jotai'; - -import { useDocCollectionPage } from './use-block-suite-workspace-page'; - -const weakMap = new WeakMap>(); -function getPageReferences(page: Doc): string[] { - return Object.values( - page.collection.indexer.backlink?.linkIndexMap[page.id] ?? {} - ).flatMap(linkNodes => linkNodes.map(linkNode => linkNode.pageId)); -} - -const getPageReferencesAtom = (page: Doc | null) => { - if (!page) { - return atom([]); - } - - if (!weakMap.has(page)) { - const baseAtom = atom([]); - baseAtom.onMount = set => { - const disposables = [ - page.slots.ready.on(() => { - set(getPageReferences(page)); - }), - page.collection.indexer.backlink?.slots.indexUpdated.on(() => { - set(getPageReferences(page)); - }), - ]; - set(getPageReferences(page)); - return () => { - disposables.forEach(disposable => disposable?.dispose()); - }; - }; - weakMap.set(page, baseAtom); - } - return weakMap.get(page) as Atom; -}; - -export function useBlockSuitePageReferences( - docCollection: DocCollection, - pageId: string -): string[] { - const page = useDocCollectionPage(docCollection, pageId); - return useAtomValue(getPageReferencesAtom(page)); -} diff --git a/packages/frontend/core/src/modules/doc-link/entities/doc-backlinks.ts b/packages/frontend/core/src/modules/doc-link/entities/doc-backlinks.ts new file mode 100644 index 0000000000..e0a64d5794 --- /dev/null +++ b/packages/frontend/core/src/modules/doc-link/entities/doc-backlinks.ts @@ -0,0 +1,24 @@ +import type { DocService } from '@toeverything/infra'; +import { Entity, LiveData } from '@toeverything/infra'; + +import type { DocsSearchService } from '../../docs-search'; + +interface Backlink { + docId: string; + blockId: string; + title: string; +} + +export class DocBacklinks extends Entity { + constructor( + private readonly docsSearchService: DocsSearchService, + private readonly docService: DocService + ) { + super(); + } + + backlinks$ = LiveData.from( + this.docsSearchService.watchRefsTo(this.docService.doc.id), + [] + ); +} diff --git a/packages/frontend/core/src/modules/doc-link/entities/doc-links.ts b/packages/frontend/core/src/modules/doc-link/entities/doc-links.ts new file mode 100644 index 0000000000..52665aefd6 --- /dev/null +++ b/packages/frontend/core/src/modules/doc-link/entities/doc-links.ts @@ -0,0 +1,23 @@ +import type { DocService } from '@toeverything/infra'; +import { Entity, LiveData } from '@toeverything/infra'; + +import type { DocsSearchService } from '../../docs-search'; + +interface Link { + docId: string; + title: string; +} + +export class DocLinks extends Entity { + constructor( + private readonly docsSearchService: DocsSearchService, + private readonly docService: DocService + ) { + super(); + } + + links$ = LiveData.from( + this.docsSearchService.watchRefsFrom(this.docService.doc.id), + [] + ); +} diff --git a/packages/frontend/core/src/modules/doc-link/index.ts b/packages/frontend/core/src/modules/doc-link/index.ts new file mode 100644 index 0000000000..c9b923bc65 --- /dev/null +++ b/packages/frontend/core/src/modules/doc-link/index.ts @@ -0,0 +1,22 @@ +import { + DocScope, + DocService, + type Framework, + WorkspaceScope, +} from '@toeverything/infra'; + +import { DocsSearchService } from '../docs-search'; +import { DocBacklinks } from './entities/doc-backlinks'; +import { DocLinks } from './entities/doc-links'; +import { DocLinksService } from './services/doc-links'; + +export { DocLinksService } from './services/doc-links'; + +export function configureDocLinksModule(framework: Framework) { + framework + .scope(WorkspaceScope) + .scope(DocScope) + .service(DocLinksService) + .entity(DocBacklinks, [DocsSearchService, DocService]) + .entity(DocLinks, [DocsSearchService, DocService]); +} diff --git a/packages/frontend/core/src/modules/doc-link/services/doc-links.ts b/packages/frontend/core/src/modules/doc-link/services/doc-links.ts new file mode 100644 index 0000000000..64049e8746 --- /dev/null +++ b/packages/frontend/core/src/modules/doc-link/services/doc-links.ts @@ -0,0 +1,9 @@ +import { Service } from '@toeverything/infra'; + +import { DocBacklinks } from '../entities/doc-backlinks'; +import { DocLinks } from '../entities/doc-links'; + +export class DocLinksService extends Service { + backlinks = this.framework.createEntity(DocBacklinks); + links = this.framework.createEntity(DocLinks); +} diff --git a/packages/frontend/core/src/modules/docs-search/entities/docs-indexer.ts b/packages/frontend/core/src/modules/docs-search/entities/docs-indexer.ts index 48e4189d6e..58bc596b98 100644 --- a/packages/frontend/core/src/modules/docs-search/entities/docs-indexer.ts +++ b/packages/frontend/core/src/modules/docs-search/entities/docs-indexer.ts @@ -91,6 +91,7 @@ export class DocsIndexer extends Entity { // jobs should have the same docId, so we just pick the first one const docId = jobs[0].payload.docId; + const startTime = performance.now(); logger.debug('Start crawling job for docId:', docId); if (docId) { @@ -100,6 +101,11 @@ export class DocsIndexer extends Entity { await this.crawlingDocData(docId); } } + + const duration = performance.now() - startTime; + logger.debug( + 'Finish crawling job for docId:' + docId + ' in ' + duration + 'ms ' + ); } startCrawling() { diff --git a/packages/frontend/core/src/modules/docs-search/services/docs-search.ts b/packages/frontend/core/src/modules/docs-search/services/docs-search.ts index 2210af5861..8e10fac808 100644 --- a/packages/frontend/core/src/modules/docs-search/services/docs-search.ts +++ b/packages/frontend/core/src/modules/docs-search/services/docs-search.ts @@ -45,7 +45,7 @@ export class DocsSearchService extends Service { }, { type: 'boost', - boost: 100, + boost: 1.5, query: { type: 'match', field: 'flavour', @@ -225,6 +225,195 @@ export class DocsSearchService extends Service { ); } + async searchRefsFrom(docId: string): Promise< + { + docId: string; + title: string; + }[] + > { + const { nodes } = await this.indexer.blockIndex.search( + { + type: 'boolean', + occur: 'must', + queries: [ + { + type: 'match', + field: 'docId', + match: docId, + }, + { + type: 'exists', + field: 'ref', + }, + ], + }, + { + fields: ['ref'], + pagination: { + limit: 100, + }, + } + ); + + const docIds = new Set( + nodes.flatMap(node => { + const refs = node.fields.ref; + return typeof refs === 'string' ? [refs] : refs; + }) + ); + + const docData = await this.indexer.docIndex.getAll(Array.from(docIds)); + + return docData.map(doc => { + const title = doc.get('title'); + return { + docId: doc.id, + title: title ? (typeof title === 'string' ? title : title[0]) : '', + }; + }); + } + + watchRefsFrom(docId: string) { + return this.indexer.blockIndex + .search$( + { + type: 'boolean', + occur: 'must', + queries: [ + { + type: 'match', + field: 'docId', + match: docId, + }, + { + type: 'exists', + field: 'ref', + }, + ], + }, + { + fields: ['ref'], + pagination: { + limit: 100, + }, + } + ) + .pipe( + switchMap(({ nodes }) => { + return fromPromise(async () => { + const docIds = new Set( + nodes.flatMap(node => { + const refs = node.fields.ref; + return typeof refs === 'string' ? [refs] : refs; + }) + ); + + const docData = await this.indexer.docIndex.getAll( + Array.from(docIds) + ); + + return docData.map(doc => { + const title = doc.get('title'); + return { + docId: doc.id, + title: title + ? typeof title === 'string' + ? title + : title[0] + : '', + }; + }); + }); + }) + ); + } + + async searchRefsTo(docId: string): Promise< + { + docId: string; + blockId: string; + title: string; + }[] + > { + const { buckets } = await this.indexer.blockIndex.aggregate( + { + type: 'match', + field: 'ref', + match: docId, + }, + 'docId', + { + hits: { + fields: ['docId', 'blockId'], + pagination: { + limit: 1, + }, + }, + pagination: { + limit: 100, + }, + } + ); + + const docData = await this.indexer.docIndex.getAll( + buckets.map(bucket => bucket.key) + ); + + return buckets.map(bucket => { + const title = + docData.find(doc => doc.id === bucket.key)?.get('title') ?? ''; + const blockId = bucket.hits.nodes[0]?.fields.blockId ?? ''; + return { + docId: bucket.key, + blockId: typeof blockId === 'string' ? blockId : blockId[0], + title: typeof title === 'string' ? title : title[0], + }; + }); + } + + watchRefsTo(docId: string) { + return this.indexer.blockIndex + .aggregate$( + { + type: 'match', + field: 'ref', + match: docId, + }, + 'docId', + { + hits: { + fields: ['docId', 'blockId'], + pagination: { + limit: 1, + }, + }, + pagination: { + limit: 100, + }, + } + ) + .pipe( + switchMap(({ buckets }) => { + return fromPromise(async () => { + const docData = await this.indexer.docIndex.getAll( + buckets.map(bucket => bucket.key) + ); + + return buckets.map(bucket => { + const title = + docData.find(doc => doc.id === bucket.key)?.get('title') ?? ''; + const blockId = bucket.hits.nodes[0]?.fields.blockId ?? ''; + return { + docId: bucket.key, + blockId: typeof blockId === 'string' ? blockId : blockId[0], + title: typeof title === 'string' ? title : title[0], + }; + }); + }); + }) + ); + } + async getDocTitle(docId: string) { const doc = await this.indexer.docIndex.get(docId); const title = doc?.get('title'); diff --git a/packages/frontend/core/src/modules/index.ts b/packages/frontend/core/src/modules/index.ts index e90865b3b9..d4533ab0e2 100644 --- a/packages/frontend/core/src/modules/index.ts +++ b/packages/frontend/core/src/modules/index.ts @@ -3,6 +3,7 @@ import { configureInfraModules, type Framework } from '@toeverything/infra'; import { configureCloudModule } from './cloud'; import { configureCollectionModule } from './collection'; +import { configureDocLinksModule } from './doc-link'; import { configureDocsSearchModule } from './docs-search'; import { configureFindInPageModule } from './find-in-page'; import { configureNavigationModule } from './navigation'; @@ -34,6 +35,7 @@ export function configureCommonModules(framework: Framework) { configurePeekViewModule(framework); configureQuickSearchModule(framework); configureDocsSearchModule(framework); + configureDocLinksModule(framework); } export function configureImpls(framework: Framework) { diff --git a/packages/frontend/core/src/modules/workspace-engine/impls/cloud.ts b/packages/frontend/core/src/modules/workspace-engine/impls/cloud.ts index 819d1be116..e9014867dd 100644 --- a/packages/frontend/core/src/modules/workspace-engine/impls/cloud.ts +++ b/packages/frontend/core/src/modules/workspace-engine/impls/cloud.ts @@ -210,6 +210,8 @@ export class CloudWorkspaceFlavourProviderService const bs = new DocCollection({ id, schema: globalBlockSuiteSchema, + disableBacklinkIndex: true, + disableSearchIndex: true, }); if (localData) applyUpdate(bs.doc, localData); diff --git a/packages/frontend/core/src/modules/workspace-engine/impls/local.ts b/packages/frontend/core/src/modules/workspace-engine/impls/local.ts index fbf2a93aa0..93465920c7 100644 --- a/packages/frontend/core/src/modules/workspace-engine/impls/local.ts +++ b/packages/frontend/core/src/modules/workspace-engine/impls/local.ts @@ -142,6 +142,8 @@ export class LocalWorkspaceFlavourProvider const bs = new DocCollection({ id, schema: globalBlockSuiteSchema, + disableBacklinkIndex: true, + disableSearchIndex: true, }); if (localData) applyUpdate(bs.doc, localData); diff --git a/packages/frontend/electron/src/helper/db/merge-update.ts b/packages/frontend/electron/src/helper/db/merge-update.ts index b17b92001c..6bfc7f4505 100644 --- a/packages/frontend/electron/src/helper/db/merge-update.ts +++ b/packages/frontend/electron/src/helper/db/merge-update.ts @@ -1,6 +1,12 @@ import { applyUpdate, Doc as YDoc, encodeStateAsUpdate, transact } from 'yjs'; export function mergeUpdate(updates: Uint8Array[]) { + if (updates.length === 0) { + return new Uint8Array(); + } + if (updates.length === 1) { + return updates[0]; + } const yDoc = new YDoc(); transact(yDoc, () => { for (const update of updates) { diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 12e98248ff..27579a6239 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1082,8 +1082,10 @@ "com.affine.resetSyncStatus.description": "This operation may fix some synchronization issues.", "com.affine.rootAppSidebar.collections": "Collections", "com.affine.rootAppSidebar.favorites": "Favourites", + "com.affine.rootAppSidebar.docs.no-subdoc": "No linked docs", "com.affine.rootAppSidebar.favorites.empty": "You can add documents to your favourites", "com.affine.rootAppSidebar.others": "Others", + "com.affine.rootAppSidebar.docs.references-loading": "Loading linked docs...", "com.affine.search-tags.placeholder": "Type here ...", "com.affine.selectPage.empty": "Empty", "com.affine.selectPage.empty.tips": "No doc titles contain <1>{{search}}", diff --git a/tests/affine-local/e2e/local-first-favorites-items.spec.ts b/tests/affine-local/e2e/local-first-favorites-items.spec.ts index 6cf10f66a6..717b956e76 100644 --- a/tests/affine-local/e2e/local-first-favorites-items.spec.ts +++ b/tests/affine-local/e2e/local-first-favorites-items.spec.ts @@ -107,6 +107,23 @@ test("Deleted page's reference will not be shown in sidebar", async ({ page.locator('.doc-title-container:has-text("Another page")') ).toBeVisible(); + const anotherPageId = page.url().split('/').reverse()[0]; + + const favItemTestId = 'favourite-page-' + newPageId; + + await expect(page.getByTestId(favItemTestId)).toHaveText( + 'this is a new page to favorite' + ); + + await page + .getByTestId(favItemTestId) + .locator('[data-testid="fav-collapsed-button"]') + .click(); + + const favItemAnotherPageTestId = 'reference-page-' + anotherPageId; + + await expect(page.getByTestId(favItemAnotherPageTestId)).toBeVisible(); + // delete the page await clickPageMoreActions(page); @@ -116,18 +133,7 @@ test("Deleted page's reference will not be shown in sidebar", async ({ // confirm delete await page.locator('button >> text=Delete').click(); - const favItemTestId = 'favourite-page-' + newPageId; - - const favoriteListItemInSidebar = page.getByTestId(favItemTestId); - expect(await favoriteListItemInSidebar.textContent()).toBe( - 'this is a new page to favorite' - ); - - const collapseButton = favoriteListItemInSidebar.locator( - '[data-testid="fav-collapsed-button"]' - ); - - expect(collapseButton).toHaveAttribute('data-disabled', 'true'); + await expect(page.getByTestId(favItemAnotherPageTestId)).toBeVisible(); const currentWorkspace = await workspace.current(); expect(currentWorkspace.meta.flavour).toContain('local');