From 8b669b725bef875851acfe469e691e03b6aa969e Mon Sep 17 00:00:00 2001 From: CatsJuice Date: Mon, 19 May 2025 01:59:38 +0000 Subject: [PATCH] feat(core): new doc list for collection detail (#12278) ## Summary by CodeRabbit - **New Features** - Introduced a new document list view with support for different layouts (list, grid, masonry) and improved multi-selection and batch deletion capabilities. - Added a view toggle control for switching between document list layouts across relevant pages. - Implemented a new collection list header with breadcrumb navigation and streamlined actions for editing collections and creating new pages. - **Improvements** - Simplified and unified document list rendering by delegating logic to a shared component. - Enhanced document selection behavior, allowing toggling of individual items in select mode. - Updated styles for document lists, group headers, and collection pages for a more consistent appearance. - Refined wrapper component styling to prevent unintended HTML attribute forwarding. - **Refactor** - Replaced local implementations of view toggles and document grouping with shared components for easier maintenance. - Removed deprecated and unused styles and props to streamline components and improve code clarity. - Refactored collection detail and header components to adopt new context-driven document explorer architecture. - **Documentation** - Added deprecation notice to an outdated collection page list header component. --- .../component/src/ui/layout/wrapper.tsx | 2 + .../explorer/display-menu/view-toggle.css.ts | 21 ++ .../explorer/display-menu/view-toggle.tsx | 55 +++++ .../explorer/docs-view/doc-list-item.tsx | 7 +- .../explorer/docs-view/docs-list.css.ts | 10 + .../explorer/docs-view/docs-list.tsx | 223 ++++++++++++++++++ .../page-list/docs/page-list-header.tsx | 3 + .../workspace/all-page/all-page-header.css.ts | 4 - .../workspace/all-page/all-page-header.tsx | 55 +---- .../pages/workspace/all-page/all-page.css.ts | 8 - .../pages/workspace/all-page/all-page.tsx | 187 +-------------- .../pages/workspace/collection/header.tsx | 34 +-- .../pages/workspace/collection/index.css.ts | 59 +++++ .../pages/workspace/collection/index.tsx | 89 +++++-- .../workspace/collection/list-header.tsx | 113 +++++++++ 15 files changed, 575 insertions(+), 295 deletions(-) create mode 100644 packages/frontend/core/src/components/explorer/display-menu/view-toggle.css.ts create mode 100644 packages/frontend/core/src/components/explorer/display-menu/view-toggle.tsx create mode 100644 packages/frontend/core/src/components/explorer/docs-view/docs-list.css.ts create mode 100644 packages/frontend/core/src/components/explorer/docs-view/docs-list.tsx create mode 100644 packages/frontend/core/src/desktop/pages/workspace/collection/index.css.ts create mode 100644 packages/frontend/core/src/desktop/pages/workspace/collection/list-header.tsx diff --git a/packages/frontend/component/src/ui/layout/wrapper.tsx b/packages/frontend/component/src/ui/layout/wrapper.tsx index 4d25e79748..7a9ce850af 100644 --- a/packages/frontend/component/src/ui/layout/wrapper.tsx +++ b/packages/frontend/component/src/ui/layout/wrapper.tsx @@ -46,6 +46,8 @@ export const Wrapper = styled('div', { 'marginLeft', 'marginRight', 'marginBottom', + 'flexGrow', + 'flexShrink', ].includes(prop as string); }, })(({ diff --git a/packages/frontend/core/src/components/explorer/display-menu/view-toggle.css.ts b/packages/frontend/core/src/components/explorer/display-menu/view-toggle.css.ts new file mode 100644 index 0000000000..bcee20f77d --- /dev/null +++ b/packages/frontend/core/src/components/explorer/display-menu/view-toggle.css.ts @@ -0,0 +1,21 @@ +import { cssVarV2 } from '@toeverything/theme/v2'; +import { style } from '@vanilla-extract/css'; + +export const viewToggle = style({ + backgroundColor: 'transparent', +}); +export const viewToggleItem = style({ + padding: 0, + fontSize: 16, + width: 24, + color: cssVarV2.icon.primary, + selectors: { + '&[data-state=checked]': { + color: cssVarV2.icon.primary, + }, + }, +}); +export const viewToggleIndicator = style({ + backgroundColor: cssVarV2.layer.background.hoverOverlay, + boxShadow: 'none', +}); diff --git a/packages/frontend/core/src/components/explorer/display-menu/view-toggle.tsx b/packages/frontend/core/src/components/explorer/display-menu/view-toggle.tsx new file mode 100644 index 0000000000..80b4bd4cdb --- /dev/null +++ b/packages/frontend/core/src/components/explorer/display-menu/view-toggle.tsx @@ -0,0 +1,55 @@ +import { RadioGroup, type RadioItem } from '@affine/component'; +import { useLiveData } from '@toeverything/infra'; +import { useCallback, useContext } from 'react'; + +import { DocExplorerContext } from '../context'; +import { + type DocListItemView, + DocListViewIcon, +} from '../docs-view/doc-list-item'; +import * as styles from './view-toggle.css'; + +const views = [ + { + label: , + value: 'masonry', + className: styles.viewToggleItem, + }, + { + label: , + value: 'grid', + className: styles.viewToggleItem, + }, + { + label: , + value: 'list', + className: styles.viewToggleItem, + }, +] satisfies RadioItem[]; + +export const ViewToggle = () => { + const explorerContextValue = useContext(DocExplorerContext); + + const view = useLiveData(explorerContextValue.view$); + + const handleViewChange = useCallback( + (view: DocListItemView) => { + explorerContextValue.view$?.next(view); + }, + [explorerContextValue.view$] + ); + + return ( + + ); +}; diff --git a/packages/frontend/core/src/components/explorer/docs-view/doc-list-item.tsx b/packages/frontend/core/src/components/explorer/docs-view/doc-list-item.tsx index 66df4c852f..8b46c7aaa8 100644 --- a/packages/frontend/core/src/components/explorer/docs-view/doc-list-item.tsx +++ b/packages/frontend/core/src/components/explorer/docs-view/doc-list-item.tsx @@ -124,11 +124,16 @@ export const DocListItem = ({ ...props }: DocListItemProps) => { // do multi select handleMultiSelect(prevCheckAnchorId, currCursor); } else { - contextValue.selectedDocIds$?.next([docId]); + contextValue.selectedDocIds$?.next( + contextValue.selectedDocIds$.value.includes(docId) + ? contextValue.selectedDocIds$.value.filter(id => id !== docId) + : [...contextValue.selectedDocIds$.value, docId] + ); contextValue.prevCheckAnchorId$?.next(currCursor); } } else { if (e.shiftKey) { + contextValue.selectMode$?.next(true); contextValue.selectedDocIds$?.next([docId]); contextValue.prevCheckAnchorId$?.next(currCursor); return; diff --git a/packages/frontend/core/src/components/explorer/docs-view/docs-list.css.ts b/packages/frontend/core/src/components/explorer/docs-view/docs-list.css.ts new file mode 100644 index 0000000000..6ff315fad0 --- /dev/null +++ b/packages/frontend/core/src/components/explorer/docs-view/docs-list.css.ts @@ -0,0 +1,10 @@ +import { cssVarV2 } from '@toeverything/theme/v2'; +import { style } from '@vanilla-extract/css'; + +export const groupHeader = style({ + background: cssVarV2.layer.background.primary, +}); + +export const docItem = style({ + transition: 'width 0.2s ease-in-out', +}); diff --git a/packages/frontend/core/src/components/explorer/docs-view/docs-list.tsx b/packages/frontend/core/src/components/explorer/docs-view/docs-list.tsx new file mode 100644 index 0000000000..7ae7613aff --- /dev/null +++ b/packages/frontend/core/src/components/explorer/docs-view/docs-list.tsx @@ -0,0 +1,223 @@ +import { Masonry, type MasonryGroup, useConfirmModal } from '@affine/component'; +import { DocsService } from '@affine/core/modules/doc'; +import { WorkspacePropertyService } from '@affine/core/modules/workspace-property'; +import { Trans, useI18n } from '@affine/i18n'; +import { useLiveData, useService } from '@toeverything/infra'; +import { cssVarV2 } from '@toeverything/theme/v2'; +import { memo, useCallback, useContext, useEffect, useMemo } from 'react'; + +import { ListFloatingToolbar } from '../../page-list/components/list-floating-toolbar'; +import { WorkspacePropertyTypes } from '../../workspace-property-types'; +import { DocExplorerContext } from '../context'; +import { DocListItem } from './doc-list-item'; +import * as styles from './docs-list.css'; + +const GroupHeader = memo(function GroupHeader({ + groupId, + collapsed, + itemCount, +}: { + groupId: string; + collapsed?: boolean; + itemCount: number; +}) { + const contextValue = useContext(DocExplorerContext); + const propertyService = useService(WorkspacePropertyService); + const allProperties = useLiveData(propertyService.sortedProperties$); + const groupBy = useLiveData(contextValue.groupBy$); + + const groupType = groupBy?.type; + const groupKey = groupBy?.key; + + const header = useMemo(() => { + if (groupType === 'property') { + const property = allProperties.find(p => p.id === groupKey); + if (!property) return null; + + const config = WorkspacePropertyTypes[property.type]; + if (!config?.groupHeader) return null; + return ( + + ); + } else { + console.warn('unsupported group type', groupType); + return null; + } + }, [allProperties, collapsed, groupId, groupKey, groupType, itemCount]); + + if (!groupType) { + return null; + } + + return header; +}); + +const calcCardHeightById = (id: string) => { + if (!id) { + return 250; + } + const max = 5; + const min = 1; + const code = id.charCodeAt(0); + const value = Math.floor((code % (max - min)) + min); + return 250 + value * 10; +}; + +const DocListItemComponent = memo(function DocListItemComponent({ + itemId, + groupId, +}: { + groupId: string; + itemId: string; +}) { + return ; +}); + +export const DocsExplorer = ({ + className, + disableMultiDelete, +}: { + className?: string; + disableMultiDelete?: boolean; +}) => { + const t = useI18n(); + const contextValue = useContext(DocExplorerContext); + const docsService = useService(DocsService); + + const groups = useLiveData(contextValue.groups$); + const view = useLiveData(contextValue.view$); + const selectMode = useLiveData(contextValue.selectMode$); + const selectedDocIds = useLiveData(contextValue.selectedDocIds$); + const collapsedGroups = useLiveData(contextValue.collapsedGroups$); + + const { openConfirmModal } = useConfirmModal(); + + const masonryItems = useMemo(() => { + const items = groups.map((group: any) => { + return { + id: group.key, + Component: groups.length > 1 ? GroupHeader : undefined, + height: groups.length > 1 ? 24 : 0, + className: styles.groupHeader, + items: group.items.map((docId: string) => { + return { + id: docId, + Component: DocListItemComponent, + height: + view === 'list' + ? 42 + : view === 'grid' + ? 280 + : calcCardHeightById(docId), + 'data-view': view, + className: styles.docItem, + }; + }), + } satisfies MasonryGroup; + }); + return items; + }, [groups, view]); + + const handleCloseFloatingToolbar = useCallback(() => { + contextValue.selectMode$?.next(false); + contextValue.selectedDocIds$.next([]); + }, [contextValue]); + + const handleMultiDelete = useCallback(() => { + if (disableMultiDelete) { + handleCloseFloatingToolbar(); + return; + } + if (selectedDocIds.length === 0) { + return; + } + + openConfirmModal({ + title: t['com.affine.moveToTrash.confirmModal.title.multiple']({ + number: selectedDocIds.length.toString(), + }), + description: t[ + 'com.affine.moveToTrash.confirmModal.description.multiple' + ]({ + number: selectedDocIds.length.toString(), + }), + cancelText: t['com.affine.confirmModal.button.cancel'](), + confirmText: t.Delete(), + confirmButtonOptions: { + variant: 'error', + }, + onConfirm: () => { + const selectedDocIds = contextValue.selectedDocIds$.value; + for (const docId of selectedDocIds) { + const doc = docsService.list.doc$(docId).value; + doc?.moveToTrash(); + } + }, + }); + }, [ + contextValue.selectedDocIds$, + disableMultiDelete, + docsService.list, + handleCloseFloatingToolbar, + openConfirmModal, + selectedDocIds.length, + t, + ]); + + useEffect(() => { + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + contextValue.selectMode$?.next(false); + contextValue.selectedDocIds$.next([]); + contextValue.prevCheckAnchorId$?.next(null); + } + }; + document.addEventListener('keydown', onKeyDown); + return () => { + document.removeEventListener('keydown', onKeyDown); + }; + }, [contextValue]); + + return ( + <> + (w > 500 ? 24 : w > 393 ? 20 : 16), + [] + )} + /> + +
+ {{ count: selectedDocIds.length } as any} +
+ selected + + } + /> + + ); +}; diff --git a/packages/frontend/core/src/components/page-list/docs/page-list-header.tsx b/packages/frontend/core/src/components/page-list/docs/page-list-header.tsx index a7dbf3e069..094dffd3e3 100644 --- a/packages/frontend/core/src/components/page-list/docs/page-list-header.tsx +++ b/packages/frontend/core/src/components/page-list/docs/page-list-header.tsx @@ -105,6 +105,9 @@ export const PageListHeader = () => { ); }; +/** + * @deprecated + */ export const CollectionPageListHeader = ({ collection, workspaceId, diff --git a/packages/frontend/core/src/desktop/pages/workspace/all-page/all-page-header.css.ts b/packages/frontend/core/src/desktop/pages/workspace/all-page/all-page-header.css.ts index 898a89d519..8a946fb511 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/all-page/all-page-header.css.ts +++ b/packages/frontend/core/src/desktop/pages/workspace/all-page/all-page-header.css.ts @@ -29,7 +29,3 @@ export const viewToggleItem = style({ }, }, }); -export const viewToggleIndicator = style({ - backgroundColor: cssVarV2.layer.background.hoverOverlay, - boxShadow: 'none', -}); diff --git a/packages/frontend/core/src/desktop/pages/workspace/all-page/all-page-header.tsx b/packages/frontend/core/src/desktop/pages/workspace/all-page/all-page-header.tsx index 6ccde0f9d6..a300a4aff3 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/all-page/all-page-header.tsx +++ b/packages/frontend/core/src/desktop/pages/workspace/all-page/all-page-header.tsx @@ -1,61 +1,10 @@ -import { type MenuProps, RadioGroup, type RadioItem } from '@affine/component'; -import { DocExplorerContext } from '@affine/core/components/explorer/context'; +import { type MenuProps } from '@affine/component'; import { ExplorerDisplayMenuButton } from '@affine/core/components/explorer/display-menu'; -import { - type DocListItemView, - DocListViewIcon, -} from '@affine/core/components/explorer/docs-view/doc-list-item'; +import { ViewToggle } from '@affine/core/components/explorer/display-menu/view-toggle'; import { ExplorerNavigation } from '@affine/core/components/explorer/header/navigation'; -import { useLiveData } from '@toeverything/infra'; -import { useCallback, useContext } from 'react'; import * as styles from './all-page-header.css'; -const views = [ - { - label: , - value: 'masonry', - className: styles.viewToggleItem, - }, - { - label: , - value: 'grid', - className: styles.viewToggleItem, - }, - { - label: , - value: 'list', - className: styles.viewToggleItem, - }, -] satisfies RadioItem[]; - -const ViewToggle = () => { - const explorerContextValue = useContext(DocExplorerContext); - - const view = useLiveData(explorerContextValue.view$); - - const handleViewChange = useCallback( - (view: DocListItemView) => { - explorerContextValue.view$?.next(view); - }, - [explorerContextValue.view$] - ); - - return ( - - ); -}; - const menuProps: Partial = { contentOptions: { side: 'bottom', diff --git a/packages/frontend/core/src/desktop/pages/workspace/all-page/all-page.css.ts b/packages/frontend/core/src/desktop/pages/workspace/all-page/all-page.css.ts index d3867f4153..c15adfe572 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/all-page/all-page.css.ts +++ b/packages/frontend/core/src/desktop/pages/workspace/all-page/all-page.css.ts @@ -1,4 +1,3 @@ -import { cssVarV2 } from '@toeverything/theme/v2'; import { style } from '@vanilla-extract/css'; export const scrollContainer = style({ flex: 1, @@ -38,13 +37,6 @@ export const scrollArea = style({ }); // group -export const groupHeader = style({ - background: cssVarV2.layer.background.primary, -}); - -export const docItem = style({ - transition: 'width 0.2s ease-in-out', -}); export const pinnedCollection = style({ display: 'flex', diff --git a/packages/frontend/core/src/desktop/pages/workspace/all-page/all-page.tsx b/packages/frontend/core/src/desktop/pages/workspace/all-page/all-page.tsx index c94440a067..419d5e6641 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/all-page/all-page.tsx +++ b/packages/frontend/core/src/desktop/pages/workspace/all-page/all-page.tsx @@ -1,38 +1,20 @@ -import { - Button, - Masonry, - type MasonryGroup, - useConfirmModal, - usePromptModal, -} from '@affine/component'; +import { Button, usePromptModal } from '@affine/component'; import { createDocExplorerContext, DocExplorerContext, } from '@affine/core/components/explorer/context'; -import { DocListItem } from '@affine/core/components/explorer/docs-view/doc-list-item'; +import { DocsExplorer } from '@affine/core/components/explorer/docs-view/docs-list'; import { Filters } from '@affine/core/components/filter'; -import { ListFloatingToolbar } from '@affine/core/components/page-list/components/list-floating-toolbar'; -import { WorkspacePropertyTypes } from '@affine/core/components/workspace-property-types'; import { CollectionService, PinnedCollectionService, } from '@affine/core/modules/collection'; import { CollectionRulesService } from '@affine/core/modules/collection-rules'; import type { FilterParams } from '@affine/core/modules/collection-rules/types'; -import { DocsService } from '@affine/core/modules/doc'; import { FeatureFlagService } from '@affine/core/modules/feature-flag'; -import { WorkspacePropertyService } from '@affine/core/modules/workspace-property'; -import { Trans, useI18n } from '@affine/i18n'; +import { useI18n } from '@affine/i18n'; import { useLiveData, useService } from '@toeverything/infra'; -import { cssVarV2 } from '@toeverything/theme/v2'; -import { - memo, - useCallback, - useContext, - useEffect, - useMemo, - useState, -} from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { ViewBody, @@ -47,70 +29,9 @@ import { AllDocsHeader } from './all-page-header'; import { MigrationAllDocsDataNotification } from './migration-data'; import { PinnedCollections } from './pinned-collections'; -const GroupHeader = memo(function GroupHeader({ - groupId, - collapsed, - itemCount, -}: { - groupId: string; - collapsed?: boolean; - itemCount: number; -}) { - const contextValue = useContext(DocExplorerContext); - const propertyService = useService(WorkspacePropertyService); - const allProperties = useLiveData(propertyService.sortedProperties$); - const groupBy = useLiveData(contextValue.groupBy$); - - const groupType = groupBy?.type; - const groupKey = groupBy?.key; - - const header = useMemo(() => { - if (groupType === 'property') { - const property = allProperties.find(p => p.id === groupKey); - if (!property) return null; - - const config = WorkspacePropertyTypes[property.type]; - if (!config?.groupHeader) return null; - return ( - - ); - } else { - return '// TODO: ' + groupType; - } - }, [allProperties, collapsed, groupId, groupKey, groupType, itemCount]); - - if (!groupType) { - return null; - } - - return header; -}); - -const calcCardHeightById = (id: string) => { - const max = 5; - const min = 1; - const code = id.charCodeAt(0); - const value = Math.floor((code % (max - min)) + min); - return 250 + value * 10; -}; - -const DocListItemComponent = memo(function DocListItemComponent({ - itemId, - groupId, -}: { - groupId: string; - itemId: string; -}) { - return ; -}); - export const AllPage = () => { const t = useI18n(); - const docsService = useService(DocsService); + const collectionService = useService(CollectionService); const pinnedCollectionService = useService(PinnedCollectionService); @@ -138,41 +59,10 @@ export const AllPage = () => { const [explorerContextValue] = useState(createDocExplorerContext); - const view = useLiveData(explorerContextValue.view$); const groupBy = useLiveData(explorerContextValue.groupBy$); const orderBy = useLiveData(explorerContextValue.orderBy$); - const groups = useLiveData(explorerContextValue.groups$); - const selectedDocIds = useLiveData(explorerContextValue.selectedDocIds$); - const collapsedGroups = useLiveData(explorerContextValue.collapsedGroups$); - const selectMode = useLiveData(explorerContextValue.selectMode$); const { openPromptModal } = usePromptModal(); - const { openConfirmModal } = useConfirmModal(); - const masonryItems = useMemo(() => { - const items = groups.map((group: any) => { - return { - id: group.key, - Component: groups.length > 1 ? GroupHeader : undefined, - height: groups.length > 1 ? 24 : 0, - className: styles.groupHeader, - items: group.items.map((docId: string) => { - return { - id: docId, - Component: DocListItemComponent, - height: - view === 'list' - ? 42 - : view === 'grid' - ? 280 - : calcCardHeightById(docId), - 'data-view': view, - className: styles.docItem, - }; - }), - } satisfies MasonryGroup; - }); - return items; - }, [groups, view]); const collectionRulesService = useService(CollectionRulesService); useEffect(() => { @@ -270,39 +160,6 @@ export const AllPage = () => { setTempFilters(filters); }, []); - const handleCloseFloatingToolbar = useCallback(() => { - explorerContextValue.selectMode$.next(false); - explorerContextValue.selectedDocIds$.next([]); - }, [explorerContextValue]); - - const handleMultiDelete = useCallback(() => { - if (selectedDocIds.length === 0) { - return; - } - - openConfirmModal({ - title: t['com.affine.moveToTrash.confirmModal.title.multiple']({ - number: selectedDocIds.length.toString(), - }), - description: t[ - 'com.affine.moveToTrash.confirmModal.description.multiple' - ]({ - number: selectedDocIds.length.toString(), - }), - cancelText: t['com.affine.confirmModal.button.cancel'](), - confirmText: t.Delete(), - confirmButtonOptions: { - variant: 'error', - }, - onConfirm: () => { - for (const docId of selectedDocIds) { - const doc = docsService.list.doc$(docId).value; - doc?.moveToTrash(); - } - }, - }); - }, [docsService.list, openConfirmModal, selectedDocIds, t]); - const handleSaveFilters = useCallback(() => { openPromptModal({ title: t['com.affine.editCollection.saveCollection'](), @@ -383,41 +240,9 @@ export const AllPage = () => { )}
- (w > 500 ? 24 : w > 393 ? 20 : 16), - [] - )} - /> +
- -
- {{ count: selectedDocIds.length } as any} -
- selected - - } - /> diff --git a/packages/frontend/core/src/desktop/pages/workspace/collection/header.tsx b/packages/frontend/core/src/desktop/pages/workspace/collection/header.tsx index 8f006e5177..75efa3fbe0 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/collection/header.tsx +++ b/packages/frontend/core/src/desktop/pages/workspace/collection/header.tsx @@ -1,35 +1,17 @@ -import { IconButton } from '@affine/component'; +import { FlexWrapper } from '@affine/component'; +import { ExplorerDisplayMenuButton } from '@affine/core/components/explorer/display-menu'; +import { ViewToggle } from '@affine/core/components/explorer/display-menu/view-toggle'; import { ExplorerNavigation } from '@affine/core/components/explorer/header/navigation'; -import { PageDisplayMenu } from '@affine/core/components/page-list'; import { Header } from '@affine/core/components/pure/header'; -import { PlusIcon } from '@blocksuite/icons/rc'; -import clsx from 'clsx'; -import * as styles from './collection.css'; - -export const CollectionDetailHeader = ({ - showCreateNew, - onCreate, -}: { - showCreateNew: boolean; - onCreate: () => void; -}) => { +export const CollectionDetailHeader = () => { return (
- } - onClick={onCreate} - className={clsx( - styles.headerCreateNewButton, - styles.headerCreateNewCollectionIconButton, - !showCreateNew && styles.headerCreateNewButtonHidden - )} - /> - - + + + + } left={} /> diff --git a/packages/frontend/core/src/desktop/pages/workspace/collection/index.css.ts b/packages/frontend/core/src/desktop/pages/workspace/collection/index.css.ts new file mode 100644 index 0000000000..e0d4e2733b --- /dev/null +++ b/packages/frontend/core/src/desktop/pages/workspace/collection/index.css.ts @@ -0,0 +1,59 @@ +import { cssVarV2 } from '@toeverything/theme/v2'; +import { style } from '@vanilla-extract/css'; + +export const scrollArea = style({ + width: '100%', + flexGrow: 1, + height: 0, +}); + +export const collectionHeader = style({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: '24px', +}); + +export const breadcrumb = style({ + fontSize: 14, + lineHeight: '22px', + color: cssVarV2.text.secondary, + display: 'flex', + alignItems: 'center', +}); +export const breadcrumbItem = style({ + display: 'flex', + alignItems: 'center', + gap: 2, + cursor: 'pointer', + selectors: { + '&[data-active="true"]': { + color: cssVarV2.text.primary, + cursor: 'default', + }, + }, +}); +export const breadcrumbLink = style({ + color: 'inherit', + textDecoration: 'none', +}); +export const breadcrumbIcon = style({ + fontSize: 20, + color: cssVarV2.icon.primary, +}); +export const breadcrumbSeparator = style({ + marginLeft: 4, + marginRight: 8, +}); + +export const headerActions = style({ + display: 'flex', + alignItems: 'center', + gap: 16, +}); + +export const newPageButtonText = style({ + fontSize: 12, + lineHeight: '20px', + fontWeight: 500, +}); diff --git a/packages/frontend/core/src/desktop/pages/workspace/collection/index.tsx b/packages/frontend/core/src/desktop/pages/workspace/collection/index.tsx index 85a4cc31af..2577cec1c1 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/collection/index.tsx +++ b/packages/frontend/core/src/desktop/pages/workspace/collection/index.tsx @@ -1,10 +1,15 @@ +import { FlexWrapper } from '@affine/component'; import { EmptyCollectionDetail } from '@affine/core/components/affine/empty/collection-detail'; -import { VirtualizedPageList } from '@affine/core/components/page-list'; +import { + createDocExplorerContext, + DocExplorerContext, +} from '@affine/core/components/explorer/context'; +import { DocsExplorer } from '@affine/core/components/explorer/docs-view/docs-list'; import { type Collection, CollectionService, } from '@affine/core/modules/collection'; -import { WorkspaceDialogService } from '@affine/core/modules/dialogs'; +import { CollectionRulesService } from '@affine/core/modules/collection-rules'; import { GlobalContextService } from '@affine/core/modules/global-context'; import { WorkspacePermissionService } from '@affine/core/modules/permissions'; import { WorkspaceService } from '@affine/core/modules/workspace'; @@ -25,42 +30,82 @@ import { import { PageNotFound } from '../../404'; import { AllDocSidebarTabs } from '../layouts/all-doc-sidebar-tabs'; import { CollectionDetailHeader } from './header'; +import * as styles from './index.css'; +import { CollectionListHeader } from './list-header'; export const CollectionDetail = ({ collection, }: { collection: Collection; }) => { - const { workspaceDialogService } = useServices({ - WorkspaceDialogService, - }); + const [explorerContextValue] = useState(createDocExplorerContext); + const collectionRulesService = useService(CollectionRulesService); + const permissionService = useService(WorkspacePermissionService); const isAdmin = useLiveData(permissionService.permission.isAdmin$); const isOwner = useLiveData(permissionService.permission.isOwner$); - const [hideHeaderCreateNew, setHideHeaderCreateNew] = useState(true); - const handleEditCollection = useCallback(() => { - workspaceDialogService.open('collection-editor', { - collectionId: collection.id, - }); - }, [collection, workspaceDialogService]); + const groupBy = useLiveData(explorerContextValue.groupBy$); + const orderBy = useLiveData(explorerContextValue.orderBy$); + const rules = useLiveData(collection.rules$); + const allowList = useLiveData(collection.allowList$); + + useEffect(() => { + const subscription = collectionRulesService + .watch({ + filters: rules.filters, + groupBy, + orderBy, + extraAllowList: allowList, + extraFilters: [ + { + type: 'system', + key: 'empty-journal', + method: 'is', + value: 'false', + }, + { + type: 'system', + key: 'trash', + method: 'is', + value: 'false', + }, + ], + }) + .subscribe({ + next: result => { + explorerContextValue.groups$.next(result.groups); + }, + error: error => { + console.error(error); + }, + }); + return () => { + subscription.unsubscribe(); + }; + }, [ + allowList, + collectionRulesService, + explorerContextValue.groups$, + groupBy, + orderBy, + rules.filters, + ]); return ( - <> + - + - + + +
+ +
+
- +
); }; diff --git a/packages/frontend/core/src/desktop/pages/workspace/collection/list-header.tsx b/packages/frontend/core/src/desktop/pages/workspace/collection/list-header.tsx new file mode 100644 index 0000000000..a8f7f57801 --- /dev/null +++ b/packages/frontend/core/src/desktop/pages/workspace/collection/list-header.tsx @@ -0,0 +1,113 @@ +import { Button, useConfirmModal } from '@affine/component'; +import { usePageHelper } from '@affine/core/blocksuite/block-suite-page-list/utils'; +import { PageListNewPageButton } from '@affine/core/components/page-list'; +import { + type Collection, + CollectionService, +} from '@affine/core/modules/collection'; +import { WorkspaceDialogService } from '@affine/core/modules/dialogs'; +import type { DocRecord } from '@affine/core/modules/doc'; +import { WorkbenchLink } from '@affine/core/modules/workbench'; +import { WorkspaceService } from '@affine/core/modules/workspace'; +import { useI18n } from '@affine/i18n'; +import type { DocMode } from '@blocksuite/affine/model'; +import { ViewLayersIcon } from '@blocksuite/icons/rc'; +import { useLiveData, useServices } from '@toeverything/infra'; +import { useCallback } from 'react'; + +import * as styles from './index.css'; + +export const CollectionListHeader = ({ + collection, +}: { + collection: Collection; +}) => { + const t = useI18n(); + const { collectionService, workspaceService, workspaceDialogService } = + useServices({ + CollectionService, + WorkspaceService, + WorkspaceDialogService, + }); + + const handleEdit = useCallback(() => { + workspaceDialogService.open('collection-editor', { + collectionId: collection.id, + }); + }, [collection, workspaceDialogService]); + + const workspace = workspaceService.workspace; + const { createEdgeless, createPage } = usePageHelper(workspace.docCollection); + const { openConfirmModal } = useConfirmModal(); + const name = useLiveData(collection.name$); + + const createAndAddDocument = useCallback( + (createDocumentFn: () => DocRecord) => { + const newDoc = createDocumentFn(); + collectionService.addDocToCollection(collection.id, newDoc.id); + }, + [collection.id, collectionService] + ); + + const onConfirmAddDocument = useCallback( + (createDocumentFn: () => DocRecord) => { + openConfirmModal({ + title: t['com.affine.collection.add-doc.confirm.title'](), + description: t['com.affine.collection.add-doc.confirm.description'](), + cancelText: t['Cancel'](), + confirmText: t['Confirm'](), + confirmButtonOptions: { + variant: 'primary', + }, + onConfirm: () => createAndAddDocument(createDocumentFn), + }); + }, + [openConfirmModal, t, createAndAddDocument] + ); + + const createPageModeDoc = useCallback( + () => createPage('page' as DocMode), + [createPage] + ); + + const onCreateEdgeless = useCallback( + () => onConfirmAddDocument(createEdgeless), + [createEdgeless, onConfirmAddDocument] + ); + const onCreatePage = useCallback(() => { + onConfirmAddDocument(createPageModeDoc); + }, [createPageModeDoc, onConfirmAddDocument]); + const onCreateDoc = useCallback(() => { + onConfirmAddDocument(createPage); + }, [createPage, onConfirmAddDocument]); + + return ( +
+
+
+ + {t['com.affine.collections.header']()} + +
+
/
+
+ + {name} +
+
+ +
+ + +
{t['New Page']()}
+
+
+
+ ); +};