diff --git a/packages/frontend/core/src/components/page-list/collections/collection-list-header.css.ts b/packages/frontend/core/src/components/page-list/collections/collection-list-header.css.ts index 7ac21e2025..76467cb8de 100644 --- a/packages/frontend/core/src/components/page-list/collections/collection-list-header.css.ts +++ b/packages/frontend/core/src/components/page-list/collections/collection-list-header.css.ts @@ -16,6 +16,7 @@ export const collectionListHeaderTitle = style({ display: 'flex', alignItems: 'center', gap: 8, + userSelect: 'none', }); export const newCollectionButton = style({ padding: '6px 10px', diff --git a/packages/frontend/core/src/components/page-list/collections/collection-list-item.tsx b/packages/frontend/core/src/components/page-list/collections/collection-list-item.tsx index 2edea5dc8a..21d4c67ed3 100644 --- a/packages/frontend/core/src/components/page-list/collections/collection-list-item.tsx +++ b/packages/frontend/core/src/components/page-list/collections/collection-list-item.tsx @@ -144,7 +144,7 @@ export const CollectionListItem = (props: CollectionListItemProps) => { {props.operations ? ( diff --git a/packages/frontend/core/src/components/page-list/collections/virtualized-collection-list.tsx b/packages/frontend/core/src/components/page-list/collections/virtualized-collection-list.tsx index fd68998420..e8b56f7778 100644 --- a/packages/frontend/core/src/components/page-list/collections/virtualized-collection-list.tsx +++ b/packages/frontend/core/src/components/page-list/collections/virtualized-collection-list.tsx @@ -31,7 +31,7 @@ const useCollectionOperationsRenderer = ({ config: AllPageListConfig; service: CollectionService; }) => { - const pageOperationsRenderer = useCallback( + const collectionOperationsRenderer = useCallback( (collection: Collection) => { return ( { + const legacyProperties = useService(WorkspaceLegacyProperties); + const options = useLiveData(legacyProperties.tagOptions$); const t = useAFFiNEI18N(); const { jumpToTags, jumpToCollection } = useNavigateHelper(); const collectionService = useService(CollectionService); + const [openMenu, setOpenMenu] = useState(false); const { open, node } = useEditCollectionName({ title: t['com.affine.editCollection.saveCollection'](), showTips: true, @@ -131,15 +141,31 @@ export const TagPageListHeader = ({ > {t['Tags']()} / -
-
-
{tag.value}
-
+ } + > +
+
+
{tag.value}
+ +
+
+ + + + + + + ); +}; diff --git a/packages/frontend/core/src/components/page-list/tags/tag-list-header.css.ts b/packages/frontend/core/src/components/page-list/tags/tag-list-header.css.ts index 0d38513db0..76f8ec7c40 100644 --- a/packages/frontend/core/src/components/page-list/tags/tag-list-header.css.ts +++ b/packages/frontend/core/src/components/page-list/tags/tag-list-header.css.ts @@ -16,6 +16,7 @@ export const tagListHeaderTitle = style({ display: 'flex', alignItems: 'center', gap: 8, + userSelect: 'none', }); export const newTagButton = style({ padding: '6px 10px', diff --git a/packages/frontend/core/src/components/page-list/tags/tag-list-header.tsx b/packages/frontend/core/src/components/page-list/tags/tag-list-header.tsx index b225853867..02662c8b0a 100644 --- a/packages/frontend/core/src/components/page-list/tags/tag-list-header.tsx +++ b/packages/frontend/core/src/components/page-list/tags/tag-list-header.tsx @@ -1,12 +1,16 @@ +import { Button } from '@affine/component'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import * as styles from './tag-list-header.css'; -export const TagListHeader = () => { +export const TagListHeader = ({ onOpen }: { onOpen: () => void }) => { const t = useAFFiNEI18N(); return (
{t['Tags']()}
+
); }; diff --git a/packages/frontend/core/src/components/page-list/tags/tag-list-item.css.ts b/packages/frontend/core/src/components/page-list/tags/tag-list-item.css.ts index 7af6206cbd..0e1d0b7807 100644 --- a/packages/frontend/core/src/components/page-list/tags/tag-list-item.css.ts +++ b/packages/frontend/core/src/components/page-list/tags/tag-list-item.css.ts @@ -87,7 +87,7 @@ globalStyle(`${root} > :last-child`, { paddingRight: '8px', }); export const titleIconsWrapper = style({ - padding: '0 5px', + padding: '5px', display: 'flex', alignItems: 'center', gap: '10px', @@ -119,7 +119,7 @@ export const titleCellPreview = style({ overflow: 'hidden', color: cssVar('textSecondaryColor'), fontSize: cssVar('fontBase'), - flex: 1, + flexShrink: 0, whiteSpace: 'nowrap', textOverflow: 'ellipsis', alignSelf: 'stretch', @@ -162,6 +162,13 @@ export const operationsCell = style({ columnGap: '6px', flexShrink: 0, }); +export const tagIndicatorWrapper = style({ + width: '24px', + height: '24px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +}); export const tagIndicator = style({ width: '8px', height: '8px', diff --git a/packages/frontend/core/src/components/page-list/tags/tag-list-item.tsx b/packages/frontend/core/src/components/page-list/tags/tag-list-item.tsx index 1e2bc739fd..506ed92bba 100644 --- a/packages/frontend/core/src/components/page-list/tags/tag-list-item.tsx +++ b/packages/frontend/core/src/components/page-list/tags/tag-list-item.tsx @@ -14,9 +14,9 @@ const TagListTitleCell = ({ }: Pick) => { const t = useAFFiNEI18N(); return ( -
+
{title || t['Untitled']()} @@ -25,7 +25,7 @@ const TagListTitleCell = ({ data-testid="page-list-item-preview-text" className={styles.titleCellPreview} > - {`· ${pageCount} doc(s)`} + {` · ${t['com.affine.tags.count']({ count: pageCount || 0 })}`}
); @@ -33,12 +33,14 @@ const TagListTitleCell = ({ const ListIconCell = ({ color }: Pick) => { return ( -
+
+
+
); }; @@ -138,7 +140,7 @@ export const TagListItem = (props: TagListItemProps) => { {props.operations ? ( diff --git a/packages/frontend/core/src/components/page-list/tags/virtualized-tag-list.tsx b/packages/frontend/core/src/components/page-list/tags/virtualized-tag-list.tsx index 71adb30e53..2e205c8727 100644 --- a/packages/frontend/core/src/components/page-list/tags/virtualized-tag-list.tsx +++ b/packages/frontend/core/src/components/page-list/tags/virtualized-tag-list.tsx @@ -1,4 +1,6 @@ +import { toast } from '@affine/component'; import { Trans } from '@affine/i18n'; +import { useAFFiNEI18N } from '@affine/i18n/hooks'; import type { Tag } from '@blocksuite/store'; import { useService } from '@toeverything/infra'; import { Workspace } from '@toeverything/infra'; @@ -6,10 +8,12 @@ import { useCallback, useMemo, useRef, useState } from 'react'; import { ListFloatingToolbar } from '../components/list-floating-toolbar'; import { tagHeaderColsDef } from '../header-col-def'; +import { TagOperationCell } from '../operation-cell'; import { TagListItemRenderer } from '../page-group'; import { ListTableHeader } from '../page-header'; import type { ItemListHandle, ListItem, TagMeta } from '../types'; import { VirtualizedList } from '../virtualized-list'; +import { CreateOrEditTag } from './create-tag'; import { TagListHeader } from './tag-list-header'; export const VirtualizedTagList = ({ @@ -21,11 +25,20 @@ export const VirtualizedTagList = ({ tagMetas: TagMeta[]; onTagDelete: (tagIds: string[]) => void; }) => { + const t = useAFFiNEI18N(); const listRef = useRef(null); const [showFloatingToolbar, setShowFloatingToolbar] = useState(false); + const [showCreateTagInput, setShowCreateTagInput] = useState(false); const [selectedTagIds, setSelectedTagIds] = useState([]); const currentWorkspace = useService(Workspace); + const tagOperations = useCallback( + (tag: TagMeta) => { + return ; + }, + [onTagDelete] + ); + const filteredSelectedTagIds = useMemo(() => { const ids = tags.map(tag => tag.id); return selectedTagIds.filter(id => ids.includes(id)); @@ -35,13 +48,25 @@ export const VirtualizedTagList = ({ listRef.current?.toggleSelectable(); }, []); - const tagOperationRenderer = useCallback(() => { - return null; - }, []); + const tagOperationRenderer = useCallback( + (item: ListItem) => { + const tag = item as TagMeta; + return tagOperations(tag); + }, + [tagOperations] + ); const tagHeaderRenderer = useCallback(() => { - return ; - }, []); + return ( + <> + + + + ); + }, [showCreateTagInput]); const tagItemRenderer = useCallback((item: ListItem) => { return ; @@ -49,20 +74,25 @@ export const VirtualizedTagList = ({ const handleDelete = useCallback(() => { onTagDelete(selectedTagIds); + toast(t['com.affine.delete-tags.count']({ count: selectedTagIds.length })); hideFloatingToolbar(); return; - }, [hideFloatingToolbar, onTagDelete, selectedTagIds]); + }, [hideFloatingToolbar, onTagDelete, selectedTagIds, t]); + + const onOpenCreate = useCallback(() => { + setShowCreateTagInput(true); + }, [setShowCreateTagInput]); return ( <> } + heading={} selectedIds={filteredSelectedTagIds} onSelectedIdsChange={setSelectedTagIds} items={tagMetas} diff --git a/packages/frontend/core/src/components/page-list/types.ts b/packages/frontend/core/src/components/page-list/types.ts index 0b871ef675..895ca86414 100644 --- a/packages/frontend/core/src/components/page-list/types.ts +++ b/packages/frontend/core/src/components/page-list/types.ts @@ -104,7 +104,7 @@ export interface ListProps { export interface VirtualizedListProps extends ListProps { heading?: ReactNode; // the user provided heading part (non sticky, above the original header) - headerRenderer?: () => ReactNode; // the user provided header renderer + headerRenderer?: (item?: T) => ReactNode; // the user provided header renderer itemRenderer?: (item: T) => ReactNode; // the user provided item renderer atTopThreshold?: number; // the threshold to determine whether or not the user has scrolled to the top. default is 0 atTopStateChange?: (atTop: boolean) => void; // called when the user scrolls to the top or not diff --git a/packages/frontend/core/src/components/page-list/use-tag-metas.ts b/packages/frontend/core/src/components/page-list/use-tag-metas.ts index 4d720973ba..4e28c36165 100644 --- a/packages/frontend/core/src/components/page-list/use-tag-metas.ts +++ b/packages/frontend/core/src/components/page-list/use-tag-metas.ts @@ -1,14 +1,15 @@ -import type { DocMeta, Tag, Workspace } from '@blocksuite/store'; +import { WorkspaceLegacyProperties } from '@affine/core/modules/workspace'; +import type { DocMeta } from '@blocksuite/store'; +import { useLiveData, useService } from '@toeverything/infra'; import { useCallback, useMemo } from 'react'; interface TagUsageCounts { [key: string]: number; } -export function useTagMetas(currentWorkspace: Workspace, pageMetas: DocMeta[]) { - const tags = useMemo(() => { - return currentWorkspace.meta.properties.tags?.options || []; - }, [currentWorkspace]); +export function useTagMetas(pageMetas: DocMeta[]) { + const legacyProperties = useService(WorkspaceLegacyProperties); + const tags = useLiveData(legacyProperties.tagOptions$); const [tagMetas, tagUsageCounts] = useMemo(() => { const tagUsageCounts: TagUsageCounts = {}; @@ -48,41 +49,13 @@ export function useTagMetas(currentWorkspace: Workspace, pageMetas: DocMeta[]) { [pageMetas] ); - const addNewTag = useCallback( - (tag: Tag) => { - const newTags = [...tags, tag]; - currentWorkspace.meta.setProperties({ - tags: { options: newTags }, - }); - }, - [currentWorkspace.meta, tags] - ); - - const updateTag = useCallback( - (tag: Tag) => { - const newTags = tags.map(t => { - if (t.id === tag.id) { - return tag; - } - return t; - }); - currentWorkspace.meta.setProperties({ - tags: { options: newTags }, - }); - }, - [currentWorkspace.meta, tags] - ); - const deleteTags = useCallback( (tagIds: string[]) => { - const newTags = tags.filter(tag => { - return !tagIds.includes(tag.id); - }); - currentWorkspace.meta.setProperties({ - tags: { options: newTags }, + tagIds.forEach(tagId => { + legacyProperties.removeTagOption(tagId); }); }, - [currentWorkspace.meta, tags] + [legacyProperties] ); return { @@ -90,8 +63,6 @@ export function useTagMetas(currentWorkspace: Workspace, pageMetas: DocMeta[]) { tagMetas, tagUsageCounts, filterPageMetaByTag, - addNewTag, - updateTag, deleteTags, }; } diff --git a/packages/frontend/core/src/components/page-list/virtualized-list.tsx b/packages/frontend/core/src/components/page-list/virtualized-list.tsx index 2e42139940..b057814b9c 100644 --- a/packages/frontend/core/src/components/page-list/virtualized-list.tsx +++ b/packages/frontend/core/src/components/page-list/virtualized-list.tsx @@ -39,8 +39,9 @@ interface BaseVirtuosoItem { type: VirtuosoItemType; } -interface VirtuosoItemStickyHeader extends BaseVirtuosoItem { +interface VirtuosoItemStickyHeader extends BaseVirtuosoItem { type: 'sticky-header'; + data?: T; } interface VirtuosoItemItem extends BaseVirtuosoItem { @@ -61,7 +62,7 @@ interface VirtuosoPageItemSpacer extends BaseVirtuosoItem { } type VirtuosoItem = - | VirtuosoItemStickyHeader + | VirtuosoItemStickyHeader | VirtuosoItemItem | VirtuosoItemGroupHeader | VirtuosoPageItemSpacer; @@ -187,7 +188,7 @@ const ListInner = ({ (_index: number, data: VirtuosoItem) => { switch (data.type) { case 'sticky-header': - return props.headerRenderer?.(); + return props.headerRenderer?.(data.data); case 'group-header': return ; case 'item': diff --git a/packages/frontend/core/src/pages/workspace/all-tag/index.tsx b/packages/frontend/core/src/pages/workspace/all-tag/index.tsx index 4c65fe7fca..57e52c3b25 100644 --- a/packages/frontend/core/src/pages/workspace/all-tag/index.tsx +++ b/packages/frontend/core/src/pages/workspace/all-tag/index.tsx @@ -3,23 +3,39 @@ import { TagListHeader, VirtualizedTagList, } from '@affine/core/components/page-list/tags'; +import { CreateOrEditTag } from '@affine/core/components/page-list/tags/create-tag'; import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta'; import { useService } from '@toeverything/infra'; import { Workspace } from '@toeverything/infra'; +import { useCallback, useState } from 'react'; import { ViewBodyIsland, ViewHeaderIsland } from '../../../modules/workbench'; import { EmptyTagList } from '../page-list-empty'; import * as styles from './all-tag.css'; import { AllTagHeader } from './header'; +const EmptyTagListHeader = () => { + const [showCreateTagInput, setShowCreateTagInput] = useState(false); + const handleOpen = useCallback(() => { + setShowCreateTagInput(true); + }, [setShowCreateTagInput]); + + return ( +
+ + +
+ ); +}; + export const AllTag = () => { const currentWorkspace = useService(Workspace); const pageMetas = useBlockSuiteDocMeta(currentWorkspace.blockSuiteWorkspace); - const { tags, tagMetas, deleteTags } = useTagMetas( - currentWorkspace.blockSuiteWorkspace, - pageMetas - ); + const { tags, tagMetas, deleteTags } = useTagMetas(pageMetas); return ( <> @@ -35,7 +51,7 @@ export const AllTag = () => { onTagDelete={deleteTags} /> ) : ( - } /> + } /> )}
diff --git a/packages/frontend/core/src/pages/workspace/tag/index.css.ts b/packages/frontend/core/src/pages/workspace/tag/index.css.ts new file mode 100644 index 0000000000..a5f5542709 --- /dev/null +++ b/packages/frontend/core/src/pages/workspace/tag/index.css.ts @@ -0,0 +1,9 @@ +import { style } from '@vanilla-extract/css'; + +export const body = style({ + display: 'flex', + flexDirection: 'column', + flex: 1, + height: '100%', + width: '100%', +}); diff --git a/packages/frontend/core/src/pages/workspace/tag/index.tsx b/packages/frontend/core/src/pages/workspace/tag/index.tsx index 82e1eab740..df5c3b725c 100644 --- a/packages/frontend/core/src/pages/workspace/tag/index.tsx +++ b/packages/frontend/core/src/pages/workspace/tag/index.tsx @@ -1,9 +1,13 @@ import { - PageListHeader, + TagPageListHeader, useTagMetas, VirtualizedPageList, } from '@affine/core/components/page-list'; import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta'; +import { + ViewBodyIsland, + ViewHeaderIsland, +} from '@affine/core/modules/workbench'; import { useService } from '@toeverything/infra'; import { Workspace } from '@toeverything/infra'; import { useMemo } from 'react'; @@ -12,15 +16,13 @@ import { useParams } from 'react-router-dom'; import { PageNotFound } from '../../404'; import { EmptyPageList } from '../page-list-empty'; import { TagDetailHeader } from './header'; +import * as styles from './index.css'; export const TagDetail = ({ tagId }: { tagId?: string }) => { const currentWorkspace = useService(Workspace); const pageMetas = useBlockSuiteDocMeta(currentWorkspace.blockSuiteWorkspace); - const { tags, filterPageMetaByTag } = useTagMetas( - currentWorkspace.blockSuiteWorkspace, - pageMetas - ); + const { tags, filterPageMetaByTag } = useTagMetas(pageMetas); const tagPageMetas = useMemo(() => { if (tagId) { return filterPageMetaByTag(tagId); @@ -39,16 +41,27 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => { return ( <> - - {tagPageMetas.length > 0 ? ( - - ) : ( - } - blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace} - /> - )} + + + + +
+ {tagPageMetas.length > 0 ? ( + + ) : ( + + } + blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace} + /> + )} +
+
); }; diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index a2b60c2fb6..afdab4139f 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -568,7 +568,7 @@ "com.affine.collection.removePage.success": "Removed successfully", "com.affine.collection.toolbar.selected": "<0>{{count}} selected", "com.affine.collection.toolbar.selected_one": "<0>{{count}} collection selected", - "com.affine.collection.toolbar.selected_others": "<0>{{count}} collection(s) selected", + "com.affine.collection.toolbar.selected_other": "<0>{{count}} collection(s) selected", "com.affine.collectionBar.backToAll": "Back to all", "com.affine.collections.empty.message": "No collections", "com.affine.collections.empty.new-collection-button": "New Collection", @@ -804,7 +804,7 @@ "com.affine.page.group-header.select-all": "Select All", "com.affine.page.toolbar.selected": "<0>{{count}} selected", "com.affine.page.toolbar.selected_one": "<0>{{count}} doc selected", - "com.affine.page.toolbar.selected_others": "<0>{{count}} doc(s) selected", + "com.affine.page.toolbar.selected_other": "<0>{{count}} doc(s) selected", "com.affine.pageMode": "Doc Mode", "com.affine.pageMode.all": "all", "com.affine.pageMode.edgeless": "Edgeless", @@ -1051,7 +1051,7 @@ "com.affine.storage.used.hint": "Space used", "com.affine.tag.toolbar.selected": "<0>{{count}} selected", "com.affine.tag.toolbar.selected_one": "<0>{{count}} tag selected", - "com.affine.tag.toolbar.selected_others": "<0>{{count}} tag(s) selected", + "com.affine.tag.toolbar.selected_other": "<0>{{count}} tag(s) selected", "com.affine.themeSettings.dark": "Dark", "com.affine.themeSettings.light": "Light", "com.affine.themeSettings.system": "System", @@ -1146,5 +1146,19 @@ "com.affine.workbench.split-view-menu.close": "Close", "com.affine.workbench.split-view-menu.move-left": "Move Left", "com.affine.workbench.split-view-menu.move-right": "Move Right", - "com.affine.workbench.split-view-menu.full-screen": "Full Screen" + "com.affine.workbench.split-view-menu.full-screen": "Full Screen", + "com.affine.tags.empty.new-tag-button": "New Tag", + "com.affine.tags.create-tag.placeholder": "Type tag name here...", + "com.affine.tags.create-tag.toast.success": "Tag created", + "com.affine.tags.create-tag.toast.exist": "Tag already exists", + "com.affine.tags.edit-tag.toast.success": "Tag updated", + "com.affine.tags.count": "{{count}} doc", + "com.affine.tags.count_zero": "{{count}} doc", + "com.affine.tags.count_one": "{{count}} doc", + "com.affine.tags.count_other": "{{count}} docs", + "com.affine.tags.delete-tags.toast": "Tag deleted", + "com.affine.delete-tags.count": "{{count}} tag deleted", + "com.affine.delete-tags.count_one": "{{count}} tag deleted", + "com.affine.delete-tags.count_other": "{{count}} tags deleted", + "com.affine.search-tags.placeholder": "Type here ..." } diff --git a/tests/affine-local/e2e/all-page.spec.ts b/tests/affine-local/e2e/all-page.spec.ts index 16b92fe110..22b95169ce 100644 --- a/tests/affine-local/e2e/all-page.spec.ts +++ b/tests/affine-local/e2e/all-page.spec.ts @@ -210,7 +210,7 @@ test('select two pages and delete', async ({ page }) => { // the floating popover should appear await expect(page.locator('[data-testid="floating-toolbar"]')).toBeVisible(); await expect(page.locator('[data-testid="floating-toolbar"]')).toHaveText( - '2 selected' + '2 doc(s) selected' ); // click delete button @@ -253,6 +253,6 @@ test('select a group of items by clicking "Select All" in group header', async ( // check the selected count is equal to the one displayed in the floating toolbar await expect(page.locator('[data-testid="floating-toolbar"]')).toHaveText( - `${selectedItemCount} selected` + `${selectedItemCount} doc(s) selected` ); });