feat: allow collections to be added to favorites (#6288)

This commit is contained in:
pengx17
2024-03-29 04:04:17 +00:00
parent 5490944d04
commit 35af526eb2
9 changed files with 119 additions and 68 deletions

View File

@@ -40,13 +40,12 @@ export const root = style({
paddingLeft: '4px', paddingLeft: '4px',
paddingRight: '4px', paddingRight: '4px',
}, },
'&[data-type="collection-list-item"][data-collapsible="false"][data-active="true"],&[data-type="reference-page"][data-collapsible="false"][data-active="true"], &[data-type="reference-page"][data-collapsible="false"]:hover, &[data-type="collection-list-item"][data-collapsible="false"]:hover': '&[data-collapsible="false"]:is([data-active="true"], :hover)': {
{ width: 'calc(100% + 8px)',
width: 'calc(100% + 8px)', transform: 'translateX(-8px)',
transform: 'translateX(-8px)', paddingLeft: '20px',
paddingLeft: '20px', paddingRight: '12px',
paddingRight: '12px', },
},
[`${linkItemRoot}:first-of-type &`]: { [`${linkItemRoot}:first-of-type &`]: {
marginTop: '0px', marginTop: '0px',
}, },

View File

@@ -1,10 +1,5 @@
import { Button, FlexWrapper, Menu } from '@affine/component'; import { Button, FlexWrapper, Menu } from '@affine/component';
import type { import type { Collection, Filter, PropertiesMeta } from '@affine/env/filter';
Collection,
DeleteCollectionInfo,
Filter,
PropertiesMeta,
} from '@affine/env/filter';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { FilterIcon } from '@blocksuite/icons'; import { FilterIcon } from '@blocksuite/icons';
@@ -16,20 +11,14 @@ import type { AllPageListConfig } from './edit-collection/edit-collection';
export const CollectionPageListOperationsMenu = ({ export const CollectionPageListOperationsMenu = ({
collection, collection,
allPageListConfig, allPageListConfig,
userInfo,
}: { }: {
collection: Collection; collection: Collection;
allPageListConfig: AllPageListConfig; allPageListConfig: AllPageListConfig;
userInfo: DeleteCollectionInfo;
}) => { }) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
return ( return (
<FlexWrapper alignItems="center"> <FlexWrapper alignItems="center">
<CollectionOperations <CollectionOperations collection={collection} config={allPageListConfig}>
info={userInfo}
collection={collection}
config={allPageListConfig}
>
<Button <Button
className={styles.filterMenuTrigger} className={styles.filterMenuTrigger}
type="default" type="default"

View File

@@ -1,16 +1,20 @@
import type { MenuItemProps } from '@affine/component'; import type { MenuItemProps } from '@affine/component';
import { Menu, MenuIcon, MenuItem } from '@affine/component'; import { Menu, MenuIcon, MenuItem } from '@affine/component';
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper'; import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
import { useDeleteCollectionInfo } from '@affine/core/hooks/affine/use-delete-collection-info';
import { Workbench } from '@affine/core/modules/workbench'; import { Workbench } from '@affine/core/modules/workbench';
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter'; import { FavoriteItemsAdapter } from '@affine/core/modules/workspace';
import type { Collection } from '@affine/env/filter';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { import {
DeleteIcon, DeleteIcon,
EditIcon, EditIcon,
FavoritedIcon,
FavoriteIcon,
FilterIcon, FilterIcon,
SplitViewIcon, SplitViewIcon,
} from '@blocksuite/icons'; } from '@blocksuite/icons';
import { useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import type { PropsWithChildren, ReactElement } from 'react'; import type { PropsWithChildren, ReactElement } from 'react';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
@@ -25,15 +29,14 @@ import {
export const CollectionOperations = ({ export const CollectionOperations = ({
collection, collection,
config, config,
info,
openRenameModal, openRenameModal,
children, children,
}: PropsWithChildren<{ }: PropsWithChildren<{
info: DeleteCollectionInfo;
collection: Collection; collection: Collection;
config: AllPageListConfig; config: AllPageListConfig;
openRenameModal?: () => void; openRenameModal?: () => void;
}>) => { }>) => {
const deleteInfo = useDeleteCollectionInfo();
const { appSettings } = useAppSettingHelper(); const { appSettings } = useAppSettingHelper();
const service = useService(CollectionService); const service = useService(CollectionService);
const workbench = useService(Workbench); const workbench = useService(Workbench);
@@ -76,6 +79,19 @@ export const CollectionOperations = ({
workbench.openCollection(collection.id, { at: 'tail' }); workbench.openCollection(collection.id, { at: 'tail' });
}, [collection.id, workbench]); }, [collection.id, workbench]);
const favAdapter = useService(FavoriteItemsAdapter);
const onToggleFavoritePage = useCallback(() => {
favAdapter.toggle(collection.id, 'collection');
}, [favAdapter, collection.id]);
const favorite = useLiveData(
useMemo(
() => favAdapter.isFavorite$(collection.id, 'collection'),
[collection.id, favAdapter]
)
);
const actions = useMemo< const actions = useMemo<
Array< Array<
| { | {
@@ -109,6 +125,21 @@ export const CollectionOperations = ({
name: t['com.affine.collection.menu.edit'](), name: t['com.affine.collection.menu.edit'](),
click: showEdit, click: showEdit,
}, },
{
icon: (
<MenuIcon>
{favorite ? (
<FavoritedIcon style={{ color: 'var(--affine-primary-color)' }} />
) : (
<FavoriteIcon />
)}
</MenuIcon>
),
name: favorite
? t['com.affine.favoritePageOperation.remove']()
: t['com.affine.favoritePageOperation.add'](),
click: onToggleFavoritePage,
},
...(appSettings.enableMultiView ...(appSettings.enableMultiView
? [ ? [
{ {
@@ -133,7 +164,7 @@ export const CollectionOperations = ({
), ),
name: t['Delete'](), name: t['Delete'](),
click: () => { click: () => {
service.deleteCollection(info, collection.id); service.deleteCollection(deleteInfo, collection.id);
}, },
type: 'danger', type: 'danger',
}, },
@@ -142,10 +173,12 @@ export const CollectionOperations = ({
t, t,
showEditName, showEditName,
showEdit, showEdit,
favorite,
onToggleFavoritePage,
appSettings.enableMultiView, appSettings.enableMultiView,
openCollectionSplitView, openCollectionSplitView,
service, service,
info, deleteInfo,
collection.id, collection.id,
] ]
); );

View File

@@ -8,10 +8,10 @@ import {
} from '@affine/core/components/page-list'; } from '@affine/core/components/page-list';
import { CollectionService } from '@affine/core/modules/collection'; import { CollectionService } from '@affine/core/modules/collection';
import { FavoriteItemsAdapter } from '@affine/core/modules/workspace'; import { FavoriteItemsAdapter } from '@affine/core/modules/workspace';
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter'; import type { Collection } from '@affine/env/filter';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { MoreHorizontalIcon, ViewLayersIcon } from '@blocksuite/icons'; import { MoreHorizontalIcon, ViewLayersIcon } from '@blocksuite/icons';
import type { DocCollection, DocMeta } from '@blocksuite/store'; import type { DocCollection } from '@blocksuite/store';
import { useDroppable } from '@dnd-kit/core'; import { useDroppable } from '@dnd-kit/core';
import * as Collapsible from '@radix-ui/react-collapsible'; import * as Collapsible from '@radix-ui/react-collapsible';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
@@ -27,17 +27,16 @@ import type { CollectionsListProps } from '../index';
import { Page } from './page'; import { Page } from './page';
import * as styles from './styles.css'; import * as styles from './styles.css';
const CollectionRenderer = ({ export const CollectionSidebarNavItem = ({
collection, collection,
pages,
docCollection, docCollection,
info, className,
}: { }: {
collection: Collection; collection: Collection;
pages: DocMeta[];
docCollection: DocCollection; docCollection: DocCollection;
info: DeleteCollectionInfo; className?: string;
}) => { }) => {
const pages = useBlockSuiteDocMeta(docCollection);
const [collapsed, setCollapsed] = useState(true); const [collapsed, setCollapsed] = useState(true);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const collectionService = useService(CollectionService); const collectionService = useService(CollectionService);
@@ -112,7 +111,12 @@ const CollectionRenderer = ({
}, []); }, []);
return ( return (
<Collapsible.Root open={!collapsed} ref={setNodeRef}> <Collapsible.Root
open={!collapsed}
ref={setNodeRef}
data-testid="collection-"
className={className}
>
<SidebarMenuLinkItem <SidebarMenuLinkItem
data-testid="collection-item" data-testid="collection-item"
data-type="collection-list-item" data-type="collection-list-item"
@@ -127,7 +131,6 @@ const CollectionRenderer = ({
style={{ display: 'flex', alignItems: 'center' }} style={{ display: 'flex', alignItems: 'center' }}
> >
<CollectionOperations <CollectionOperations
info={info}
collection={collection} collection={collection}
config={config} config={config}
openRenameModal={handleOpen} openRenameModal={handleOpen}
@@ -174,12 +177,11 @@ const CollectionRenderer = ({
}; };
export const CollectionsList = ({ export const CollectionsList = ({
docCollection: workspace, docCollection: workspace,
info,
onCreate, onCreate,
}: CollectionsListProps) => { }: CollectionsListProps) => {
const metas = useBlockSuiteDocMeta(workspace);
const collections = useLiveData(useService(CollectionService).collections$); const collections = useLiveData(useService(CollectionService).collections$);
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
if (collections.length === 0) { if (collections.length === 0) {
return ( return (
<div className={styles.emptyCollectionWrapper}> <div className={styles.emptyCollectionWrapper}>
@@ -204,11 +206,9 @@ export const CollectionsList = ({
<div data-testid="collections" className={styles.wrapper}> <div data-testid="collections" className={styles.wrapper}>
{collections.map(view => { {collections.map(view => {
return ( return (
<CollectionRenderer <CollectionSidebarNavItem
info={info}
key={view.id} key={view.id}
collection={view} collection={view}
pages={metas}
docCollection={workspace} docCollection={workspace}
/> />
); );

View File

@@ -1,4 +1,5 @@
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta'; import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { CollectionService } from '@affine/core/modules/collection';
import { FavoriteItemsAdapter } from '@affine/core/modules/workspace'; import { FavoriteItemsAdapter } from '@affine/core/modules/workspace';
import type { DocMeta } from '@blocksuite/store'; import type { DocMeta } from '@blocksuite/store';
import { useDroppable } from '@dnd-kit/core'; import { useDroppable } from '@dnd-kit/core';
@@ -6,6 +7,7 @@ import { useLiveData, useService } from '@toeverything/infra';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { getDropItemId } from '../../../../hooks/affine/use-sidebar-drag'; import { getDropItemId } from '../../../../hooks/affine/use-sidebar-drag';
import { CollectionSidebarNavItem } from '../collections';
import type { FavoriteListProps } from '../index'; import type { FavoriteListProps } from '../index';
import EmptyItem from './empty-item'; import EmptyItem from './empty-item';
import { FavouritePage } from './favourite-page'; import { FavouritePage } from './favourite-page';
@@ -18,18 +20,10 @@ export const FavoriteList = ({
}: FavoriteListProps) => { }: FavoriteListProps) => {
const metas = useBlockSuiteDocMeta(workspace); const metas = useBlockSuiteDocMeta(workspace);
const favAdapter = useService(FavoriteItemsAdapter); const favAdapter = useService(FavoriteItemsAdapter);
const collections = useLiveData(useService(CollectionService).collections$);
const dropItemId = getDropItemId('favorites'); const dropItemId = getDropItemId('favorites');
const favourites = useLiveData( const docMetaMapping = useMemo(
favAdapter.favorites$.map(favourites =>
favourites.filter(fav => {
const meta = metas.find(m => m.id === fav.id);
return meta && !meta.trash;
})
)
);
const metaMapping = useMemo(
() => () =>
metas.reduce( metas.reduce(
(acc, meta) => { (acc, meta) => {
@@ -41,6 +35,17 @@ export const FavoriteList = ({
[metas] [metas]
); );
const favourites = useLiveData(
favAdapter.orderedFavorites$.map(favs => {
return favs.filter(fav => {
if (fav.type === 'doc') {
return !!docMetaMapping[fav.id] && !docMetaMapping[fav.id].trash;
}
return true;
});
})
);
const { setNodeRef, isOver } = useDroppable({ const { setNodeRef, isOver } = useDroppable({
id: dropItemId, id: dropItemId,
}); });
@@ -52,17 +57,32 @@ export const FavoriteList = ({
ref={setNodeRef} ref={setNodeRef}
data-over={isOver} data-over={isOver}
> >
{favourites.map((pageMeta, index) => { {favourites.map(item => {
return ( if (item.type === 'collection') {
<FavouritePage const collection = collections.find(c => c.id === item.id);
key={`${pageMeta}-${index}`} if (collection) {
metaMapping={metaMapping} return (
pageId={pageMeta.id} <CollectionSidebarNavItem
// memo? key={item.id}
parentIds={emptyPageIdSet} className={styles.favItemWrapper}
docCollection={workspace} docCollection={workspace}
/> collection={collection}
); />
);
}
} else if (item.type === 'doc' && !docMetaMapping[item.id].trash) {
return (
<FavouritePage
key={item.id}
metaMapping={docMetaMapping}
pageId={item.id}
// memo?
parentIds={emptyPageIdSet}
docCollection={workspace}
/>
);
}
return null;
})} })}
{favourites.length === 0 && <EmptyItem />} {favourites.length === 0 && <EmptyItem />}
</div> </div>

View File

@@ -1,4 +1,3 @@
import type { DeleteCollectionInfo } from '@affine/env/filter';
import type { DocCollection } from '@blocksuite/store'; import type { DocCollection } from '@blocksuite/store';
export type FavoriteListProps = { export type FavoriteListProps = {
@@ -7,6 +6,5 @@ export type FavoriteListProps = {
export type CollectionsListProps = { export type CollectionsListProps = {
docCollection: DocCollection; docCollection: DocCollection;
info: DeleteCollectionInfo;
onCreate?: () => void; onCreate?: () => void;
}; };

View File

@@ -14,7 +14,6 @@ import type { HTMLAttributes, ReactElement } from 'react';
import { forwardRef, useCallback, useEffect } from 'react'; import { forwardRef, useCallback, useEffect } from 'react';
import { useAppSettingHelper } from '../../hooks/affine/use-app-setting-helper'; import { useAppSettingHelper } from '../../hooks/affine/use-app-setting-helper';
import { useDeleteCollectionInfo } from '../../hooks/affine/use-delete-collection-info';
import { getDropItemId } from '../../hooks/affine/use-sidebar-drag'; import { getDropItemId } from '../../hooks/affine/use-sidebar-drag';
import { useTrashModalHelper } from '../../hooks/affine/use-trash-modal-helper'; import { useTrashModalHelper } from '../../hooks/affine/use-trash-modal-helper';
import { useNavigateHelper } from '../../hooks/use-navigate-helper'; import { useNavigateHelper } from '../../hooks/use-navigate-helper';
@@ -163,7 +162,6 @@ export const RootAppSidebar = ({
console.error(err); console.error(err);
}); });
}, [docCollection.id, collection, navigateHelper, open]); }, [docCollection.id, collection, navigateHelper, open]);
const userInfo = useDeleteCollectionInfo();
const allPageActive = currentPath === '/all'; const allPageActive = currentPath === '/all';
@@ -226,7 +224,6 @@ export const RootAppSidebar = ({
</CategoryDivider> </CategoryDivider>
<CollectionsList <CollectionsList
docCollection={docCollection} docCollection={docCollection}
info={userInfo}
onCreate={handleCreateCollection} onCreate={handleCreateCollection}
/> />
<CategoryDivider label={t['com.affine.rootAppSidebar.others']()} /> <CategoryDivider label={t['com.affine.rootAppSidebar.others']()} />

View File

@@ -1,3 +1,4 @@
import type { DeleteCollectionInfo } from '@affine/env/filter';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useSession } from './use-current-user'; import { useSession } from './use-current-user';
@@ -5,7 +6,7 @@ import { useSession } from './use-current-user';
export const useDeleteCollectionInfo = () => { export const useDeleteCollectionInfo = () => {
const { user } = useSession(); const { user } = useSession();
return useMemo( return useMemo<DeleteCollectionInfo | null>(
() => (user ? { userName: user.name, userId: user.id } : null), () => (user ? { userName: user.name, userId: user.id } : null),
[user] [user]
); );

View File

@@ -168,8 +168,22 @@ export class FavoriteItemsAdapter {
this.getItems().filter(i => i.value) this.getItems().filter(i => i.value)
); );
orderedFavorites$ = this.adapter.properties$.map(() => {
const seen = new Set<string>();
return this.sorter.getOrderedItems().filter(item => {
const key = FavoriteItemsAdapter.getFavItemKey(item.id, item.type);
if (seen.has(key) || !item.value) {
return null;
}
seen.add(key);
return item;
});
});
getItems() { getItems() {
return Object.values(this.adapter.favorites ?? {}); return Object.entries(this.adapter.favorites ?? {})
.filter(([k]) => k.includes(':'))
.map(([, v]) => v);
} }
get favorites() { get favorites() {