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 094dffd3e..e44e217ff 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
@@ -206,6 +206,9 @@ export const CollectionPageListHeader = ({
);
};
+/**
+ * @deprecated
+ */
export const TagPageListHeader = ({
tag,
workspaceId,
diff --git a/packages/frontend/core/src/desktop/pages/workspace/tag/header.tsx b/packages/frontend/core/src/desktop/pages/workspace/tag/header.tsx
index c235da176..acc1eab42 100644
--- a/packages/frontend/core/src/desktop/pages/workspace/tag/header.tsx
+++ b/packages/frontend/core/src/desktop/pages/workspace/tag/header.tsx
@@ -1,12 +1,12 @@
+import { ExplorerDisplayMenuButton } from '@affine/core/components/explorer/display-menu';
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';
export const TagDetailHeader = () => {
return (
}
- right={}
+ right={}
/>
);
};
diff --git a/packages/frontend/core/src/desktop/pages/workspace/tag/index.css.ts b/packages/frontend/core/src/desktop/pages/workspace/tag/index.css.ts
index a5f554270..b8ea2677c 100644
--- a/packages/frontend/core/src/desktop/pages/workspace/tag/index.css.ts
+++ b/packages/frontend/core/src/desktop/pages/workspace/tag/index.css.ts
@@ -7,3 +7,8 @@ export const body = style({
height: '100%',
width: '100%',
});
+
+export const scrollArea = style({
+ height: 0,
+ flexGrow: 1,
+});
diff --git a/packages/frontend/core/src/desktop/pages/workspace/tag/index.tsx b/packages/frontend/core/src/desktop/pages/workspace/tag/index.tsx
index 1dd8a85ff..6dedca3e7 100644
--- a/packages/frontend/core/src/desktop/pages/workspace/tag/index.tsx
+++ b/packages/frontend/core/src/desktop/pages/workspace/tag/index.tsx
@@ -1,8 +1,9 @@
-import { useBlockSuiteDocMeta } from '@affine/core/components/hooks/use-block-suite-page-meta';
import {
- TagPageListHeader,
- VirtualizedPageList,
-} from '@affine/core/components/page-list';
+ createDocExplorerContext,
+ DocExplorerContext,
+} from '@affine/core/components/explorer/context';
+import { DocsExplorer } from '@affine/core/components/explorer/docs-view/docs-list';
+import { CollectionRulesService } from '@affine/core/modules/collection-rules';
import { GlobalContextService } from '@affine/core/modules/global-context';
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
import { TagService } from '@affine/core/modules/tag';
@@ -13,9 +14,8 @@ import {
ViewIcon,
ViewTitle,
} from '@affine/core/modules/workbench';
-import { WorkspaceService } from '@affine/core/modules/workspace';
import { useLiveData, useService } from '@toeverything/infra';
-import { useEffect, useMemo } from 'react';
+import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { PageNotFound } from '../../404';
@@ -23,11 +23,12 @@ import { AllDocSidebarTabs } from '../layouts/all-doc-sidebar-tabs';
import { EmptyPageList } from '../page-list-empty';
import { TagDetailHeader } from './header';
import * as styles from './index.css';
+import { TagListHeader } from './list-header';
export const TagDetail = ({ tagId }: { tagId?: string }) => {
+ const [explorerContextValue] = useState(createDocExplorerContext);
+ const collectionRulesService = useService(CollectionRulesService);
const globalContext = useService(GlobalContextService).globalContext;
- const currentWorkspace = useService(WorkspaceService).workspace;
- const pageMetas = useBlockSuiteDocMeta(currentWorkspace.docCollection);
const permissionService = useService(WorkspacePermissionService);
const isAdmin = useLiveData(permissionService.permission.isAdmin$);
const isOwner = useLiveData(permissionService.permission.isOwner$);
@@ -35,14 +36,13 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => {
const tagList = useService(TagService).tagList;
const currentTag = useLiveData(tagList.tagByTagId$(tagId));
- const pageIds = useLiveData(currentTag?.pageIds$);
+ const groupBy = useLiveData(explorerContextValue.groupBy$);
+ const orderBy = useLiveData(explorerContextValue.orderBy$);
+ const groups = useLiveData(explorerContextValue.groups$);
- const filteredPageMetas = useMemo(() => {
- const pageIdsSet = new Set(pageIds);
- return pageMetas
- .filter(page => pageIdsSet.has(page.id))
- .filter(page => !page.trash);
- }, [pageIds, pageMetas]);
+ const isEmpty =
+ groups.length === 0 ||
+ (groups.length && groups.every(group => group.items.length === 0));
const isActiveView = useIsActiveView();
const tagName = useLiveData(currentTag?.value$);
@@ -60,12 +60,57 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => {
return;
}, [currentTag, globalContext, isActiveView]);
- if (!currentTag) {
+ useEffect(() => {
+ const subscription = collectionRulesService
+ .watch({
+ filters: [
+ {
+ type: 'system',
+ key: 'empty-journal',
+ method: 'is',
+ value: 'false',
+ },
+ {
+ type: 'system',
+ key: 'trash',
+ method: 'is',
+ value: 'false',
+ },
+ {
+ type: 'property',
+ key: 'tags',
+ method: 'include-all',
+ value: tagId,
+ },
+ ],
+ groupBy,
+ orderBy,
+ })
+ .subscribe({
+ next: result => {
+ explorerContextValue.groups$.next(result.groups);
+ },
+ error: error => {
+ console.error(error);
+ },
+ });
+ return () => {
+ subscription.unsubscribe();
+ };
+ }, [
+ collectionRulesService,
+ explorerContextValue.groups$,
+ groupBy,
+ orderBy,
+ tagId,
+ ]);
+
+ if (!currentTag || !tagId) {
return ;
}
return (
- <>
+
@@ -73,27 +118,17 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => {
- {filteredPageMetas.length > 0 ? (
-
- ) : (
-
- }
- />
- )}
+
+
+ {isEmpty ? (
+
+ ) : (
+
+ )}
+
- >
+
);
};
diff --git a/packages/frontend/core/src/desktop/pages/workspace/tag/list-header.css.ts b/packages/frontend/core/src/desktop/pages/workspace/tag/list-header.css.ts
new file mode 100644
index 000000000..2b5c6c285
--- /dev/null
+++ b/packages/frontend/core/src/desktop/pages/workspace/tag/list-header.css.ts
@@ -0,0 +1,164 @@
+import { cssVarV2 } from '@toeverything/theme/v2';
+import { style } from '@vanilla-extract/css';
+
+export const header = 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 tagSelectorTrigger = style({
+ display: 'flex',
+ alignItems: 'center',
+ gap: 2,
+ cursor: 'pointer',
+ padding: '0px 2px',
+
+ borderRadius: 100,
+ border: `1px solid ${cssVarV2.layer.insideBorder.border}`,
+});
+export const tagSelectorTriggerIcon = style({
+ width: 18,
+ height: 16,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ ':after': {
+ width: 7,
+ height: 7,
+ borderRadius: '50%',
+ content: '',
+ display: 'block',
+ backgroundColor: 'currentColor',
+ },
+});
+export const tagSelectorTriggerName = style({
+ fontSize: 14,
+ lineHeight: '22px',
+ color: cssVarV2.text.primary,
+});
+export const tagSelectorTriggerDropdown = style({
+ width: 24,
+ height: 24,
+ fontSize: 20,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ color: cssVarV2.icon.primary,
+});
+
+export const tagSelectorMenuRoot = style({
+ padding: 0,
+ maxHeight: 400,
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 2,
+});
+export const tagSelectorMenuHeader = style({
+ padding: '12px 12px 0 12px',
+ display: 'flex',
+ alignItems: 'center',
+ gap: 8,
+});
+export const tagSelectorMenuSearchIcon = style({
+ fontSize: 16,
+ color: cssVarV2.icon.secondary,
+});
+export const tagSelectorMenuScrollArea = style({
+ height: 'fit-content',
+ flexGrow: 1,
+ flexShrink: 1,
+ display: 'flex',
+ flexDirection: 'column',
+});
+export const tagSelectorMenuViewport = style({
+ padding: '1px 8px 12px 8px',
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 2,
+ height: 'fit-content',
+ flexGrow: 1,
+});
+export const tagSelectorMenuEmpty = style({
+ color: cssVarV2.text.secondary,
+ fontSize: 12,
+ lineHeight: '20px',
+});
+
+export const tagSelectorMenuItem = style({
+ padding: 0,
+});
+export const tagSelectorItem = style({
+ display: 'flex',
+ alignItems: 'center',
+ gap: 4,
+ padding: '2px 0px',
+});
+export const tagSelectorItemIcon = style({
+ width: 24,
+ height: 24,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ ':after': {
+ width: 7,
+ height: 7,
+ borderRadius: '50%',
+ content: '',
+ display: 'block',
+ backgroundColor: 'currentColor',
+ },
+});
+export const tagSelectorItemText = style({
+ fontSize: 14,
+ lineHeight: '22px',
+ color: cssVarV2.text.primary,
+ width: 0,
+ flexGrow: 1,
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ whiteSpace: 'nowrap',
+});
+export const tagSelectorItemCheckedIcon = style({
+ fontSize: 16,
+ color: cssVarV2.button.primary,
+});
diff --git a/packages/frontend/core/src/desktop/pages/workspace/tag/list-header.tsx b/packages/frontend/core/src/desktop/pages/workspace/tag/list-header.tsx
new file mode 100644
index 000000000..5c83cef70
--- /dev/null
+++ b/packages/frontend/core/src/desktop/pages/workspace/tag/list-header.tsx
@@ -0,0 +1,185 @@
+import {
+ Divider,
+ Menu,
+ MenuItem,
+ type MenuProps,
+ RowInput,
+ Scrollable,
+} from '@affine/component';
+import { type Tag, TagService } from '@affine/core/modules/tag';
+import { WorkbenchLink } from '@affine/core/modules/workbench';
+import { useI18n } from '@affine/i18n';
+import { ArrowDownSmallIcon, DoneIcon, SearchIcon } from '@blocksuite/icons/rc';
+import { useLiveData, useService } from '@toeverything/infra';
+import clsx from 'clsx';
+import {
+ forwardRef,
+ type HTMLProps,
+ useCallback,
+ useRef,
+ useState,
+} from 'react';
+
+import * as styles from './list-header.css';
+
+export const TagListHeader = ({ tag }: { tag: Tag }) => {
+ const t = useI18n();
+ return (
+
+
+
+
+ {t['Tags']()}
+
+
+
/
+
+
+
+
+
+
+
+ );
+};
+
+const contentMenuOptions: MenuProps['contentOptions'] = {
+ align: 'start',
+ side: 'bottom',
+ sideOffset: 4,
+ className: styles.tagSelectorMenuRoot,
+};
+const TagSelector = ({ currentTag }: { currentTag: Tag }) => {
+ const [isOpen, setIsOpen] = useState(false);
+
+ const onClose = useCallback(() => {
+ setIsOpen(false);
+ }, []);
+
+ return (
+
}
+ >
+
+
+ );
+};
+
+const TagSelectorTrigger = forwardRef<
+ HTMLDivElement,
+ HTMLProps & { currentTag: Tag }
+>(function TagSelectorTrigger({ currentTag, className, ...props }, ref) {
+ const tagColor = useLiveData(currentTag.color$);
+ const tagName = useLiveData(currentTag.value$);
+
+ return (
+
+ );
+});
+
+const TagSelectorMenu = ({
+ currentTagId,
+ onClose,
+}: {
+ currentTagId: string;
+ onClose: () => void;
+}) => {
+ const t = useI18n();
+ const [inputValue, setInputValue] = useState('');
+ const tagList = useService(TagService).tagList;
+ const filteredTags = useLiveData(
+ inputValue ? tagList.filterTagsByName$(inputValue) : tagList.tags$
+ );
+ return (
+ <>
+
+
+
+
+ {filteredTags.map(tag => {
+ return (
+
+ );
+ })}
+ {filteredTags.length === 0 ? (
+
+ {t['Find 0 result']()}
+
+ ) : null}
+
+
+
+ >
+ );
+};
+
+const TagLink = ({
+ tag,
+ checked,
+ onClick,
+}: {
+ tag: Tag;
+ checked: boolean;
+ onClick: () => void;
+}) => {
+ const tagColor = useLiveData(tag.color$);
+ const tagTitle = useLiveData(tag.value$);
+ const aRef = useRef(null);
+
+ const onSelect = useCallback(() => {
+ aRef.current?.click();
+ }, []);
+
+ return (
+
+ );
+};