mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-25 14:28:51 +08:00
feat(core): fav item reordering (#6302)
This commit is contained in:
+7
-1
@@ -35,5 +35,11 @@ export const useFavorite = (pageId: string) => {
|
||||
export const FavoriteButton = ({ pageId }: FavoriteButtonProps) => {
|
||||
const { favorite, toggleFavorite } = useFavorite(pageId);
|
||||
|
||||
return <FavoriteTag active={!!favorite} onClick={toggleFavorite} />;
|
||||
return (
|
||||
<FavoriteTag
|
||||
data-testid="pin-button"
|
||||
active={!!favorite}
|
||||
onClick={toggleFavorite}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-14
@@ -21,20 +21,7 @@ export const root = style({
|
||||
},
|
||||
},
|
||||
});
|
||||
export const dragOverlay = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
zIndex: 1001,
|
||||
cursor: 'grabbing',
|
||||
maxWidth: '360px',
|
||||
transition: 'transform 0.2s',
|
||||
willChange: 'transform',
|
||||
selectors: {
|
||||
'&[data-over=true]': {
|
||||
transform: 'scale(0.8)',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const dragPageItemOverlay = style({
|
||||
height: '54px',
|
||||
borderRadius: '10px',
|
||||
|
||||
+11
-4
@@ -1,4 +1,5 @@
|
||||
import { Checkbox } from '@affine/component';
|
||||
import { getDNDId } from '@affine/core/hooks/affine/use-global-dnd-helper';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useDraggable } from '@dnd-kit/core';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
@@ -97,16 +98,22 @@ export const CollectionListItem = (props: CollectionListItemProps) => {
|
||||
/>
|
||||
<ListIconCell icon={props.icon} />
|
||||
</div>
|
||||
<ListTitleCell title={props.title} />
|
||||
</div>
|
||||
);
|
||||
}, [props.icon, props.onSelectedChange, props.selectable, props.selected]);
|
||||
}, [
|
||||
props.icon,
|
||||
props.onSelectedChange,
|
||||
props.selectable,
|
||||
props.selected,
|
||||
props.title,
|
||||
]);
|
||||
|
||||
// TODO: use getDropItemId
|
||||
const { setNodeRef, attributes, listeners, isDragging } = useDraggable({
|
||||
id: 'collection-list-item-title-' + props.collectionId,
|
||||
id: getDNDId('collection-list', 'collection', props.collectionId),
|
||||
data: {
|
||||
pageId: props.collectionId,
|
||||
pageTitle: collectionTitleElement,
|
||||
preview: collectionTitleElement,
|
||||
} satisfies DraggableTitleCellData,
|
||||
disabled: !props.draggable,
|
||||
});
|
||||
|
||||
+1
-1
@@ -106,7 +106,7 @@ export const VirtualizedCollectionList = ({
|
||||
<VirtualizedList
|
||||
ref={listRef}
|
||||
selectable="toggle"
|
||||
draggable={false}
|
||||
draggable
|
||||
atTopThreshold={80}
|
||||
atTopStateChange={setHideHeaderCreateNewCollection}
|
||||
onSelectionActiveChange={setShowFloatingToolbar}
|
||||
|
||||
@@ -21,20 +21,7 @@ export const root = style({
|
||||
},
|
||||
},
|
||||
});
|
||||
export const dragOverlay = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
zIndex: 1001,
|
||||
cursor: 'grabbing',
|
||||
maxWidth: '360px',
|
||||
transition: 'transform 0.2s',
|
||||
willChange: 'transform',
|
||||
selectors: {
|
||||
'&[data-over=true]': {
|
||||
transform: 'scale(0.8)',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const dragPageItemOverlay = style({
|
||||
height: '54px',
|
||||
borderRadius: '10px',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Checkbox } from '@affine/component';
|
||||
import { getDNDId } from '@affine/core/hooks/affine/use-global-dnd-helper';
|
||||
import { TagService } from '@affine/core/modules/tag';
|
||||
import { useDraggable } from '@dnd-kit/core';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
@@ -152,10 +153,9 @@ export const PageListItem = (props: PageListItemProps) => {
|
||||
|
||||
// TODO: use getDropItemId
|
||||
const { setNodeRef, attributes, listeners, isDragging } = useDraggable({
|
||||
id: 'page-list-item-title-' + props.pageId,
|
||||
id: getDNDId('doc-list', 'doc', props.pageId),
|
||||
data: {
|
||||
pageId: props.pageId,
|
||||
pageTitle: pageTitleElement,
|
||||
preview: pageTitleElement,
|
||||
} satisfies DraggableTitleCellData,
|
||||
disabled: !props.draggable,
|
||||
});
|
||||
@@ -279,16 +279,3 @@ function PageListItemWrapper({
|
||||
return <div {...commonProps}>{children}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export const PageListDragOverlay = ({
|
||||
children,
|
||||
over,
|
||||
}: PropsWithChildren<{
|
||||
over?: boolean;
|
||||
}>) => {
|
||||
return (
|
||||
<div data-over={over} className={styles.dragOverlay}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -21,20 +21,6 @@ export const root = style({
|
||||
},
|
||||
},
|
||||
});
|
||||
export const dragOverlay = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
zIndex: 1001,
|
||||
cursor: 'grabbing',
|
||||
maxWidth: '360px',
|
||||
transition: 'transform 0.2s',
|
||||
willChange: 'transform',
|
||||
selectors: {
|
||||
'&[data-over=true]': {
|
||||
transform: 'scale(0.8)',
|
||||
},
|
||||
},
|
||||
});
|
||||
export const dragPageItemOverlay = style({
|
||||
height: '54px',
|
||||
borderRadius: '10px',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Checkbox } from '@affine/component';
|
||||
import { getDNDId } from '@affine/core/hooks/affine/use-global-dnd-helper';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useDraggable } from '@dnd-kit/core';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
@@ -99,10 +100,9 @@ export const TagListItem = (props: TagListItemProps) => {
|
||||
|
||||
// TODO: use getDropItemId
|
||||
const { setNodeRef, attributes, listeners, isDragging } = useDraggable({
|
||||
id: 'tag-list-item-title-' + props.tagId,
|
||||
id: getDNDId('tag-list', 'tag', props.tagId),
|
||||
data: {
|
||||
pageId: props.tagId,
|
||||
pageTitle: tagTitleElement,
|
||||
preview: tagTitleElement,
|
||||
} satisfies DraggableTitleCellData,
|
||||
disabled: !props.draggable,
|
||||
});
|
||||
|
||||
@@ -141,8 +141,7 @@ type MakeRecord<T> = {
|
||||
export type MetaRecord<T> = MakeRecord<T>;
|
||||
|
||||
export type DraggableTitleCellData = {
|
||||
pageId: string;
|
||||
pageTitle: ReactNode;
|
||||
preview: ReactNode;
|
||||
};
|
||||
|
||||
export type HeaderColDef = {
|
||||
|
||||
+63
-21
@@ -6,34 +6,49 @@ import {
|
||||
filterPage,
|
||||
stopPropagation,
|
||||
} from '@affine/core/components/page-list';
|
||||
import {
|
||||
type DNDIdentifier,
|
||||
getDNDId,
|
||||
parseDNDId,
|
||||
resolveDragEndIntent,
|
||||
} from '@affine/core/hooks/affine/use-global-dnd-helper';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/workspace';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { MoreHorizontalIcon, ViewLayersIcon } from '@blocksuite/icons';
|
||||
import type { DocCollection } from '@blocksuite/store';
|
||||
import { useDroppable } from '@dnd-kit/core';
|
||||
import { type AnimateLayoutChanges, useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { useAllPageListConfig } from '../../../../hooks/affine/use-all-page-list-config';
|
||||
import { getDropItemId } from '../../../../hooks/affine/use-sidebar-drag';
|
||||
import { useBlockSuiteDocMeta } from '../../../../hooks/use-block-suite-page-meta';
|
||||
import { Workbench } from '../../../../modules/workbench';
|
||||
import { WorkbenchLink } from '../../../../modules/workbench/view/workbench-link';
|
||||
import { MenuLinkItem as SidebarMenuLinkItem } from '../../../app-sidebar';
|
||||
import { DragMenuItemOverlay } from '../components/drag-menu-item-overlay';
|
||||
import * as draggableMenuItemStyles from '../components/draggable-menu-item.css';
|
||||
import type { CollectionsListProps } from '../index';
|
||||
import { Page } from './page';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
const animateLayoutChanges: AnimateLayoutChanges = ({
|
||||
isSorting,
|
||||
wasDragging,
|
||||
}) => (isSorting || wasDragging ? false : true);
|
||||
|
||||
export const CollectionSidebarNavItem = ({
|
||||
collection,
|
||||
docCollection,
|
||||
className,
|
||||
dndId,
|
||||
}: {
|
||||
collection: Collection;
|
||||
docCollection: DocCollection;
|
||||
dndId: DNDIdentifier;
|
||||
className?: string;
|
||||
}) => {
|
||||
const pages = useBlockSuiteDocMeta(docCollection);
|
||||
@@ -42,37 +57,50 @@ export const CollectionSidebarNavItem = ({
|
||||
const collectionService = useService(CollectionService);
|
||||
const favAdapter = useService(FavoriteItemsAdapter);
|
||||
const t = useAFFiNEI18N();
|
||||
const dragItemId = getDropItemId('collections', collection.id);
|
||||
|
||||
const favourites = useLiveData(favAdapter.favorites$);
|
||||
|
||||
const removeFromAllowList = useCallback(
|
||||
(id: string) => {
|
||||
collectionService.updateCollection(collection.id, () => ({
|
||||
...collection,
|
||||
allowList: collection.allowList?.filter(v => v !== id),
|
||||
}));
|
||||
|
||||
collectionService.deletePageFromCollection(collection.id, id);
|
||||
toast(t['com.affine.collection.removePage.success']());
|
||||
},
|
||||
[collection, collectionService, t]
|
||||
);
|
||||
|
||||
const { setNodeRef, isOver } = useDroppable({
|
||||
id: dragItemId,
|
||||
const overlayPreview = useMemo(() => {
|
||||
return (
|
||||
<DragMenuItemOverlay icon={<ViewLayersIcon />} title={collection.name} />
|
||||
);
|
||||
}, [collection.name]);
|
||||
|
||||
const {
|
||||
setNodeRef,
|
||||
isDragging,
|
||||
attributes,
|
||||
listeners,
|
||||
transform,
|
||||
over,
|
||||
active,
|
||||
transition,
|
||||
} = useSortable({
|
||||
id: dndId,
|
||||
data: {
|
||||
addToCollection: (id: string) => {
|
||||
if (collection.allowList.includes(id)) {
|
||||
toast(t['com.affine.collection.addPage.alreadyExists']());
|
||||
return;
|
||||
} else {
|
||||
toast(t['com.affine.collection.addPage.success']());
|
||||
}
|
||||
collectionService.addPageToCollection(collection.id, id);
|
||||
},
|
||||
preview: overlayPreview,
|
||||
},
|
||||
animateLayoutChanges,
|
||||
});
|
||||
|
||||
const isSorting = parseDNDId(active?.id)?.where === 'sidebar-pin';
|
||||
const dragOverIntent = resolveDragEndIntent(active, over);
|
||||
|
||||
const style = {
|
||||
transform: CSS.Translate.toString(transform),
|
||||
transition: isSorting ? transition : undefined,
|
||||
};
|
||||
|
||||
const isOver = over?.id === dndId && dragOverIntent === 'collection:add';
|
||||
|
||||
const config = useAllPageListConfig();
|
||||
const allPagesMeta = useMemo(
|
||||
() => Object.fromEntries(pages.map(v => [v.id, v])),
|
||||
@@ -113,12 +141,18 @@ export const CollectionSidebarNavItem = ({
|
||||
return (
|
||||
<Collapsible.Root
|
||||
open={!collapsed}
|
||||
ref={setNodeRef}
|
||||
data-testid="collection-"
|
||||
className={className}
|
||||
style={style}
|
||||
ref={setNodeRef}
|
||||
{...attributes}
|
||||
>
|
||||
<SidebarMenuLinkItem
|
||||
{...listeners}
|
||||
data-draggable={true}
|
||||
data-dragging={isDragging}
|
||||
className={draggableMenuItemStyles.draggableMenuItem}
|
||||
data-testid="collection-item"
|
||||
data-collection-id={collection.id}
|
||||
data-type="collection-list-item"
|
||||
onCollapsedChange={setCollapsed}
|
||||
active={isOver || currentPath === path}
|
||||
@@ -161,6 +195,7 @@ export const CollectionSidebarNavItem = ({
|
||||
{pagesToRender.map(page => {
|
||||
return (
|
||||
<Page
|
||||
parentId={dndId}
|
||||
inAllowList={allowList.has(page.id)}
|
||||
removeFromAllowList={removeFromAllowList}
|
||||
allPageMeta={allPagesMeta}
|
||||
@@ -205,11 +240,18 @@ export const CollectionsList = ({
|
||||
return (
|
||||
<div data-testid="collections" className={styles.wrapper}>
|
||||
{collections.map(view => {
|
||||
const dragItemId = getDNDId(
|
||||
'sidebar-collections',
|
||||
'collection',
|
||||
view.id
|
||||
);
|
||||
|
||||
return (
|
||||
<CollectionSidebarNavItem
|
||||
key={view.id}
|
||||
collection={view}
|
||||
docCollection={workspace}
|
||||
dndId={dragItemId}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
+9
-6
@@ -8,7 +8,10 @@ import { PageRecordList, useLiveData, useService } from '@toeverything/infra';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { getDragItemId } from '../../../../hooks/affine/use-sidebar-drag';
|
||||
import {
|
||||
type DNDIdentifier,
|
||||
getDNDId,
|
||||
} from '../../../../hooks/affine/use-global-dnd-helper';
|
||||
import { useNavigateHelper } from '../../../../hooks/use-navigate-helper';
|
||||
import { MenuItem as CollectionItem } from '../../../app-sidebar';
|
||||
import { DragMenuItemOverlay } from '../components/drag-menu-item-overlay';
|
||||
@@ -18,11 +21,13 @@ import * as styles from './styles.css';
|
||||
|
||||
export const Page = ({
|
||||
page,
|
||||
parentId,
|
||||
docCollection,
|
||||
allPageMeta,
|
||||
inAllowList,
|
||||
removeFromAllowList,
|
||||
}: {
|
||||
parentId: DNDIdentifier;
|
||||
page: DocMeta;
|
||||
inAllowList: boolean;
|
||||
removeFromAllowList: (id: string) => void;
|
||||
@@ -38,7 +43,7 @@ export const Page = ({
|
||||
const active = params.pageId === pageId;
|
||||
const pageRecord = useLiveData(useService(PageRecordList).record$(pageId));
|
||||
const pageMode = useLiveData(pageRecord?.mode$);
|
||||
const dragItemId = getDragItemId('collectionPage', pageId);
|
||||
const dragItemId = getDNDId('collection-list', 'doc', pageId, parentId);
|
||||
|
||||
const icon = useMemo(() => {
|
||||
return pageMode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />;
|
||||
@@ -56,15 +61,13 @@ export const Page = ({
|
||||
|
||||
const pageTitle = page.title || t['Untitled']();
|
||||
const pageTitleElement = useMemo(() => {
|
||||
return <DragMenuItemOverlay icon={icon} pageTitle={pageTitle} />;
|
||||
return <DragMenuItemOverlay icon={icon} title={pageTitle} />;
|
||||
}, [icon, pageTitle]);
|
||||
|
||||
const { setNodeRef, attributes, listeners, isDragging } = useDraggable({
|
||||
id: dragItemId,
|
||||
data: {
|
||||
pageId,
|
||||
pageTitle: pageTitleElement,
|
||||
removeFromCollection: () => removeFromAllowList(pageId),
|
||||
preview: pageTitleElement,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+3
@@ -102,6 +102,9 @@ export const collapsibleContent = style({
|
||||
overflow: 'hidden',
|
||||
marginTop: '4px',
|
||||
selectors: {
|
||||
'&[data-hidden="true"]': {
|
||||
display: 'none',
|
||||
},
|
||||
'&[data-state="open"]': {
|
||||
animation: `${slideDown} 0.2s ease-in-out`,
|
||||
},
|
||||
|
||||
+3
-3
@@ -1,16 +1,16 @@
|
||||
import * as styles from '../favorite/styles.css';
|
||||
|
||||
export const DragMenuItemOverlay = ({
|
||||
pageTitle,
|
||||
title,
|
||||
icon,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
pageTitle: React.ReactNode;
|
||||
title: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<div className={styles.dragPageItemOverlay}>
|
||||
{icon}
|
||||
<span>{pageTitle}</span>
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const draggableMenuItem = style({
|
||||
selectors: {
|
||||
'&[data-draggable=true]:before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
left: 0,
|
||||
width: 4,
|
||||
height: 4,
|
||||
transition: 'height 0.2s, opacity 0.2s',
|
||||
backgroundColor: cssVar('placeholderColor'),
|
||||
borderRadius: '2px',
|
||||
opacity: 0,
|
||||
willChange: 'height, opacity',
|
||||
},
|
||||
'&[data-draggable=true]:hover:before': {
|
||||
height: 12,
|
||||
opacity: 1,
|
||||
},
|
||||
'&[data-draggable=true][data-dragging=true]': {
|
||||
backgroundColor: cssVar('hoverColor'),
|
||||
},
|
||||
'&[data-draggable=true][data-dragging=true]:before': {
|
||||
height: 32,
|
||||
width: 2,
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
+3
-3
@@ -14,7 +14,7 @@ export interface ReferencePageProps {
|
||||
docCollection: DocCollection;
|
||||
pageId: string;
|
||||
metaMapping: Record<string, DocMeta>;
|
||||
parentIds: Set<string>;
|
||||
parentIds?: Set<string>;
|
||||
}
|
||||
|
||||
export const ReferencePage = ({
|
||||
@@ -44,7 +44,7 @@ export const ReferencePage = ({
|
||||
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const collapsible = referencesToShow.length > 0;
|
||||
const nestedItem = parentIds.size > 0;
|
||||
const nestedItem = parentIds && parentIds.size > 0;
|
||||
|
||||
const untitled = !metaMapping[pageId]?.title;
|
||||
const pageTitle = metaMapping[pageId]?.title || t['Untitled']();
|
||||
@@ -86,7 +86,7 @@ export const ReferencePage = ({
|
||||
docCollection={docCollection}
|
||||
pageId={ref}
|
||||
metaMapping={metaMapping}
|
||||
parentIds={new Set([...parentIds, pageId])}
|
||||
parentIds={new Set([...(parentIds ?? []), pageId])}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
+80
-37
@@ -1,27 +1,34 @@
|
||||
import { CategoryDivider } from '@affine/core/components/app-sidebar';
|
||||
import {
|
||||
getDNDId,
|
||||
resolveDragEndIntent,
|
||||
} from '@affine/core/hooks/affine/use-global-dnd-helper';
|
||||
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 { WorkspaceFavoriteItem } from '@affine/core/modules/workspace/properties/schema';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { DocMeta } from '@blocksuite/store';
|
||||
import { useDroppable } from '@dnd-kit/core';
|
||||
import { useDndContext, useDroppable } from '@dnd-kit/core';
|
||||
import {
|
||||
SortableContext,
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useMemo } from 'react';
|
||||
import { Fragment, useCallback, useMemo } from 'react';
|
||||
|
||||
import { getDropItemId } from '../../../../hooks/affine/use-sidebar-drag';
|
||||
import { CollectionSidebarNavItem } from '../collections';
|
||||
import type { FavoriteListProps } from '../index';
|
||||
import { AddFavouriteButton } from './add-favourite-button';
|
||||
import EmptyItem from './empty-item';
|
||||
import { FavouritePage } from './favourite-page';
|
||||
import { FavouriteDocSidebarNavItem } from './favourite-nav-item';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
const emptyPageIdSet = new Set<string>();
|
||||
|
||||
export const FavoriteList = ({
|
||||
docCollection: workspace,
|
||||
}: FavoriteListProps) => {
|
||||
const FavoriteListInner = ({ docCollection: workspace }: FavoriteListProps) => {
|
||||
const metas = useBlockSuiteDocMeta(workspace);
|
||||
const favAdapter = useService(FavoriteItemsAdapter);
|
||||
const collections = useLiveData(useService(CollectionService).collections$);
|
||||
const dropItemId = getDropItemId('favorites');
|
||||
const dropItemId = getDNDId('sidebar-pin', 'container', workspace.id);
|
||||
|
||||
const docMetaMapping = useMemo(
|
||||
() =>
|
||||
@@ -46,47 +53,83 @@ export const FavoriteList = ({
|
||||
})
|
||||
);
|
||||
|
||||
const { setNodeRef, isOver } = useDroppable({
|
||||
// disable drop styles when dragging from the pin list
|
||||
const { active } = useDndContext();
|
||||
|
||||
const { setNodeRef, over } = useDroppable({
|
||||
id: dropItemId,
|
||||
});
|
||||
|
||||
const intent = resolveDragEndIntent(active, over);
|
||||
const shouldRenderDragOver = intent === 'pin:add';
|
||||
|
||||
const renderFavItem = useCallback(
|
||||
(item: WorkspaceFavoriteItem) => {
|
||||
if (item.type === 'collection') {
|
||||
const collection = collections.find(c => c.id === item.id);
|
||||
if (collection) {
|
||||
const dragItemId = getDNDId(
|
||||
'sidebar-pin',
|
||||
'collection',
|
||||
collection.id
|
||||
);
|
||||
return (
|
||||
<CollectionSidebarNavItem
|
||||
dndId={dragItemId}
|
||||
className={styles.favItemWrapper}
|
||||
docCollection={workspace}
|
||||
collection={collection}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else if (item.type === 'doc' && !docMetaMapping[item.id].trash) {
|
||||
return (
|
||||
<FavouriteDocSidebarNavItem
|
||||
metaMapping={docMetaMapping}
|
||||
pageId={item.id}
|
||||
// memo?
|
||||
docCollection={workspace}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[collections, docMetaMapping, workspace]
|
||||
);
|
||||
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.favoriteList}
|
||||
data-testid="favourites"
|
||||
ref={setNodeRef}
|
||||
data-over={isOver}
|
||||
data-over={shouldRenderDragOver}
|
||||
>
|
||||
<CategoryDivider label={t['com.affine.rootAppSidebar.favorites']()}>
|
||||
<AddFavouriteButton docCollection={workspace} />
|
||||
</CategoryDivider>
|
||||
{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;
|
||||
return <Fragment key={item.id}>{renderFavItem(item)}</Fragment>;
|
||||
})}
|
||||
{favourites.length === 0 && <EmptyItem />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const FavoriteList = ({
|
||||
docCollection: workspace,
|
||||
}: FavoriteListProps) => {
|
||||
const favAdapter = useService(FavoriteItemsAdapter);
|
||||
const favourites = useLiveData(favAdapter.orderedFavorites$);
|
||||
const sortItems = useMemo(() => {
|
||||
return favourites.map(fav => getDNDId('sidebar-pin', fav.type, fav.id));
|
||||
}, [favourites]);
|
||||
return (
|
||||
<SortableContext items={sortItems} strategy={verticalListSortingStrategy}>
|
||||
<FavoriteListInner docCollection={workspace} />
|
||||
</SortableContext>
|
||||
);
|
||||
};
|
||||
|
||||
export default FavoriteList;
|
||||
|
||||
+46
-22
@@ -1,30 +1,40 @@
|
||||
import {
|
||||
getDNDId,
|
||||
parseDNDId,
|
||||
} from '@affine/core/hooks/affine/use-global-dnd-helper';
|
||||
import { useBlockSuitePageReferences } from '@affine/core/hooks/use-block-suite-page-references';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
||||
import { useDraggable } from '@dnd-kit/core';
|
||||
import { type AnimateLayoutChanges, useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import { PageRecordList, useLiveData, useService } from '@toeverything/infra';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { getDragItemId } from '../../../../hooks/affine/use-sidebar-drag';
|
||||
import { MenuLinkItem } from '../../../app-sidebar';
|
||||
import { DragMenuItemOverlay } from '../components/drag-menu-item-overlay';
|
||||
import * as draggableMenuItemStyles from '../components/draggable-menu-item.css';
|
||||
import { PostfixItem } from '../components/postfix-item';
|
||||
import type { ReferencePageProps } from '../components/reference-page';
|
||||
import { ReferencePage } from '../components/reference-page';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export const FavouritePage = ({
|
||||
const animateLayoutChanges: AnimateLayoutChanges = ({
|
||||
isSorting,
|
||||
wasDragging,
|
||||
}) => (isSorting || wasDragging ? false : true);
|
||||
|
||||
export const FavouriteDocSidebarNavItem = ({
|
||||
docCollection: workspace,
|
||||
pageId,
|
||||
metaMapping,
|
||||
parentIds,
|
||||
}: ReferencePageProps) => {
|
||||
}: ReferencePageProps & {
|
||||
sortable?: boolean;
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const params = useParams();
|
||||
const active = params.pageId === pageId;
|
||||
const dragItemId = getDragItemId('favouritePage', pageId);
|
||||
const linkActive = params.pageId === pageId;
|
||||
const pageRecord = useLiveData(useService(PageRecordList).record$(pageId));
|
||||
const pageMode = useLiveData(pageRecord?.mode$);
|
||||
|
||||
@@ -43,43 +53,57 @@ export const FavouritePage = ({
|
||||
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const collapsible = referencesToShow.length > 0;
|
||||
const nestedItem = parentIds.size > 0;
|
||||
|
||||
const untitled = !metaMapping[pageId]?.title;
|
||||
const pageTitle = metaMapping[pageId]?.title || t['Untitled']();
|
||||
|
||||
const pageTitleElement = useMemo(() => {
|
||||
return <DragMenuItemOverlay icon={icon} pageTitle={pageTitle} />;
|
||||
const overlayPreview = useMemo(() => {
|
||||
return <DragMenuItemOverlay icon={icon} title={pageTitle} />;
|
||||
}, [icon, pageTitle]);
|
||||
|
||||
const { setNodeRef, attributes, listeners, isDragging } = useDraggable({
|
||||
const dragItemId = getDNDId('sidebar-pin', 'doc', pageId);
|
||||
|
||||
const {
|
||||
setNodeRef,
|
||||
isDragging,
|
||||
attributes,
|
||||
listeners,
|
||||
transform,
|
||||
transition,
|
||||
active,
|
||||
} = useSortable({
|
||||
id: dragItemId,
|
||||
data: {
|
||||
pageId,
|
||||
pageTitle: pageTitleElement,
|
||||
preview: overlayPreview,
|
||||
},
|
||||
animateLayoutChanges,
|
||||
});
|
||||
|
||||
const isSorting = parseDNDId(active?.id)?.where === 'sidebar-pin';
|
||||
const style = {
|
||||
transform: CSS.Translate.toString(transform),
|
||||
transition: isSorting ? transition : undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<Collapsible.Root
|
||||
className={styles.favItemWrapper}
|
||||
data-nested={nestedItem}
|
||||
open={!collapsed}
|
||||
data-draggable={true}
|
||||
data-dragging={isDragging}
|
||||
style={style}
|
||||
ref={setNodeRef}
|
||||
{...attributes}
|
||||
>
|
||||
<MenuLinkItem
|
||||
{...listeners}
|
||||
data-testid={`favourite-page-${pageId}`}
|
||||
data-type="favourite-list-item"
|
||||
icon={icon}
|
||||
className={styles.favItem}
|
||||
active={active}
|
||||
data-draggable={true}
|
||||
data-dragging={isDragging}
|
||||
className={draggableMenuItemStyles.draggableMenuItem}
|
||||
active={linkActive}
|
||||
to={`/workspace/${workspace.id}/${pageId}`}
|
||||
collapsed={collapsible ? collapsed : undefined}
|
||||
onCollapsedChange={setCollapsed}
|
||||
ref={setNodeRef}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
postfix={
|
||||
<PostfixItem
|
||||
docCollection={workspace}
|
||||
+7
-28
@@ -10,6 +10,8 @@ export const label = style({
|
||||
export const favItemWrapper = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexShrink: 0,
|
||||
userSelect: 'none',
|
||||
selectors: {
|
||||
'&[data-nested="true"]': {
|
||||
marginLeft: '20px',
|
||||
@@ -40,6 +42,9 @@ export const collapsibleContent = style({
|
||||
overflow: 'hidden',
|
||||
marginTop: '4px',
|
||||
selectors: {
|
||||
'&[data-hidden="true"]': {
|
||||
display: 'none',
|
||||
},
|
||||
'&[data-state="open"]': {
|
||||
animation: `${slideDown} 0.2s ease-out`,
|
||||
},
|
||||
@@ -52,33 +57,7 @@ export const collapsibleContentInner = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
});
|
||||
export const favItem = style({});
|
||||
globalStyle(`[data-draggable=true] ${favItem}:before`, {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
left: 0,
|
||||
width: 4,
|
||||
height: 4,
|
||||
transition: 'height 0.2s, opacity 0.2s',
|
||||
backgroundColor: cssVar('placeholderColor'),
|
||||
borderRadius: '2px',
|
||||
opacity: 0,
|
||||
willChange: 'height, opacity',
|
||||
});
|
||||
globalStyle(`[data-draggable=true] ${favItem}:hover:before`, {
|
||||
height: 12,
|
||||
opacity: 1,
|
||||
});
|
||||
globalStyle(`[data-draggable=true][data-dragging=true] ${favItem}`, {
|
||||
opacity: 0.5,
|
||||
});
|
||||
globalStyle(`[data-draggable=true][data-dragging=true] ${favItem}:before`, {
|
||||
height: 32,
|
||||
width: 2,
|
||||
opacity: 1,
|
||||
});
|
||||
|
||||
export const dragPageItemOverlay = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
@@ -91,7 +70,6 @@ export const dragPageItemOverlay = style({
|
||||
gap: '8px',
|
||||
padding: '4px',
|
||||
borderRadius: '4px',
|
||||
cursor: 'grabbing',
|
||||
});
|
||||
globalStyle(`${dragPageItemOverlay} svg`, {
|
||||
width: '20px',
|
||||
@@ -104,6 +82,7 @@ globalStyle(`${dragPageItemOverlay} span`, {
|
||||
overflow: 'hidden',
|
||||
});
|
||||
export const favoriteList = style({
|
||||
overflow: 'hidden',
|
||||
selectors: {
|
||||
'&[data-over="true"]': {
|
||||
background: cssVar('hoverColorFilled'),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AnimatedDeleteIcon } from '@affine/component';
|
||||
import { getDNDId } from '@affine/core/hooks/affine/use-global-dnd-helper';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import { apis, events } from '@affine/electron-api';
|
||||
@@ -14,7 +15,6 @@ import type { HTMLAttributes, ReactElement } from 'react';
|
||||
import { forwardRef, useCallback, useEffect } from 'react';
|
||||
|
||||
import { useAppSettingHelper } from '../../hooks/affine/use-app-setting-helper';
|
||||
import { getDropItemId } from '../../hooks/affine/use-sidebar-drag';
|
||||
import { useTrashModalHelper } from '../../hooks/affine/use-trash-modal-helper';
|
||||
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
|
||||
import { Workbench } from '../../modules/workbench';
|
||||
@@ -37,7 +37,6 @@ import {
|
||||
} from '../page-list';
|
||||
import { CollectionsList } from '../pure/workspace-slider-bar/collections';
|
||||
import { AddCollectionButton } from '../pure/workspace-slider-bar/collections/add-collection-button';
|
||||
import { AddFavouriteButton } from '../pure/workspace-slider-bar/favorite/add-favourite-button';
|
||||
import FavoriteList from '../pure/workspace-slider-bar/favorite/favorite-list';
|
||||
import { WorkspaceSelector } from '../workspace-selector';
|
||||
import ImportPage from './import-page';
|
||||
@@ -141,7 +140,7 @@ export const RootAppSidebar = ({
|
||||
}
|
||||
}, [sidebarOpen]);
|
||||
|
||||
const dropItemId = getDropItemId('trash');
|
||||
const dropItemId = getDNDId('sidebar-trash', 'container', 'trash');
|
||||
const trashDroppable = useDroppable({
|
||||
id: dropItemId,
|
||||
});
|
||||
@@ -215,9 +214,6 @@ export const RootAppSidebar = ({
|
||||
</SidebarContainer>
|
||||
|
||||
<SidebarScrollableContainer>
|
||||
<CategoryDivider label={t['com.affine.rootAppSidebar.favorites']()}>
|
||||
<AddFavouriteButton docCollection={docCollection} />
|
||||
</CategoryDivider>
|
||||
<FavoriteList docCollection={docCollection} />
|
||||
<CategoryDivider label={t['com.affine.rootAppSidebar.collections']()}>
|
||||
<AddCollectionButton node={node} onClick={handleCreateCollection} />
|
||||
@@ -233,7 +229,7 @@ export const RootAppSidebar = ({
|
||||
<RouteMenuLinkItem
|
||||
ref={trashDroppable.setNodeRef}
|
||||
icon={<AnimatedDeleteIcon closed={trashDroppable.isOver} />}
|
||||
active={trashActive}
|
||||
active={trashActive || trashDroppable.isOver}
|
||||
path={paths.trash(currentWorkspaceId)}
|
||||
>
|
||||
<span data-testid="trash-page">
|
||||
|
||||
Reference in New Issue
Block a user