mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-16 13:57:02 +08:00
feat(core): new docs list for tag detail (#12298)
close AF-2583 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a new tag list header with breadcrumb navigation and a tag selector dropdown for improved navigation and tag management. - Added a searchable dropdown menu for selecting and switching between tags. - **Improvements** - Updated the tag detail page to use a more dynamic, subscription-based document explorer for displaying tagged content. - Enhanced header controls with an updated display menu button. - **Style** - Added comprehensive new styles for the tag list header and tag selector components. - Introduced a new scroll area style for flexible layout. - **Documentation** - Marked an older page list header component as deprecated. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -206,6 +206,9 @@ export const CollectionPageListHeader = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
export const TagPageListHeader = ({
|
export const TagPageListHeader = ({
|
||||||
tag,
|
tag,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
|
import { ExplorerDisplayMenuButton } from '@affine/core/components/explorer/display-menu';
|
||||||
import { ExplorerNavigation } from '@affine/core/components/explorer/header/navigation';
|
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 { Header } from '@affine/core/components/pure/header';
|
||||||
|
|
||||||
export const TagDetailHeader = () => {
|
export const TagDetailHeader = () => {
|
||||||
return (
|
return (
|
||||||
<Header
|
<Header
|
||||||
left={<ExplorerNavigation active={'tags'} />}
|
left={<ExplorerNavigation active={'tags'} />}
|
||||||
right={<PageDisplayMenu />}
|
right={<ExplorerDisplayMenuButton />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,3 +7,8 @@ export const body = style({
|
|||||||
height: '100%',
|
height: '100%',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const scrollArea = style({
|
||||||
|
height: 0,
|
||||||
|
flexGrow: 1,
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useBlockSuiteDocMeta } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
|
||||||
import {
|
import {
|
||||||
TagPageListHeader,
|
createDocExplorerContext,
|
||||||
VirtualizedPageList,
|
DocExplorerContext,
|
||||||
} from '@affine/core/components/page-list';
|
} 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 { GlobalContextService } from '@affine/core/modules/global-context';
|
||||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||||
import { TagService } from '@affine/core/modules/tag';
|
import { TagService } from '@affine/core/modules/tag';
|
||||||
@@ -13,9 +14,8 @@ import {
|
|||||||
ViewIcon,
|
ViewIcon,
|
||||||
ViewTitle,
|
ViewTitle,
|
||||||
} from '@affine/core/modules/workbench';
|
} from '@affine/core/modules/workbench';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { PageNotFound } from '../../404';
|
import { PageNotFound } from '../../404';
|
||||||
@@ -23,11 +23,12 @@ import { AllDocSidebarTabs } from '../layouts/all-doc-sidebar-tabs';
|
|||||||
import { EmptyPageList } from '../page-list-empty';
|
import { EmptyPageList } from '../page-list-empty';
|
||||||
import { TagDetailHeader } from './header';
|
import { TagDetailHeader } from './header';
|
||||||
import * as styles from './index.css';
|
import * as styles from './index.css';
|
||||||
|
import { TagListHeader } from './list-header';
|
||||||
|
|
||||||
export const TagDetail = ({ tagId }: { tagId?: string }) => {
|
export const TagDetail = ({ tagId }: { tagId?: string }) => {
|
||||||
|
const [explorerContextValue] = useState(createDocExplorerContext);
|
||||||
|
const collectionRulesService = useService(CollectionRulesService);
|
||||||
const globalContext = useService(GlobalContextService).globalContext;
|
const globalContext = useService(GlobalContextService).globalContext;
|
||||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
|
||||||
const pageMetas = useBlockSuiteDocMeta(currentWorkspace.docCollection);
|
|
||||||
const permissionService = useService(WorkspacePermissionService);
|
const permissionService = useService(WorkspacePermissionService);
|
||||||
const isAdmin = useLiveData(permissionService.permission.isAdmin$);
|
const isAdmin = useLiveData(permissionService.permission.isAdmin$);
|
||||||
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
||||||
@@ -35,14 +36,13 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => {
|
|||||||
const tagList = useService(TagService).tagList;
|
const tagList = useService(TagService).tagList;
|
||||||
const currentTag = useLiveData(tagList.tagByTagId$(tagId));
|
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 isEmpty =
|
||||||
const pageIdsSet = new Set(pageIds);
|
groups.length === 0 ||
|
||||||
return pageMetas
|
(groups.length && groups.every(group => group.items.length === 0));
|
||||||
.filter(page => pageIdsSet.has(page.id))
|
|
||||||
.filter(page => !page.trash);
|
|
||||||
}, [pageIds, pageMetas]);
|
|
||||||
|
|
||||||
const isActiveView = useIsActiveView();
|
const isActiveView = useIsActiveView();
|
||||||
const tagName = useLiveData(currentTag?.value$);
|
const tagName = useLiveData(currentTag?.value$);
|
||||||
@@ -60,12 +60,57 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => {
|
|||||||
return;
|
return;
|
||||||
}, [currentTag, globalContext, isActiveView]);
|
}, [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 <PageNotFound />;
|
return <PageNotFound />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DocExplorerContext.Provider value={explorerContextValue}>
|
||||||
<ViewTitle title={tagName ?? 'Untitled'} />
|
<ViewTitle title={tagName ?? 'Untitled'} />
|
||||||
<ViewIcon icon="tag" />
|
<ViewIcon icon="tag" />
|
||||||
<ViewHeader>
|
<ViewHeader>
|
||||||
@@ -73,27 +118,17 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => {
|
|||||||
</ViewHeader>
|
</ViewHeader>
|
||||||
<ViewBody>
|
<ViewBody>
|
||||||
<div className={styles.body}>
|
<div className={styles.body}>
|
||||||
{filteredPageMetas.length > 0 ? (
|
<TagListHeader tag={currentTag} />
|
||||||
<VirtualizedPageList
|
<div className={styles.scrollArea}>
|
||||||
tag={currentTag}
|
{isEmpty ? (
|
||||||
listItem={filteredPageMetas}
|
<EmptyPageList type="all" tagId={tagId} />
|
||||||
disableMultiDelete={!isAdmin && !isOwner}
|
) : (
|
||||||
/>
|
<DocsExplorer disableMultiDelete={!isAdmin && !isOwner} />
|
||||||
) : (
|
)}
|
||||||
<EmptyPageList
|
</div>
|
||||||
type="all"
|
|
||||||
tagId={tagId}
|
|
||||||
heading={
|
|
||||||
<TagPageListHeader
|
|
||||||
tag={currentTag}
|
|
||||||
workspaceId={currentWorkspace.id}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</ViewBody>
|
</ViewBody>
|
||||||
</>
|
</DocExplorerContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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 (
|
||||||
|
<header className={styles.header}>
|
||||||
|
<div className={styles.breadcrumb}>
|
||||||
|
<div className={styles.breadcrumbItem}>
|
||||||
|
<WorkbenchLink to="/tag" className={styles.breadcrumbLink}>
|
||||||
|
{t['Tags']()}
|
||||||
|
</WorkbenchLink>
|
||||||
|
</div>
|
||||||
|
<div className={styles.breadcrumbSeparator}>/</div>
|
||||||
|
<div className={styles.breadcrumbItem} data-active={true}>
|
||||||
|
<TagSelector currentTag={tag} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.headerActions}></div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Menu
|
||||||
|
rootOptions={{
|
||||||
|
open: isOpen,
|
||||||
|
onOpenChange: setIsOpen,
|
||||||
|
}}
|
||||||
|
contentOptions={contentMenuOptions}
|
||||||
|
items={<TagSelectorMenu currentTagId={currentTag.id} onClose={onClose} />}
|
||||||
|
>
|
||||||
|
<TagSelectorTrigger currentTag={currentTag} />
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TagSelectorTrigger = forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
HTMLProps<HTMLDivElement> & { currentTag: Tag }
|
||||||
|
>(function TagSelectorTrigger({ currentTag, className, ...props }, ref) {
|
||||||
|
const tagColor = useLiveData(currentTag.color$);
|
||||||
|
const tagName = useLiveData(currentTag.value$);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(styles.tagSelectorTrigger, className)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={styles.tagSelectorTriggerIcon}
|
||||||
|
style={{ color: tagColor }}
|
||||||
|
/>
|
||||||
|
<div className={styles.tagSelectorTriggerName}>{tagName}</div>
|
||||||
|
<div className={styles.tagSelectorTriggerDropdown}>
|
||||||
|
<ArrowDownSmallIcon />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<header className={styles.tagSelectorMenuHeader}>
|
||||||
|
<SearchIcon className={styles.tagSelectorMenuSearchIcon} />
|
||||||
|
<RowInput
|
||||||
|
value={inputValue}
|
||||||
|
onChange={setInputValue}
|
||||||
|
placeholder={t['Search tags']()}
|
||||||
|
/>
|
||||||
|
</header>
|
||||||
|
<Divider size="thinner" />
|
||||||
|
<Scrollable.Root className={styles.tagSelectorMenuScrollArea}>
|
||||||
|
<Scrollable.Viewport className={styles.tagSelectorMenuViewport}>
|
||||||
|
{filteredTags.map(tag => {
|
||||||
|
return (
|
||||||
|
<TagLink
|
||||||
|
key={tag.id}
|
||||||
|
tag={tag}
|
||||||
|
checked={tag.id === currentTagId}
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{filteredTags.length === 0 ? (
|
||||||
|
<div className={styles.tagSelectorMenuEmpty}>
|
||||||
|
{t['Find 0 result']()}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</Scrollable.Viewport>
|
||||||
|
<Scrollable.Scrollbar />
|
||||||
|
</Scrollable.Root>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TagLink = ({
|
||||||
|
tag,
|
||||||
|
checked,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
tag: Tag;
|
||||||
|
checked: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
}) => {
|
||||||
|
const tagColor = useLiveData(tag.color$);
|
||||||
|
const tagTitle = useLiveData(tag.value$);
|
||||||
|
const aRef = useRef<HTMLAnchorElement>(null);
|
||||||
|
|
||||||
|
const onSelect = useCallback(() => {
|
||||||
|
aRef.current?.click();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem onSelect={onSelect} className={styles.tagSelectorMenuItem}>
|
||||||
|
<WorkbenchLink
|
||||||
|
ref={aRef}
|
||||||
|
key={tag.id}
|
||||||
|
className={styles.tagSelectorItem}
|
||||||
|
data-tag-id={tag.id}
|
||||||
|
data-tag-value={tagTitle}
|
||||||
|
to={`/tag/${tag.id}`}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={styles.tagSelectorItemIcon}
|
||||||
|
style={{ color: tagColor }}
|
||||||
|
/>
|
||||||
|
<div className={styles.tagSelectorItemText}>{tagTitle}</div>
|
||||||
|
{checked ? (
|
||||||
|
<DoneIcon className={styles.tagSelectorItemCheckedIcon} />
|
||||||
|
) : null}
|
||||||
|
</WorkbenchLink>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user