mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-11 23:26:30 +08:00
feat: allow collections to be added to favorites (#6288)
This commit is contained in:
@@ -40,13 +40,12 @@ export const root = style({
|
||||
paddingLeft: '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':
|
||||
{
|
||||
width: 'calc(100% + 8px)',
|
||||
transform: 'translateX(-8px)',
|
||||
paddingLeft: '20px',
|
||||
paddingRight: '12px',
|
||||
},
|
||||
'&[data-collapsible="false"]:is([data-active="true"], :hover)': {
|
||||
width: 'calc(100% + 8px)',
|
||||
transform: 'translateX(-8px)',
|
||||
paddingLeft: '20px',
|
||||
paddingRight: '12px',
|
||||
},
|
||||
[`${linkItemRoot}:first-of-type &`]: {
|
||||
marginTop: '0px',
|
||||
},
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { Button, FlexWrapper, Menu } from '@affine/component';
|
||||
import type {
|
||||
Collection,
|
||||
DeleteCollectionInfo,
|
||||
Filter,
|
||||
PropertiesMeta,
|
||||
} from '@affine/env/filter';
|
||||
import type { Collection, Filter, PropertiesMeta } from '@affine/env/filter';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { FilterIcon } from '@blocksuite/icons';
|
||||
|
||||
@@ -16,20 +11,14 @@ import type { AllPageListConfig } from './edit-collection/edit-collection';
|
||||
export const CollectionPageListOperationsMenu = ({
|
||||
collection,
|
||||
allPageListConfig,
|
||||
userInfo,
|
||||
}: {
|
||||
collection: Collection;
|
||||
allPageListConfig: AllPageListConfig;
|
||||
userInfo: DeleteCollectionInfo;
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
return (
|
||||
<FlexWrapper alignItems="center">
|
||||
<CollectionOperations
|
||||
info={userInfo}
|
||||
collection={collection}
|
||||
config={allPageListConfig}
|
||||
>
|
||||
<CollectionOperations collection={collection} config={allPageListConfig}>
|
||||
<Button
|
||||
className={styles.filterMenuTrigger}
|
||||
type="default"
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import type { MenuItemProps } from '@affine/component';
|
||||
import { Menu, MenuIcon, MenuItem } from '@affine/component';
|
||||
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 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 {
|
||||
DeleteIcon,
|
||||
EditIcon,
|
||||
FavoritedIcon,
|
||||
FavoriteIcon,
|
||||
FilterIcon,
|
||||
SplitViewIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import type { PropsWithChildren, ReactElement } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
@@ -25,15 +29,14 @@ import {
|
||||
export const CollectionOperations = ({
|
||||
collection,
|
||||
config,
|
||||
info,
|
||||
openRenameModal,
|
||||
children,
|
||||
}: PropsWithChildren<{
|
||||
info: DeleteCollectionInfo;
|
||||
collection: Collection;
|
||||
config: AllPageListConfig;
|
||||
openRenameModal?: () => void;
|
||||
}>) => {
|
||||
const deleteInfo = useDeleteCollectionInfo();
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
const service = useService(CollectionService);
|
||||
const workbench = useService(Workbench);
|
||||
@@ -76,6 +79,19 @@ export const CollectionOperations = ({
|
||||
workbench.openCollection(collection.id, { at: 'tail' });
|
||||
}, [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<
|
||||
Array<
|
||||
| {
|
||||
@@ -109,6 +125,21 @@ export const CollectionOperations = ({
|
||||
name: t['com.affine.collection.menu.edit'](),
|
||||
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
|
||||
? [
|
||||
{
|
||||
@@ -133,7 +164,7 @@ export const CollectionOperations = ({
|
||||
),
|
||||
name: t['Delete'](),
|
||||
click: () => {
|
||||
service.deleteCollection(info, collection.id);
|
||||
service.deleteCollection(deleteInfo, collection.id);
|
||||
},
|
||||
type: 'danger',
|
||||
},
|
||||
@@ -142,10 +173,12 @@ export const CollectionOperations = ({
|
||||
t,
|
||||
showEditName,
|
||||
showEdit,
|
||||
favorite,
|
||||
onToggleFavoritePage,
|
||||
appSettings.enableMultiView,
|
||||
openCollectionSplitView,
|
||||
service,
|
||||
info,
|
||||
deleteInfo,
|
||||
collection.id,
|
||||
]
|
||||
);
|
||||
|
||||
+14
-14
@@ -8,10 +8,10 @@ import {
|
||||
} from '@affine/core/components/page-list';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
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 { 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 * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
@@ -27,17 +27,16 @@ import type { CollectionsListProps } from '../index';
|
||||
import { Page } from './page';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
const CollectionRenderer = ({
|
||||
export const CollectionSidebarNavItem = ({
|
||||
collection,
|
||||
pages,
|
||||
docCollection,
|
||||
info,
|
||||
className,
|
||||
}: {
|
||||
collection: Collection;
|
||||
pages: DocMeta[];
|
||||
docCollection: DocCollection;
|
||||
info: DeleteCollectionInfo;
|
||||
className?: string;
|
||||
}) => {
|
||||
const pages = useBlockSuiteDocMeta(docCollection);
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const [open, setOpen] = useState(false);
|
||||
const collectionService = useService(CollectionService);
|
||||
@@ -112,7 +111,12 @@ const CollectionRenderer = ({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Collapsible.Root open={!collapsed} ref={setNodeRef}>
|
||||
<Collapsible.Root
|
||||
open={!collapsed}
|
||||
ref={setNodeRef}
|
||||
data-testid="collection-"
|
||||
className={className}
|
||||
>
|
||||
<SidebarMenuLinkItem
|
||||
data-testid="collection-item"
|
||||
data-type="collection-list-item"
|
||||
@@ -127,7 +131,6 @@ const CollectionRenderer = ({
|
||||
style={{ display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
<CollectionOperations
|
||||
info={info}
|
||||
collection={collection}
|
||||
config={config}
|
||||
openRenameModal={handleOpen}
|
||||
@@ -174,12 +177,11 @@ const CollectionRenderer = ({
|
||||
};
|
||||
export const CollectionsList = ({
|
||||
docCollection: workspace,
|
||||
info,
|
||||
onCreate,
|
||||
}: CollectionsListProps) => {
|
||||
const metas = useBlockSuiteDocMeta(workspace);
|
||||
const collections = useLiveData(useService(CollectionService).collections$);
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
if (collections.length === 0) {
|
||||
return (
|
||||
<div className={styles.emptyCollectionWrapper}>
|
||||
@@ -204,11 +206,9 @@ export const CollectionsList = ({
|
||||
<div data-testid="collections" className={styles.wrapper}>
|
||||
{collections.map(view => {
|
||||
return (
|
||||
<CollectionRenderer
|
||||
info={info}
|
||||
<CollectionSidebarNavItem
|
||||
key={view.id}
|
||||
collection={view}
|
||||
pages={metas}
|
||||
docCollection={workspace}
|
||||
/>
|
||||
);
|
||||
|
||||
+41
-21
@@ -1,4 +1,5 @@
|
||||
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 type { DocMeta } from '@blocksuite/store';
|
||||
import { useDroppable } from '@dnd-kit/core';
|
||||
@@ -6,6 +7,7 @@ import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { getDropItemId } from '../../../../hooks/affine/use-sidebar-drag';
|
||||
import { CollectionSidebarNavItem } from '../collections';
|
||||
import type { FavoriteListProps } from '../index';
|
||||
import EmptyItem from './empty-item';
|
||||
import { FavouritePage } from './favourite-page';
|
||||
@@ -18,18 +20,10 @@ export const FavoriteList = ({
|
||||
}: FavoriteListProps) => {
|
||||
const metas = useBlockSuiteDocMeta(workspace);
|
||||
const favAdapter = useService(FavoriteItemsAdapter);
|
||||
const collections = useLiveData(useService(CollectionService).collections$);
|
||||
const dropItemId = getDropItemId('favorites');
|
||||
|
||||
const favourites = useLiveData(
|
||||
favAdapter.favorites$.map(favourites =>
|
||||
favourites.filter(fav => {
|
||||
const meta = metas.find(m => m.id === fav.id);
|
||||
return meta && !meta.trash;
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const metaMapping = useMemo(
|
||||
const docMetaMapping = useMemo(
|
||||
() =>
|
||||
metas.reduce(
|
||||
(acc, meta) => {
|
||||
@@ -41,6 +35,17 @@ export const FavoriteList = ({
|
||||
[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({
|
||||
id: dropItemId,
|
||||
});
|
||||
@@ -52,17 +57,32 @@ export const FavoriteList = ({
|
||||
ref={setNodeRef}
|
||||
data-over={isOver}
|
||||
>
|
||||
{favourites.map((pageMeta, index) => {
|
||||
return (
|
||||
<FavouritePage
|
||||
key={`${pageMeta}-${index}`}
|
||||
metaMapping={metaMapping}
|
||||
pageId={pageMeta.id}
|
||||
// memo?
|
||||
parentIds={emptyPageIdSet}
|
||||
docCollection={workspace}
|
||||
/>
|
||||
);
|
||||
{favourites.map(item => {
|
||||
if (item.type === 'collection') {
|
||||
const collection = collections.find(c => c.id === item.id);
|
||||
if (collection) {
|
||||
return (
|
||||
<CollectionSidebarNavItem
|
||||
key={item.id}
|
||||
className={styles.favItemWrapper}
|
||||
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 />}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { DeleteCollectionInfo } from '@affine/env/filter';
|
||||
import type { DocCollection } from '@blocksuite/store';
|
||||
|
||||
export type FavoriteListProps = {
|
||||
@@ -7,6 +6,5 @@ export type FavoriteListProps = {
|
||||
|
||||
export type CollectionsListProps = {
|
||||
docCollection: DocCollection;
|
||||
info: DeleteCollectionInfo;
|
||||
onCreate?: () => void;
|
||||
};
|
||||
|
||||
@@ -14,7 +14,6 @@ import type { HTMLAttributes, ReactElement } from 'react';
|
||||
import { forwardRef, useCallback, useEffect } from 'react';
|
||||
|
||||
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 { useTrashModalHelper } from '../../hooks/affine/use-trash-modal-helper';
|
||||
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
|
||||
@@ -163,7 +162,6 @@ export const RootAppSidebar = ({
|
||||
console.error(err);
|
||||
});
|
||||
}, [docCollection.id, collection, navigateHelper, open]);
|
||||
const userInfo = useDeleteCollectionInfo();
|
||||
|
||||
const allPageActive = currentPath === '/all';
|
||||
|
||||
@@ -226,7 +224,6 @@ export const RootAppSidebar = ({
|
||||
</CategoryDivider>
|
||||
<CollectionsList
|
||||
docCollection={docCollection}
|
||||
info={userInfo}
|
||||
onCreate={handleCreateCollection}
|
||||
/>
|
||||
<CategoryDivider label={t['com.affine.rootAppSidebar.others']()} />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { DeleteCollectionInfo } from '@affine/env/filter';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useSession } from './use-current-user';
|
||||
@@ -5,7 +6,7 @@ import { useSession } from './use-current-user';
|
||||
export const useDeleteCollectionInfo = () => {
|
||||
const { user } = useSession();
|
||||
|
||||
return useMemo(
|
||||
return useMemo<DeleteCollectionInfo | null>(
|
||||
() => (user ? { userName: user.name, userId: user.id } : null),
|
||||
[user]
|
||||
);
|
||||
|
||||
@@ -168,8 +168,22 @@ export class FavoriteItemsAdapter {
|
||||
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() {
|
||||
return Object.values(this.adapter.favorites ?? {});
|
||||
return Object.entries(this.adapter.favorites ?? {})
|
||||
.filter(([k]) => k.includes(':'))
|
||||
.map(([, v]) => v);
|
||||
}
|
||||
|
||||
get favorites() {
|
||||
|
||||
Reference in New Issue
Block a user