feat(core): fav item reordering (#6302)
This commit is contained in:
@@ -29,6 +29,7 @@
|
|||||||
"@dnd-kit/core": "^6.1.0",
|
"@dnd-kit/core": "^6.1.0",
|
||||||
"@dnd-kit/modifiers": "^7.0.0",
|
"@dnd-kit/modifiers": "^7.0.0",
|
||||||
"@dnd-kit/sortable": "^8.0.0",
|
"@dnd-kit/sortable": "^8.0.0",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@emotion/cache": "^11.11.0",
|
"@emotion/cache": "^11.11.0",
|
||||||
"@emotion/react": "^11.11.3",
|
"@emotion/react": "^11.11.3",
|
||||||
"@emotion/server": "^11.11.0",
|
"@emotion/server": "^11.11.0",
|
||||||
|
|||||||
@@ -35,5 +35,11 @@ export const useFavorite = (pageId: string) => {
|
|||||||
export const FavoriteButton = ({ pageId }: FavoriteButtonProps) => {
|
export const FavoriteButton = ({ pageId }: FavoriteButtonProps) => {
|
||||||
const { favorite, toggleFavorite } = useFavorite(pageId);
|
const { favorite, toggleFavorite } = useFavorite(pageId);
|
||||||
|
|
||||||
return <FavoriteTag active={!!favorite} onClick={toggleFavorite} />;
|
return (
|
||||||
|
<FavoriteTag
|
||||||
|
data-testid="pin-button"
|
||||||
|
active={!!favorite}
|
||||||
|
onClick={toggleFavorite}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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({
|
export const dragPageItemOverlay = style({
|
||||||
height: '54px',
|
height: '54px',
|
||||||
borderRadius: '10px',
|
borderRadius: '10px',
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Checkbox } from '@affine/component';
|
import { Checkbox } from '@affine/component';
|
||||||
|
import { getDNDId } from '@affine/core/hooks/affine/use-global-dnd-helper';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { useDraggable } from '@dnd-kit/core';
|
import { useDraggable } from '@dnd-kit/core';
|
||||||
import type { PropsWithChildren } from 'react';
|
import type { PropsWithChildren } from 'react';
|
||||||
@@ -97,16 +98,22 @@ export const CollectionListItem = (props: CollectionListItemProps) => {
|
|||||||
/>
|
/>
|
||||||
<ListIconCell icon={props.icon} />
|
<ListIconCell icon={props.icon} />
|
||||||
</div>
|
</div>
|
||||||
|
<ListTitleCell title={props.title} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}, [props.icon, props.onSelectedChange, props.selectable, props.selected]);
|
}, [
|
||||||
|
props.icon,
|
||||||
|
props.onSelectedChange,
|
||||||
|
props.selectable,
|
||||||
|
props.selected,
|
||||||
|
props.title,
|
||||||
|
]);
|
||||||
|
|
||||||
// TODO: use getDropItemId
|
// TODO: use getDropItemId
|
||||||
const { setNodeRef, attributes, listeners, isDragging } = useDraggable({
|
const { setNodeRef, attributes, listeners, isDragging } = useDraggable({
|
||||||
id: 'collection-list-item-title-' + props.collectionId,
|
id: getDNDId('collection-list', 'collection', props.collectionId),
|
||||||
data: {
|
data: {
|
||||||
pageId: props.collectionId,
|
preview: collectionTitleElement,
|
||||||
pageTitle: collectionTitleElement,
|
|
||||||
} satisfies DraggableTitleCellData,
|
} satisfies DraggableTitleCellData,
|
||||||
disabled: !props.draggable,
|
disabled: !props.draggable,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export const VirtualizedCollectionList = ({
|
|||||||
<VirtualizedList
|
<VirtualizedList
|
||||||
ref={listRef}
|
ref={listRef}
|
||||||
selectable="toggle"
|
selectable="toggle"
|
||||||
draggable={false}
|
draggable
|
||||||
atTopThreshold={80}
|
atTopThreshold={80}
|
||||||
atTopStateChange={setHideHeaderCreateNewCollection}
|
atTopStateChange={setHideHeaderCreateNewCollection}
|
||||||
onSelectionActiveChange={setShowFloatingToolbar}
|
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({
|
export const dragPageItemOverlay = style({
|
||||||
height: '54px',
|
height: '54px',
|
||||||
borderRadius: '10px',
|
borderRadius: '10px',
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Checkbox } from '@affine/component';
|
import { Checkbox } from '@affine/component';
|
||||||
|
import { getDNDId } from '@affine/core/hooks/affine/use-global-dnd-helper';
|
||||||
import { TagService } from '@affine/core/modules/tag';
|
import { TagService } from '@affine/core/modules/tag';
|
||||||
import { useDraggable } from '@dnd-kit/core';
|
import { useDraggable } from '@dnd-kit/core';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
@@ -152,10 +153,9 @@ export const PageListItem = (props: PageListItemProps) => {
|
|||||||
|
|
||||||
// TODO: use getDropItemId
|
// TODO: use getDropItemId
|
||||||
const { setNodeRef, attributes, listeners, isDragging } = useDraggable({
|
const { setNodeRef, attributes, listeners, isDragging } = useDraggable({
|
||||||
id: 'page-list-item-title-' + props.pageId,
|
id: getDNDId('doc-list', 'doc', props.pageId),
|
||||||
data: {
|
data: {
|
||||||
pageId: props.pageId,
|
preview: pageTitleElement,
|
||||||
pageTitle: pageTitleElement,
|
|
||||||
} satisfies DraggableTitleCellData,
|
} satisfies DraggableTitleCellData,
|
||||||
disabled: !props.draggable,
|
disabled: !props.draggable,
|
||||||
});
|
});
|
||||||
@@ -279,16 +279,3 @@ function PageListItemWrapper({
|
|||||||
return <div {...commonProps}>{children}</div>;
|
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({
|
export const dragPageItemOverlay = style({
|
||||||
height: '54px',
|
height: '54px',
|
||||||
borderRadius: '10px',
|
borderRadius: '10px',
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Checkbox } from '@affine/component';
|
import { Checkbox } from '@affine/component';
|
||||||
|
import { getDNDId } from '@affine/core/hooks/affine/use-global-dnd-helper';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { useDraggable } from '@dnd-kit/core';
|
import { useDraggable } from '@dnd-kit/core';
|
||||||
import type { PropsWithChildren } from 'react';
|
import type { PropsWithChildren } from 'react';
|
||||||
@@ -99,10 +100,9 @@ export const TagListItem = (props: TagListItemProps) => {
|
|||||||
|
|
||||||
// TODO: use getDropItemId
|
// TODO: use getDropItemId
|
||||||
const { setNodeRef, attributes, listeners, isDragging } = useDraggable({
|
const { setNodeRef, attributes, listeners, isDragging } = useDraggable({
|
||||||
id: 'tag-list-item-title-' + props.tagId,
|
id: getDNDId('tag-list', 'tag', props.tagId),
|
||||||
data: {
|
data: {
|
||||||
pageId: props.tagId,
|
preview: tagTitleElement,
|
||||||
pageTitle: tagTitleElement,
|
|
||||||
} satisfies DraggableTitleCellData,
|
} satisfies DraggableTitleCellData,
|
||||||
disabled: !props.draggable,
|
disabled: !props.draggable,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -141,8 +141,7 @@ type MakeRecord<T> = {
|
|||||||
export type MetaRecord<T> = MakeRecord<T>;
|
export type MetaRecord<T> = MakeRecord<T>;
|
||||||
|
|
||||||
export type DraggableTitleCellData = {
|
export type DraggableTitleCellData = {
|
||||||
pageId: string;
|
preview: ReactNode;
|
||||||
pageTitle: ReactNode;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HeaderColDef = {
|
export type HeaderColDef = {
|
||||||
|
|||||||
@@ -6,34 +6,49 @@ import {
|
|||||||
filterPage,
|
filterPage,
|
||||||
stopPropagation,
|
stopPropagation,
|
||||||
} from '@affine/core/components/page-list';
|
} 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 { CollectionService } from '@affine/core/modules/collection';
|
||||||
import { FavoriteItemsAdapter } from '@affine/core/modules/workspace';
|
import { FavoriteItemsAdapter } from '@affine/core/modules/workspace';
|
||||||
import type { Collection } 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 } from '@blocksuite/store';
|
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 * as Collapsible from '@radix-ui/react-collapsible';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { useAllPageListConfig } from '../../../../hooks/affine/use-all-page-list-config';
|
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 { useBlockSuiteDocMeta } from '../../../../hooks/use-block-suite-page-meta';
|
||||||
import { Workbench } from '../../../../modules/workbench';
|
import { Workbench } from '../../../../modules/workbench';
|
||||||
import { WorkbenchLink } from '../../../../modules/workbench/view/workbench-link';
|
import { WorkbenchLink } from '../../../../modules/workbench/view/workbench-link';
|
||||||
import { MenuLinkItem as SidebarMenuLinkItem } from '../../../app-sidebar';
|
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 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 animateLayoutChanges: AnimateLayoutChanges = ({
|
||||||
|
isSorting,
|
||||||
|
wasDragging,
|
||||||
|
}) => (isSorting || wasDragging ? false : true);
|
||||||
|
|
||||||
export const CollectionSidebarNavItem = ({
|
export const CollectionSidebarNavItem = ({
|
||||||
collection,
|
collection,
|
||||||
docCollection,
|
docCollection,
|
||||||
className,
|
className,
|
||||||
|
dndId,
|
||||||
}: {
|
}: {
|
||||||
collection: Collection;
|
collection: Collection;
|
||||||
docCollection: DocCollection;
|
docCollection: DocCollection;
|
||||||
|
dndId: DNDIdentifier;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const pages = useBlockSuiteDocMeta(docCollection);
|
const pages = useBlockSuiteDocMeta(docCollection);
|
||||||
@@ -42,37 +57,50 @@ export const CollectionSidebarNavItem = ({
|
|||||||
const collectionService = useService(CollectionService);
|
const collectionService = useService(CollectionService);
|
||||||
const favAdapter = useService(FavoriteItemsAdapter);
|
const favAdapter = useService(FavoriteItemsAdapter);
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const dragItemId = getDropItemId('collections', collection.id);
|
|
||||||
|
|
||||||
const favourites = useLiveData(favAdapter.favorites$);
|
const favourites = useLiveData(favAdapter.favorites$);
|
||||||
|
|
||||||
const removeFromAllowList = useCallback(
|
const removeFromAllowList = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
collectionService.updateCollection(collection.id, () => ({
|
collectionService.deletePageFromCollection(collection.id, id);
|
||||||
...collection,
|
|
||||||
allowList: collection.allowList?.filter(v => v !== id),
|
|
||||||
}));
|
|
||||||
|
|
||||||
toast(t['com.affine.collection.removePage.success']());
|
toast(t['com.affine.collection.removePage.success']());
|
||||||
},
|
},
|
||||||
[collection, collectionService, t]
|
[collection, collectionService, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { setNodeRef, isOver } = useDroppable({
|
const overlayPreview = useMemo(() => {
|
||||||
id: dragItemId,
|
return (
|
||||||
|
<DragMenuItemOverlay icon={<ViewLayersIcon />} title={collection.name} />
|
||||||
|
);
|
||||||
|
}, [collection.name]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
setNodeRef,
|
||||||
|
isDragging,
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
transform,
|
||||||
|
over,
|
||||||
|
active,
|
||||||
|
transition,
|
||||||
|
} = useSortable({
|
||||||
|
id: dndId,
|
||||||
data: {
|
data: {
|
||||||
addToCollection: (id: string) => {
|
preview: overlayPreview,
|
||||||
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);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
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 config = useAllPageListConfig();
|
||||||
const allPagesMeta = useMemo(
|
const allPagesMeta = useMemo(
|
||||||
() => Object.fromEntries(pages.map(v => [v.id, v])),
|
() => Object.fromEntries(pages.map(v => [v.id, v])),
|
||||||
@@ -113,12 +141,18 @@ export const CollectionSidebarNavItem = ({
|
|||||||
return (
|
return (
|
||||||
<Collapsible.Root
|
<Collapsible.Root
|
||||||
open={!collapsed}
|
open={!collapsed}
|
||||||
ref={setNodeRef}
|
|
||||||
data-testid="collection-"
|
|
||||||
className={className}
|
className={className}
|
||||||
|
style={style}
|
||||||
|
ref={setNodeRef}
|
||||||
|
{...attributes}
|
||||||
>
|
>
|
||||||
<SidebarMenuLinkItem
|
<SidebarMenuLinkItem
|
||||||
|
{...listeners}
|
||||||
|
data-draggable={true}
|
||||||
|
data-dragging={isDragging}
|
||||||
|
className={draggableMenuItemStyles.draggableMenuItem}
|
||||||
data-testid="collection-item"
|
data-testid="collection-item"
|
||||||
|
data-collection-id={collection.id}
|
||||||
data-type="collection-list-item"
|
data-type="collection-list-item"
|
||||||
onCollapsedChange={setCollapsed}
|
onCollapsedChange={setCollapsed}
|
||||||
active={isOver || currentPath === path}
|
active={isOver || currentPath === path}
|
||||||
@@ -161,6 +195,7 @@ export const CollectionSidebarNavItem = ({
|
|||||||
{pagesToRender.map(page => {
|
{pagesToRender.map(page => {
|
||||||
return (
|
return (
|
||||||
<Page
|
<Page
|
||||||
|
parentId={dndId}
|
||||||
inAllowList={allowList.has(page.id)}
|
inAllowList={allowList.has(page.id)}
|
||||||
removeFromAllowList={removeFromAllowList}
|
removeFromAllowList={removeFromAllowList}
|
||||||
allPageMeta={allPagesMeta}
|
allPageMeta={allPagesMeta}
|
||||||
@@ -205,11 +240,18 @@ export const CollectionsList = ({
|
|||||||
return (
|
return (
|
||||||
<div data-testid="collections" className={styles.wrapper}>
|
<div data-testid="collections" className={styles.wrapper}>
|
||||||
{collections.map(view => {
|
{collections.map(view => {
|
||||||
|
const dragItemId = getDNDId(
|
||||||
|
'sidebar-collections',
|
||||||
|
'collection',
|
||||||
|
view.id
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CollectionSidebarNavItem
|
<CollectionSidebarNavItem
|
||||||
key={view.id}
|
key={view.id}
|
||||||
collection={view}
|
collection={view}
|
||||||
docCollection={workspace}
|
docCollection={workspace}
|
||||||
|
dndId={dragItemId}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ import { PageRecordList, useLiveData, useService } from '@toeverything/infra';
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
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 { useNavigateHelper } from '../../../../hooks/use-navigate-helper';
|
||||||
import { MenuItem as CollectionItem } from '../../../app-sidebar';
|
import { MenuItem as CollectionItem } from '../../../app-sidebar';
|
||||||
import { DragMenuItemOverlay } from '../components/drag-menu-item-overlay';
|
import { DragMenuItemOverlay } from '../components/drag-menu-item-overlay';
|
||||||
@@ -18,11 +21,13 @@ import * as styles from './styles.css';
|
|||||||
|
|
||||||
export const Page = ({
|
export const Page = ({
|
||||||
page,
|
page,
|
||||||
|
parentId,
|
||||||
docCollection,
|
docCollection,
|
||||||
allPageMeta,
|
allPageMeta,
|
||||||
inAllowList,
|
inAllowList,
|
||||||
removeFromAllowList,
|
removeFromAllowList,
|
||||||
}: {
|
}: {
|
||||||
|
parentId: DNDIdentifier;
|
||||||
page: DocMeta;
|
page: DocMeta;
|
||||||
inAllowList: boolean;
|
inAllowList: boolean;
|
||||||
removeFromAllowList: (id: string) => void;
|
removeFromAllowList: (id: string) => void;
|
||||||
@@ -38,7 +43,7 @@ export const Page = ({
|
|||||||
const active = params.pageId === pageId;
|
const active = params.pageId === pageId;
|
||||||
const pageRecord = useLiveData(useService(PageRecordList).record$(pageId));
|
const pageRecord = useLiveData(useService(PageRecordList).record$(pageId));
|
||||||
const pageMode = useLiveData(pageRecord?.mode$);
|
const pageMode = useLiveData(pageRecord?.mode$);
|
||||||
const dragItemId = getDragItemId('collectionPage', pageId);
|
const dragItemId = getDNDId('collection-list', 'doc', pageId, parentId);
|
||||||
|
|
||||||
const icon = useMemo(() => {
|
const icon = useMemo(() => {
|
||||||
return pageMode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />;
|
return pageMode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />;
|
||||||
@@ -56,15 +61,13 @@ export const Page = ({
|
|||||||
|
|
||||||
const pageTitle = page.title || t['Untitled']();
|
const pageTitle = page.title || t['Untitled']();
|
||||||
const pageTitleElement = useMemo(() => {
|
const pageTitleElement = useMemo(() => {
|
||||||
return <DragMenuItemOverlay icon={icon} pageTitle={pageTitle} />;
|
return <DragMenuItemOverlay icon={icon} title={pageTitle} />;
|
||||||
}, [icon, pageTitle]);
|
}, [icon, pageTitle]);
|
||||||
|
|
||||||
const { setNodeRef, attributes, listeners, isDragging } = useDraggable({
|
const { setNodeRef, attributes, listeners, isDragging } = useDraggable({
|
||||||
id: dragItemId,
|
id: dragItemId,
|
||||||
data: {
|
data: {
|
||||||
pageId,
|
preview: pageTitleElement,
|
||||||
pageTitle: pageTitleElement,
|
|
||||||
removeFromCollection: () => removeFromAllowList(pageId),
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -102,6 +102,9 @@ export const collapsibleContent = style({
|
|||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
marginTop: '4px',
|
marginTop: '4px',
|
||||||
selectors: {
|
selectors: {
|
||||||
|
'&[data-hidden="true"]': {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
'&[data-state="open"]': {
|
'&[data-state="open"]': {
|
||||||
animation: `${slideDown} 0.2s ease-in-out`,
|
animation: `${slideDown} 0.2s ease-in-out`,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import * as styles from '../favorite/styles.css';
|
import * as styles from '../favorite/styles.css';
|
||||||
|
|
||||||
export const DragMenuItemOverlay = ({
|
export const DragMenuItemOverlay = ({
|
||||||
pageTitle,
|
title,
|
||||||
icon,
|
icon,
|
||||||
}: {
|
}: {
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
pageTitle: React.ReactNode;
|
title: React.ReactNode;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.dragPageItemOverlay}>
|
<div className={styles.dragPageItemOverlay}>
|
||||||
{icon}
|
{icon}
|
||||||
<span>{pageTitle}</span>
|
<span>{title}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -14,7 +14,7 @@ export interface ReferencePageProps {
|
|||||||
docCollection: DocCollection;
|
docCollection: DocCollection;
|
||||||
pageId: string;
|
pageId: string;
|
||||||
metaMapping: Record<string, DocMeta>;
|
metaMapping: Record<string, DocMeta>;
|
||||||
parentIds: Set<string>;
|
parentIds?: Set<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ReferencePage = ({
|
export const ReferencePage = ({
|
||||||
@@ -44,7 +44,7 @@ export const ReferencePage = ({
|
|||||||
|
|
||||||
const [collapsed, setCollapsed] = useState(true);
|
const [collapsed, setCollapsed] = useState(true);
|
||||||
const collapsible = referencesToShow.length > 0;
|
const collapsible = referencesToShow.length > 0;
|
||||||
const nestedItem = parentIds.size > 0;
|
const nestedItem = parentIds && parentIds.size > 0;
|
||||||
|
|
||||||
const untitled = !metaMapping[pageId]?.title;
|
const untitled = !metaMapping[pageId]?.title;
|
||||||
const pageTitle = metaMapping[pageId]?.title || t['Untitled']();
|
const pageTitle = metaMapping[pageId]?.title || t['Untitled']();
|
||||||
@@ -86,7 +86,7 @@ export const ReferencePage = ({
|
|||||||
docCollection={docCollection}
|
docCollection={docCollection}
|
||||||
pageId={ref}
|
pageId={ref}
|
||||||
metaMapping={metaMapping}
|
metaMapping={metaMapping}
|
||||||
parentIds={new Set([...parentIds, pageId])}
|
parentIds={new Set([...(parentIds ?? []), pageId])}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -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 { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||||
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 { WorkspaceFavoriteItem } from '@affine/core/modules/workspace/properties/schema';
|
||||||
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import type { DocMeta } from '@blocksuite/store';
|
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 { 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 { CollectionSidebarNavItem } from '../collections';
|
||||||
import type { FavoriteListProps } from '../index';
|
import type { FavoriteListProps } from '../index';
|
||||||
|
import { AddFavouriteButton } from './add-favourite-button';
|
||||||
import EmptyItem from './empty-item';
|
import EmptyItem from './empty-item';
|
||||||
import { FavouritePage } from './favourite-page';
|
import { FavouriteDocSidebarNavItem } from './favourite-nav-item';
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
const emptyPageIdSet = new Set<string>();
|
const FavoriteListInner = ({ docCollection: workspace }: FavoriteListProps) => {
|
||||||
|
|
||||||
export const FavoriteList = ({
|
|
||||||
docCollection: workspace,
|
|
||||||
}: FavoriteListProps) => {
|
|
||||||
const metas = useBlockSuiteDocMeta(workspace);
|
const metas = useBlockSuiteDocMeta(workspace);
|
||||||
const favAdapter = useService(FavoriteItemsAdapter);
|
const favAdapter = useService(FavoriteItemsAdapter);
|
||||||
const collections = useLiveData(useService(CollectionService).collections$);
|
const collections = useLiveData(useService(CollectionService).collections$);
|
||||||
const dropItemId = getDropItemId('favorites');
|
const dropItemId = getDNDId('sidebar-pin', 'container', workspace.id);
|
||||||
|
|
||||||
const docMetaMapping = useMemo(
|
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,
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={styles.favoriteList}
|
className={styles.favoriteList}
|
||||||
data-testid="favourites"
|
data-testid="favourites"
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
data-over={isOver}
|
data-over={shouldRenderDragOver}
|
||||||
>
|
>
|
||||||
|
<CategoryDivider label={t['com.affine.rootAppSidebar.favorites']()}>
|
||||||
|
<AddFavouriteButton docCollection={workspace} />
|
||||||
|
</CategoryDivider>
|
||||||
{favourites.map(item => {
|
{favourites.map(item => {
|
||||||
if (item.type === 'collection') {
|
return <Fragment key={item.id}>{renderFavItem(item)}</Fragment>;
|
||||||
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 />}
|
{favourites.length === 0 && <EmptyItem />}
|
||||||
</div>
|
</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;
|
export default FavoriteList;
|
||||||
|
|||||||
@@ -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 { useBlockSuitePageReferences } from '@affine/core/hooks/use-block-suite-page-references';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
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 * as Collapsible from '@radix-ui/react-collapsible';
|
||||||
import { PageRecordList, useLiveData, useService } from '@toeverything/infra';
|
import { PageRecordList, useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { getDragItemId } from '../../../../hooks/affine/use-sidebar-drag';
|
|
||||||
import { MenuLinkItem } from '../../../app-sidebar';
|
import { MenuLinkItem } from '../../../app-sidebar';
|
||||||
import { DragMenuItemOverlay } from '../components/drag-menu-item-overlay';
|
import { DragMenuItemOverlay } from '../components/drag-menu-item-overlay';
|
||||||
|
import * as draggableMenuItemStyles from '../components/draggable-menu-item.css';
|
||||||
import { PostfixItem } from '../components/postfix-item';
|
import { PostfixItem } from '../components/postfix-item';
|
||||||
import type { ReferencePageProps } from '../components/reference-page';
|
import type { ReferencePageProps } from '../components/reference-page';
|
||||||
import { ReferencePage } from '../components/reference-page';
|
import { ReferencePage } from '../components/reference-page';
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
export const FavouritePage = ({
|
const animateLayoutChanges: AnimateLayoutChanges = ({
|
||||||
|
isSorting,
|
||||||
|
wasDragging,
|
||||||
|
}) => (isSorting || wasDragging ? false : true);
|
||||||
|
|
||||||
|
export const FavouriteDocSidebarNavItem = ({
|
||||||
docCollection: workspace,
|
docCollection: workspace,
|
||||||
pageId,
|
pageId,
|
||||||
metaMapping,
|
metaMapping,
|
||||||
parentIds,
|
}: ReferencePageProps & {
|
||||||
}: ReferencePageProps) => {
|
sortable?: boolean;
|
||||||
|
}) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const active = params.pageId === pageId;
|
const linkActive = params.pageId === pageId;
|
||||||
const dragItemId = getDragItemId('favouritePage', pageId);
|
|
||||||
const pageRecord = useLiveData(useService(PageRecordList).record$(pageId));
|
const pageRecord = useLiveData(useService(PageRecordList).record$(pageId));
|
||||||
const pageMode = useLiveData(pageRecord?.mode$);
|
const pageMode = useLiveData(pageRecord?.mode$);
|
||||||
|
|
||||||
@@ -43,43 +53,57 @@ export const FavouritePage = ({
|
|||||||
|
|
||||||
const [collapsed, setCollapsed] = useState(true);
|
const [collapsed, setCollapsed] = useState(true);
|
||||||
const collapsible = referencesToShow.length > 0;
|
const collapsible = referencesToShow.length > 0;
|
||||||
const nestedItem = parentIds.size > 0;
|
|
||||||
|
|
||||||
const untitled = !metaMapping[pageId]?.title;
|
const untitled = !metaMapping[pageId]?.title;
|
||||||
const pageTitle = metaMapping[pageId]?.title || t['Untitled']();
|
const pageTitle = metaMapping[pageId]?.title || t['Untitled']();
|
||||||
|
|
||||||
const pageTitleElement = useMemo(() => {
|
const overlayPreview = useMemo(() => {
|
||||||
return <DragMenuItemOverlay icon={icon} pageTitle={pageTitle} />;
|
return <DragMenuItemOverlay icon={icon} title={pageTitle} />;
|
||||||
}, [icon, 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,
|
id: dragItemId,
|
||||||
data: {
|
data: {
|
||||||
pageId,
|
preview: overlayPreview,
|
||||||
pageTitle: pageTitleElement,
|
|
||||||
},
|
},
|
||||||
|
animateLayoutChanges,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isSorting = parseDNDId(active?.id)?.where === 'sidebar-pin';
|
||||||
|
const style = {
|
||||||
|
transform: CSS.Translate.toString(transform),
|
||||||
|
transition: isSorting ? transition : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapsible.Root
|
<Collapsible.Root
|
||||||
className={styles.favItemWrapper}
|
className={styles.favItemWrapper}
|
||||||
data-nested={nestedItem}
|
|
||||||
open={!collapsed}
|
open={!collapsed}
|
||||||
data-draggable={true}
|
style={style}
|
||||||
data-dragging={isDragging}
|
ref={setNodeRef}
|
||||||
|
{...attributes}
|
||||||
>
|
>
|
||||||
<MenuLinkItem
|
<MenuLinkItem
|
||||||
|
{...listeners}
|
||||||
data-testid={`favourite-page-${pageId}`}
|
data-testid={`favourite-page-${pageId}`}
|
||||||
data-type="favourite-list-item"
|
|
||||||
icon={icon}
|
icon={icon}
|
||||||
className={styles.favItem}
|
data-draggable={true}
|
||||||
active={active}
|
data-dragging={isDragging}
|
||||||
|
className={draggableMenuItemStyles.draggableMenuItem}
|
||||||
|
active={linkActive}
|
||||||
to={`/workspace/${workspace.id}/${pageId}`}
|
to={`/workspace/${workspace.id}/${pageId}`}
|
||||||
collapsed={collapsible ? collapsed : undefined}
|
collapsed={collapsible ? collapsed : undefined}
|
||||||
onCollapsedChange={setCollapsed}
|
onCollapsedChange={setCollapsed}
|
||||||
ref={setNodeRef}
|
|
||||||
{...attributes}
|
|
||||||
{...listeners}
|
|
||||||
postfix={
|
postfix={
|
||||||
<PostfixItem
|
<PostfixItem
|
||||||
docCollection={workspace}
|
docCollection={workspace}
|
||||||
@@ -10,6 +10,8 @@ export const label = style({
|
|||||||
export const favItemWrapper = style({
|
export const favItemWrapper = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
|
flexShrink: 0,
|
||||||
|
userSelect: 'none',
|
||||||
selectors: {
|
selectors: {
|
||||||
'&[data-nested="true"]': {
|
'&[data-nested="true"]': {
|
||||||
marginLeft: '20px',
|
marginLeft: '20px',
|
||||||
@@ -40,6 +42,9 @@ export const collapsibleContent = style({
|
|||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
marginTop: '4px',
|
marginTop: '4px',
|
||||||
selectors: {
|
selectors: {
|
||||||
|
'&[data-hidden="true"]': {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
'&[data-state="open"]': {
|
'&[data-state="open"]': {
|
||||||
animation: `${slideDown} 0.2s ease-out`,
|
animation: `${slideDown} 0.2s ease-out`,
|
||||||
},
|
},
|
||||||
@@ -52,33 +57,7 @@ export const collapsibleContentInner = style({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
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({
|
export const dragPageItemOverlay = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -91,7 +70,6 @@ export const dragPageItemOverlay = style({
|
|||||||
gap: '8px',
|
gap: '8px',
|
||||||
padding: '4px',
|
padding: '4px',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
cursor: 'grabbing',
|
|
||||||
});
|
});
|
||||||
globalStyle(`${dragPageItemOverlay} svg`, {
|
globalStyle(`${dragPageItemOverlay} svg`, {
|
||||||
width: '20px',
|
width: '20px',
|
||||||
@@ -104,6 +82,7 @@ globalStyle(`${dragPageItemOverlay} span`, {
|
|||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
});
|
});
|
||||||
export const favoriteList = style({
|
export const favoriteList = style({
|
||||||
|
overflow: 'hidden',
|
||||||
selectors: {
|
selectors: {
|
||||||
'&[data-over="true"]': {
|
'&[data-over="true"]': {
|
||||||
background: cssVar('hoverColorFilled'),
|
background: cssVar('hoverColorFilled'),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { AnimatedDeleteIcon } from '@affine/component';
|
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 { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import { CollectionService } from '@affine/core/modules/collection';
|
import { CollectionService } from '@affine/core/modules/collection';
|
||||||
import { apis, events } from '@affine/electron-api';
|
import { apis, events } from '@affine/electron-api';
|
||||||
@@ -14,7 +15,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 { 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';
|
||||||
import { Workbench } from '../../modules/workbench';
|
import { Workbench } from '../../modules/workbench';
|
||||||
@@ -37,7 +37,6 @@ import {
|
|||||||
} from '../page-list';
|
} from '../page-list';
|
||||||
import { CollectionsList } from '../pure/workspace-slider-bar/collections';
|
import { CollectionsList } from '../pure/workspace-slider-bar/collections';
|
||||||
import { AddCollectionButton } from '../pure/workspace-slider-bar/collections/add-collection-button';
|
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 FavoriteList from '../pure/workspace-slider-bar/favorite/favorite-list';
|
||||||
import { WorkspaceSelector } from '../workspace-selector';
|
import { WorkspaceSelector } from '../workspace-selector';
|
||||||
import ImportPage from './import-page';
|
import ImportPage from './import-page';
|
||||||
@@ -141,7 +140,7 @@ export const RootAppSidebar = ({
|
|||||||
}
|
}
|
||||||
}, [sidebarOpen]);
|
}, [sidebarOpen]);
|
||||||
|
|
||||||
const dropItemId = getDropItemId('trash');
|
const dropItemId = getDNDId('sidebar-trash', 'container', 'trash');
|
||||||
const trashDroppable = useDroppable({
|
const trashDroppable = useDroppable({
|
||||||
id: dropItemId,
|
id: dropItemId,
|
||||||
});
|
});
|
||||||
@@ -215,9 +214,6 @@ export const RootAppSidebar = ({
|
|||||||
</SidebarContainer>
|
</SidebarContainer>
|
||||||
|
|
||||||
<SidebarScrollableContainer>
|
<SidebarScrollableContainer>
|
||||||
<CategoryDivider label={t['com.affine.rootAppSidebar.favorites']()}>
|
|
||||||
<AddFavouriteButton docCollection={docCollection} />
|
|
||||||
</CategoryDivider>
|
|
||||||
<FavoriteList docCollection={docCollection} />
|
<FavoriteList docCollection={docCollection} />
|
||||||
<CategoryDivider label={t['com.affine.rootAppSidebar.collections']()}>
|
<CategoryDivider label={t['com.affine.rootAppSidebar.collections']()}>
|
||||||
<AddCollectionButton node={node} onClick={handleCreateCollection} />
|
<AddCollectionButton node={node} onClick={handleCreateCollection} />
|
||||||
@@ -233,7 +229,7 @@ export const RootAppSidebar = ({
|
|||||||
<RouteMenuLinkItem
|
<RouteMenuLinkItem
|
||||||
ref={trashDroppable.setNodeRef}
|
ref={trashDroppable.setNodeRef}
|
||||||
icon={<AnimatedDeleteIcon closed={trashDroppable.isOver} />}
|
icon={<AnimatedDeleteIcon closed={trashDroppable.isOver} />}
|
||||||
active={trashActive}
|
active={trashActive || trashDroppable.isOver}
|
||||||
path={paths.trash(currentWorkspaceId)}
|
path={paths.trash(currentWorkspaceId)}
|
||||||
>
|
>
|
||||||
<span data-testid="trash-page">
|
<span data-testid="trash-page">
|
||||||
|
|||||||
281
packages/frontend/core/src/hooks/affine/use-global-dnd-helper.ts
Normal file
281
packages/frontend/core/src/hooks/affine/use-global-dnd-helper.ts
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
import { toast } from '@affine/component';
|
||||||
|
import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||||
|
import { CollectionService } from '@affine/core/modules/collection';
|
||||||
|
import { FavoriteItemsAdapter } from '@affine/core/modules/workspace';
|
||||||
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
|
import type {
|
||||||
|
Active,
|
||||||
|
DragEndEvent,
|
||||||
|
Over,
|
||||||
|
UniqueIdentifier,
|
||||||
|
} from '@dnd-kit/core';
|
||||||
|
import { useLiveData, useService, Workspace } from '@toeverything/infra';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { useDeleteCollectionInfo } from './use-delete-collection-info';
|
||||||
|
import { useTrashModalHelper } from './use-trash-modal-helper';
|
||||||
|
|
||||||
|
export type DndWhere =
|
||||||
|
| 'sidebar-pin'
|
||||||
|
| 'sidebar-collections'
|
||||||
|
| 'sidebar-trash'
|
||||||
|
| 'doc-list'
|
||||||
|
| 'collection-list'
|
||||||
|
| 'tag-list';
|
||||||
|
|
||||||
|
export type DNDItemKind = 'container' | 'collection' | 'doc' | 'tag';
|
||||||
|
|
||||||
|
// where:kind:id
|
||||||
|
// we want to make the id something that can be used to identify the item
|
||||||
|
//
|
||||||
|
// Note, not all combinations are valid
|
||||||
|
type DNDItemIdentifier = `${DndWhere}:${DNDItemKind}:${string}`;
|
||||||
|
export type DNDIdentifier =
|
||||||
|
| `${DNDItemIdentifier}/${DNDItemIdentifier}`
|
||||||
|
| DNDItemIdentifier;
|
||||||
|
|
||||||
|
export type DndItem = {
|
||||||
|
where: DndWhere;
|
||||||
|
kind: DNDItemKind;
|
||||||
|
itemId: string;
|
||||||
|
parent?: DndItem; // for now we only support one level of nesting
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getDNDId(
|
||||||
|
where: DndWhere,
|
||||||
|
kind: DNDItemKind,
|
||||||
|
id: string,
|
||||||
|
parentId?: DNDIdentifier
|
||||||
|
): DNDIdentifier {
|
||||||
|
const itemId = `${where}:${kind}:${id}` as DNDItemIdentifier;
|
||||||
|
return parentId ? `${parentId}/${itemId}` : itemId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseDNDId(
|
||||||
|
id: UniqueIdentifier | null | undefined
|
||||||
|
): DndItem | undefined {
|
||||||
|
if (typeof id !== 'string') return undefined;
|
||||||
|
const parts = id.split('/');
|
||||||
|
if (parts.length === 1) {
|
||||||
|
const [where, kind, itemId] = id.split(':') as [
|
||||||
|
DndWhere,
|
||||||
|
DNDItemKind,
|
||||||
|
string,
|
||||||
|
];
|
||||||
|
return where && kind && itemId
|
||||||
|
? {
|
||||||
|
where,
|
||||||
|
kind,
|
||||||
|
itemId,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
} else if (parts.length === 2) {
|
||||||
|
const item = parseDNDId(parts[1]);
|
||||||
|
const parent = parseDNDId(parts[0]);
|
||||||
|
if (!item || !parent) return undefined;
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
parent,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new Error('Invalid DND ID');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveDragEndIntent(
|
||||||
|
active?: Active | null,
|
||||||
|
over?: Over | null
|
||||||
|
) {
|
||||||
|
const dragItem = parseDNDId(active?.id);
|
||||||
|
const dropItem = parseDNDId(over?.id);
|
||||||
|
|
||||||
|
if (!dragItem) return null;
|
||||||
|
|
||||||
|
// any doc item to trash
|
||||||
|
if (
|
||||||
|
dropItem?.where === 'sidebar-trash' &&
|
||||||
|
(dragItem.kind === 'doc' || dragItem.kind === 'collection')
|
||||||
|
) {
|
||||||
|
return 'trash:move-to';
|
||||||
|
}
|
||||||
|
|
||||||
|
// add page to collection
|
||||||
|
if (
|
||||||
|
dragItem.kind === 'doc' &&
|
||||||
|
dragItem.where !== dropItem?.where &&
|
||||||
|
dropItem?.kind === 'collection'
|
||||||
|
) {
|
||||||
|
return 'collection:add';
|
||||||
|
}
|
||||||
|
|
||||||
|
// move a doc from one collection to another
|
||||||
|
if (
|
||||||
|
dragItem.kind === 'doc' &&
|
||||||
|
dragItem?.where === 'collection-list' &&
|
||||||
|
dragItem.parent?.kind === 'collection' &&
|
||||||
|
dropItem?.kind !== 'collection'
|
||||||
|
) {
|
||||||
|
return 'collection:remove';
|
||||||
|
}
|
||||||
|
|
||||||
|
// move any doc/collection to sidebar pin
|
||||||
|
if (
|
||||||
|
dragItem.where !== 'sidebar-pin' &&
|
||||||
|
dropItem?.where === 'sidebar-pin' &&
|
||||||
|
(dragItem.kind === 'doc' || dragItem.kind === 'collection')
|
||||||
|
) {
|
||||||
|
return 'pin:add';
|
||||||
|
}
|
||||||
|
|
||||||
|
// from sidebar pin to sidebar pin (reorder)
|
||||||
|
if (
|
||||||
|
dragItem.where === 'sidebar-pin' &&
|
||||||
|
dropItem?.where === 'sidebar-pin' &&
|
||||||
|
(dragItem.kind === 'doc' || dragItem.kind === 'collection') &&
|
||||||
|
(dropItem.kind === 'doc' || dropItem.kind === 'collection')
|
||||||
|
) {
|
||||||
|
return 'pin:reorder';
|
||||||
|
}
|
||||||
|
|
||||||
|
// from sidebar pin to outside (remove from favourites)
|
||||||
|
if (
|
||||||
|
dragItem.where === 'sidebar-pin' &&
|
||||||
|
dropItem?.where !== 'sidebar-pin' &&
|
||||||
|
(dragItem.kind === 'doc' || dragItem.kind === 'collection')
|
||||||
|
) {
|
||||||
|
return 'pin:remove';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GlobalDragEndIntent = ReturnType<typeof resolveDragEndIntent>;
|
||||||
|
|
||||||
|
export const useGlobalDNDHelper = () => {
|
||||||
|
const t = useAFFiNEI18N();
|
||||||
|
const currentWorkspace = useService(Workspace);
|
||||||
|
const favAdapter = useService(FavoriteItemsAdapter);
|
||||||
|
const workspace = currentWorkspace.docCollection;
|
||||||
|
const { setTrashModal } = useTrashModalHelper(workspace);
|
||||||
|
const { getDocMeta } = useDocMetaHelper(workspace);
|
||||||
|
const collectionService = useService(CollectionService);
|
||||||
|
const collections = useLiveData(collectionService.collections$);
|
||||||
|
const deleteInfo = useDeleteCollectionInfo();
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
return {
|
||||||
|
handleDragEnd: (e: DragEndEvent) => {
|
||||||
|
const intent = resolveDragEndIntent(e.active, e.over);
|
||||||
|
|
||||||
|
const dragItem = parseDNDId(e.active.id);
|
||||||
|
const dropItem = parseDNDId(e.over?.id);
|
||||||
|
|
||||||
|
switch (intent) {
|
||||||
|
case 'pin:remove':
|
||||||
|
if (
|
||||||
|
dragItem &&
|
||||||
|
favAdapter.isFavorite(
|
||||||
|
dragItem.itemId,
|
||||||
|
dragItem.kind as 'doc' | 'collection'
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
favAdapter.remove(
|
||||||
|
dragItem.itemId,
|
||||||
|
dragItem.kind as 'doc' | 'collection'
|
||||||
|
);
|
||||||
|
toast(
|
||||||
|
t['com.affine.cmdk.affine.editor.remove-from-favourites']()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
|
||||||
|
case 'pin:reorder':
|
||||||
|
if (dragItem && dropItem) {
|
||||||
|
const fromId = FavoriteItemsAdapter.getFavItemKey(
|
||||||
|
dragItem.itemId,
|
||||||
|
dragItem.kind as 'doc' | 'collection'
|
||||||
|
);
|
||||||
|
const toId = FavoriteItemsAdapter.getFavItemKey(
|
||||||
|
dropItem.itemId,
|
||||||
|
dropItem.kind as 'doc' | 'collection'
|
||||||
|
);
|
||||||
|
favAdapter.sorter.move(fromId, toId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
case 'pin:add':
|
||||||
|
if (
|
||||||
|
dragItem &&
|
||||||
|
!favAdapter.isFavorite(
|
||||||
|
dragItem.itemId,
|
||||||
|
dragItem.kind as 'doc' | 'collection'
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
favAdapter.set(
|
||||||
|
dragItem.itemId,
|
||||||
|
dragItem.kind as 'collection' | 'doc',
|
||||||
|
true
|
||||||
|
);
|
||||||
|
toast(t['com.affine.cmdk.affine.editor.add-to-favourites']());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
|
||||||
|
case 'collection:add':
|
||||||
|
if (dragItem && dropItem) {
|
||||||
|
const pageId = dragItem.itemId;
|
||||||
|
const collectionId = dropItem.itemId;
|
||||||
|
const collection = collections.find(c => {
|
||||||
|
return c.id === collectionId;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (collection?.allowList.includes(pageId)) {
|
||||||
|
toast(t['com.affine.collection.addPage.alreadyExists']());
|
||||||
|
} else {
|
||||||
|
collectionService.addPageToCollection(collectionId, pageId);
|
||||||
|
toast(t['com.affine.collection.addPage.success']());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
|
||||||
|
case 'collection:remove':
|
||||||
|
if (dragItem) {
|
||||||
|
const pageId = dragItem.itemId;
|
||||||
|
const collId = dragItem.parent?.itemId;
|
||||||
|
if (collId) {
|
||||||
|
collectionService.deletePageFromCollection(collId, pageId);
|
||||||
|
toast(t['com.affine.collection.removePage.success']());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
|
||||||
|
case 'trash:move-to':
|
||||||
|
if (dragItem) {
|
||||||
|
const pageId = dragItem.itemId;
|
||||||
|
if (dragItem.kind === 'doc') {
|
||||||
|
const pageTitle = getDocMeta(pageId)?.title ?? t['Untitled']();
|
||||||
|
setTrashModal({
|
||||||
|
open: true,
|
||||||
|
pageIds: [pageId],
|
||||||
|
pageTitles: [pageTitle],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
collectionService.deleteCollection(deleteInfo, dragItem.itemId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
collectionService,
|
||||||
|
collections,
|
||||||
|
deleteInfo,
|
||||||
|
favAdapter,
|
||||||
|
getDocMeta,
|
||||||
|
setTrashModal,
|
||||||
|
t,
|
||||||
|
]);
|
||||||
|
};
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
import { toast } from '@affine/component';
|
|
||||||
import type { DraggableTitleCellData } from '@affine/core/components/page-list';
|
|
||||||
import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
|
|
||||||
import { FavoriteItemsAdapter } from '@affine/core/modules/workspace';
|
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
|
||||||
import type { DragEndEvent, UniqueIdentifier } from '@dnd-kit/core';
|
|
||||||
import { useService, Workspace } from '@toeverything/infra';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import { useTrashModalHelper } from './use-trash-modal-helper';
|
|
||||||
|
|
||||||
// Unique droppable IDs
|
|
||||||
export const DropPrefix = {
|
|
||||||
SidebarCollections: 'sidebar-collections-',
|
|
||||||
SidebarTrash: 'sidebar-trash',
|
|
||||||
SidebarFavorites: 'sidebar-favorites',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DragPrefix = {
|
|
||||||
PageListItem: 'page-list-item-title-',
|
|
||||||
FavouriteListItem: 'favourite-list-item-',
|
|
||||||
CollectionListItem: 'collection-list-item-',
|
|
||||||
CollectionListPageItem: 'collection-list-page-item-',
|
|
||||||
};
|
|
||||||
|
|
||||||
export function getDropItemId(
|
|
||||||
type: 'collections' | 'trash' | 'favorites',
|
|
||||||
id?: string
|
|
||||||
): string {
|
|
||||||
let prefix = '';
|
|
||||||
switch (type) {
|
|
||||||
case 'collections':
|
|
||||||
prefix = DropPrefix.SidebarCollections;
|
|
||||||
break;
|
|
||||||
case 'trash':
|
|
||||||
prefix = DropPrefix.SidebarTrash;
|
|
||||||
break;
|
|
||||||
case 'favorites':
|
|
||||||
prefix = DropPrefix.SidebarFavorites;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${prefix}${id}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDragItemId(
|
|
||||||
type: 'collection' | 'page' | 'collectionPage' | 'favouritePage',
|
|
||||||
id: string
|
|
||||||
): string {
|
|
||||||
let prefix = '';
|
|
||||||
switch (type) {
|
|
||||||
case 'collection':
|
|
||||||
prefix = DragPrefix.CollectionListItem;
|
|
||||||
break;
|
|
||||||
case 'page':
|
|
||||||
prefix = DragPrefix.PageListItem;
|
|
||||||
break;
|
|
||||||
case 'collectionPage':
|
|
||||||
prefix = DragPrefix.CollectionListPageItem;
|
|
||||||
break;
|
|
||||||
case 'favouritePage':
|
|
||||||
prefix = DragPrefix.FavouriteListItem;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${prefix}${id}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useSidebarDrag = () => {
|
|
||||||
const t = useAFFiNEI18N();
|
|
||||||
const currentWorkspace = useService(Workspace);
|
|
||||||
const favAdapter = useService(FavoriteItemsAdapter);
|
|
||||||
const workspace = currentWorkspace.docCollection;
|
|
||||||
const { setTrashModal } = useTrashModalHelper(workspace);
|
|
||||||
|
|
||||||
const { getDocMeta } = useDocMetaHelper(workspace);
|
|
||||||
|
|
||||||
const isDropArea = useCallback(
|
|
||||||
(id: UniqueIdentifier | undefined, prefix: string) => {
|
|
||||||
return typeof id === 'string' && id.startsWith(prefix);
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const processDrag = useCallback(
|
|
||||||
(e: DragEndEvent, dropPrefix: string, action: (pageId: string) => void) => {
|
|
||||||
const validPrefixes = Object.values(DragPrefix);
|
|
||||||
const isActiveIdValid = validPrefixes.some(pref =>
|
|
||||||
String(e.active.id).startsWith(pref)
|
|
||||||
);
|
|
||||||
if (isDropArea(e.over?.id, dropPrefix) && isActiveIdValid) {
|
|
||||||
const { pageId } = e.active.data.current as DraggableTitleCellData;
|
|
||||||
action(pageId);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
[isDropArea]
|
|
||||||
);
|
|
||||||
|
|
||||||
const processCollectionsDrag = useCallback(
|
|
||||||
(e: DragEndEvent) =>
|
|
||||||
processDrag(e, DropPrefix.SidebarCollections, pageId => {
|
|
||||||
e.over?.data.current?.addToCollection?.(pageId);
|
|
||||||
}),
|
|
||||||
[processDrag]
|
|
||||||
);
|
|
||||||
|
|
||||||
const processMoveToTrashDrag = useCallback(
|
|
||||||
(e: DragEndEvent) => {
|
|
||||||
const { pageId } = e.active.data.current as DraggableTitleCellData;
|
|
||||||
const pageTitle = getDocMeta(pageId)?.title ?? t['Untitled']();
|
|
||||||
processDrag(e, DropPrefix.SidebarTrash, pageId => {
|
|
||||||
setTrashModal({
|
|
||||||
open: true,
|
|
||||||
pageIds: [pageId],
|
|
||||||
pageTitles: [pageTitle],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[getDocMeta, processDrag, setTrashModal, t]
|
|
||||||
);
|
|
||||||
|
|
||||||
const processFavouritesDrag = useCallback(
|
|
||||||
(e: DragEndEvent) => {
|
|
||||||
const { pageId } = e.active.data.current as DraggableTitleCellData;
|
|
||||||
const isFavourited = favAdapter.isFavorite(pageId, 'doc');
|
|
||||||
const isFavouriteDrag = String(e.over?.id).startsWith(
|
|
||||||
DropPrefix.SidebarFavorites
|
|
||||||
);
|
|
||||||
if (isFavourited && isFavouriteDrag) {
|
|
||||||
return toast(t['com.affine.collection.addPage.alreadyExists']());
|
|
||||||
}
|
|
||||||
processDrag(e, DropPrefix.SidebarFavorites, pageId => {
|
|
||||||
favAdapter.set(pageId, 'doc', true);
|
|
||||||
toast(t['com.affine.cmdk.affine.editor.add-to-favourites']());
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[processDrag, t, favAdapter]
|
|
||||||
);
|
|
||||||
|
|
||||||
const processRemoveDrag = useCallback(
|
|
||||||
(e: DragEndEvent) => {
|
|
||||||
if (e.over) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (String(e.active.id).startsWith(DragPrefix.FavouriteListItem)) {
|
|
||||||
const pageId = e.active.data.current?.pageId;
|
|
||||||
favAdapter.remove(pageId, 'doc');
|
|
||||||
toast(t['com.affine.cmdk.affine.editor.remove-from-favourites']());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (String(e.active.id).startsWith(DragPrefix.CollectionListPageItem)) {
|
|
||||||
return e.active.data.current?.removeFromCollection?.();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
[favAdapter, t]
|
|
||||||
);
|
|
||||||
|
|
||||||
return useCallback(
|
|
||||||
(e: DragEndEvent) => {
|
|
||||||
processCollectionsDrag(e);
|
|
||||||
processFavouritesDrag(e);
|
|
||||||
processMoveToTrashDrag(e);
|
|
||||||
processRemoveDrag(e);
|
|
||||||
},
|
|
||||||
[
|
|
||||||
processCollectionsDrag,
|
|
||||||
processFavouritesDrag,
|
|
||||||
processMoveToTrashDrag,
|
|
||||||
processRemoveDrag,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
19
packages/frontend/core/src/layouts/styles.css.ts
Normal file
19
packages/frontend/core/src/layouts/styles.css.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const dragOverlay = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
zIndex: 1001,
|
||||||
|
cursor: 'grabbing',
|
||||||
|
maxWidth: '360px',
|
||||||
|
transition: 'transform 0.2s, opacity 0.2s',
|
||||||
|
willChange: 'transform opacity',
|
||||||
|
selectors: {
|
||||||
|
'&[data-over-drop=true]': {
|
||||||
|
transform: 'scale(0.8)',
|
||||||
|
},
|
||||||
|
'&[data-sorting=true]': {
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
DndContext,
|
DndContext,
|
||||||
DragOverlay,
|
DragOverlay,
|
||||||
MouseSensor,
|
MouseSensor,
|
||||||
pointerWithin,
|
|
||||||
useDndContext,
|
useDndContext,
|
||||||
useSensor,
|
useSensor,
|
||||||
useSensors,
|
useSensors,
|
||||||
@@ -18,6 +17,7 @@ import {
|
|||||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||||
import type { PropsWithChildren, ReactNode } from 'react';
|
import type { PropsWithChildren, ReactNode } from 'react';
|
||||||
import { lazy, Suspense, useCallback, useEffect, useState } from 'react';
|
import { lazy, Suspense, useCallback, useEffect, useState } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import { matchPath } from 'react-router-dom';
|
import { matchPath } from 'react-router-dom';
|
||||||
import { Map as YMap } from 'yjs';
|
import { Map as YMap } from 'yjs';
|
||||||
|
|
||||||
@@ -30,12 +30,14 @@ import {
|
|||||||
} from '../components/app-sidebar';
|
} from '../components/app-sidebar';
|
||||||
import { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils';
|
import { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils';
|
||||||
import type { DraggableTitleCellData } from '../components/page-list';
|
import type { DraggableTitleCellData } from '../components/page-list';
|
||||||
import { PageListDragOverlay } from '../components/page-list';
|
|
||||||
import { RootAppSidebar } from '../components/root-app-sidebar';
|
import { RootAppSidebar } from '../components/root-app-sidebar';
|
||||||
import { MainContainer, WorkspaceFallback } from '../components/workspace';
|
import { MainContainer, WorkspaceFallback } from '../components/workspace';
|
||||||
import { WorkspaceUpgrade } from '../components/workspace-upgrade';
|
import { WorkspaceUpgrade } from '../components/workspace-upgrade';
|
||||||
import { useAppSettingHelper } from '../hooks/affine/use-app-setting-helper';
|
import { useAppSettingHelper } from '../hooks/affine/use-app-setting-helper';
|
||||||
import { useSidebarDrag } from '../hooks/affine/use-sidebar-drag';
|
import {
|
||||||
|
resolveDragEndIntent,
|
||||||
|
useGlobalDNDHelper,
|
||||||
|
} from '../hooks/affine/use-global-dnd-helper';
|
||||||
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||||
import { useRegisterWorkspaceCommands } from '../hooks/use-register-workspace-commands';
|
import { useRegisterWorkspaceCommands } from '../hooks/use-register-workspace-commands';
|
||||||
import { Workbench } from '../modules/workbench';
|
import { Workbench } from '../modules/workbench';
|
||||||
@@ -45,6 +47,7 @@ import {
|
|||||||
} from '../providers/modal-provider';
|
} from '../providers/modal-provider';
|
||||||
import { SWRConfigProvider } from '../providers/swr-config-provider';
|
import { SWRConfigProvider } from '../providers/swr-config-provider';
|
||||||
import { pathGenerator } from '../shared';
|
import { pathGenerator } from '../shared';
|
||||||
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
const CMDKQuickSearchModal = lazy(() =>
|
const CMDKQuickSearchModal = lazy(() =>
|
||||||
import('../components/pure/cmdk').then(module => ({
|
import('../components/pure/cmdk').then(module => ({
|
||||||
@@ -149,20 +152,14 @@ export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDragEnd = useSidebarDrag();
|
const { handleDragEnd } = useGlobalDNDHelper();
|
||||||
|
|
||||||
const { appSettings } = useAppSettingHelper();
|
const { appSettings } = useAppSettingHelper();
|
||||||
|
|
||||||
const upgradeStatus = useWorkspaceStatus(currentWorkspace, s => s.upgrade);
|
const upgradeStatus = useWorkspaceStatus(currentWorkspace, s => s.upgrade);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* This DndContext is used for drag page from all-pages list into a folder in sidebar */}
|
{/* This DndContext is used for drag page from all-pages list into a folder in sidebar */}
|
||||||
<DndContext
|
<DndContext sensors={sensors} onDragEnd={handleDragEnd}>
|
||||||
sensors={sensors}
|
|
||||||
collisionDetection={pointerWithin}
|
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
>
|
|
||||||
<AppContainer resizing={resizing}>
|
<AppContainer resizing={resizing}>
|
||||||
<Suspense fallback={<AppSidebarFallback />}>
|
<Suspense fallback={<AppSidebarFallback />}>
|
||||||
<RootAppSidebar
|
<RootAppSidebar
|
||||||
@@ -191,7 +188,7 @@ export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
</MainContainer>
|
</MainContainer>
|
||||||
</AppContainer>
|
</AppContainer>
|
||||||
<PageListTitleCellDragOverlay />
|
<GlobalDragOverlay />
|
||||||
</DndContext>
|
</DndContext>
|
||||||
<QuickSearch />
|
<QuickSearch />
|
||||||
<SyncAwareness />
|
<SyncAwareness />
|
||||||
@@ -199,26 +196,48 @@ export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function PageListTitleCellDragOverlay() {
|
function GlobalDragOverlay() {
|
||||||
const { active, over } = useDndContext();
|
const { active, over } = useDndContext();
|
||||||
const [content, setContent] = useState<ReactNode>();
|
const [preview, setPreview] = useState<ReactNode>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (active) {
|
if (active) {
|
||||||
const data = active.data.current as DraggableTitleCellData;
|
const data = active.data.current as DraggableTitleCellData;
|
||||||
setContent(data.pageTitle);
|
setPreview(data.preview);
|
||||||
}
|
}
|
||||||
// do not update content since it may disappear because of virtual rendering
|
// do not update content since it may disappear because of virtual rendering
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [active?.id]);
|
}, [active?.id]);
|
||||||
|
|
||||||
const renderChildren = useCallback(() => {
|
const intent = resolveDragEndIntent(active, over);
|
||||||
return <PageListDragOverlay over={!!over}>{content}</PageListDragOverlay>;
|
|
||||||
}, [content, over]);
|
|
||||||
|
|
||||||
return (
|
const overDropZone =
|
||||||
<DragOverlay dropAnimation={null}>
|
intent === 'pin:add' ||
|
||||||
{active ? renderChildren() : null}
|
intent === 'collection:add' ||
|
||||||
</DragOverlay>
|
intent === 'trash:move-to';
|
||||||
|
|
||||||
|
const accent =
|
||||||
|
intent === 'pin:remove'
|
||||||
|
? 'warning'
|
||||||
|
: intent === 'trash:move-to'
|
||||||
|
? 'error'
|
||||||
|
: 'normal';
|
||||||
|
|
||||||
|
const sorting = intent === 'pin:reorder';
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<DragOverlay adjustScale={false} dropAnimation={null}>
|
||||||
|
{preview ? (
|
||||||
|
<div
|
||||||
|
data-over-drop={overDropZone}
|
||||||
|
data-sorting={sorting}
|
||||||
|
data-accent={accent}
|
||||||
|
className={styles.dragOverlay}
|
||||||
|
>
|
||||||
|
{preview}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</DragOverlay>,
|
||||||
|
document.body
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,15 @@ export class CollectionService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deletePageFromCollection(collectionId: string, pageId: string) {
|
||||||
|
this.updateCollection(collectionId, old => {
|
||||||
|
return {
|
||||||
|
...old,
|
||||||
|
allowList: old.allowList?.filter(id => id !== pageId),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
deleteCollection(info: DeleteCollectionInfo, ...ids: string[]) {
|
deleteCollection(info: DeleteCollectionInfo, ...ids: string[]) {
|
||||||
const collectionsYArray = this.collectionsYArray;
|
const collectionsYArray = this.collectionsYArray;
|
||||||
if (!collectionsYArray) {
|
if (!collectionsYArray) {
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ export class FavoriteItemsAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getItemId(item: WorkspaceFavoriteItem) {
|
getItemId(item: WorkspaceFavoriteItem) {
|
||||||
return item.id;
|
return FavoriteItemsAdapter.getFavItemKey(item.id, item.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
getItemOrder(item: WorkspaceFavoriteItem) {
|
getItemOrder(item: WorkspaceFavoriteItem) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable unicorn/prefer-dom-node-dataset */
|
||||||
import { test } from '@affine-test/kit/playwright';
|
import { test } from '@affine-test/kit/playwright';
|
||||||
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
||||||
import {
|
import {
|
||||||
@@ -13,37 +14,58 @@ import { expect } from '@playwright/test';
|
|||||||
const dragToFavourites = async (
|
const dragToFavourites = async (
|
||||||
page: Page,
|
page: Page,
|
||||||
dragItem: Locator,
|
dragItem: Locator,
|
||||||
pageId: string
|
id: string,
|
||||||
|
type: 'page' | 'collection' = 'page'
|
||||||
) => {
|
) => {
|
||||||
const favourites = page.getByTestId('favourites');
|
const favourites = page.getByTestId('favourites');
|
||||||
await dragTo(page, dragItem, favourites);
|
await dragTo(page, dragItem, favourites);
|
||||||
const favouritePage = page.getByTestId(`favourite-page-${pageId}`);
|
if (type === 'collection') {
|
||||||
expect(favouritePage).not.toBeUndefined();
|
const collection = page
|
||||||
return favouritePage;
|
.getByTestId(`favourites`)
|
||||||
|
.locator(`[data-collection-id="${id}"]`);
|
||||||
|
await expect(collection).toBeVisible();
|
||||||
|
return collection;
|
||||||
|
} else {
|
||||||
|
const favouritePage = page.getByTestId(`favourite-page-${id}`);
|
||||||
|
await expect(favouritePage).toBeVisible();
|
||||||
|
return favouritePage;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const dragToCollection = async (page: Page, dragItem: Locator) => {
|
const createCollection = async (page: Page, name: string) => {
|
||||||
await page.getByTestId('slider-bar-add-collection-button').click();
|
await page.getByTestId('slider-bar-add-collection-button').click();
|
||||||
const input = page.getByTestId('input-collection-title');
|
const input = page.getByTestId('input-collection-title');
|
||||||
await expect(input).toBeVisible();
|
await expect(input).toBeVisible();
|
||||||
await input.fill('test collection');
|
await input.fill(name);
|
||||||
await page.getByTestId('save-collection').click();
|
await page.getByTestId('save-collection').click();
|
||||||
const collection = page.getByTestId('collection-item');
|
const collection = page.locator(
|
||||||
expect(collection).not.toBeUndefined();
|
`[data-testid=collection-item]:has-text("${name}")`
|
||||||
|
);
|
||||||
|
await expect(collection).toBeVisible();
|
||||||
|
return collection;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createPage = async (page: Page, title: string) => {
|
||||||
|
await clickNewPageButton(page);
|
||||||
|
await getBlockSuiteEditorTitle(page).fill(title);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dragToCollection = async (page: Page, dragItem: Locator) => {
|
||||||
|
const collection = await createCollection(page, 'test collection');
|
||||||
await clickSideBarAllPageButton(page);
|
await clickSideBarAllPageButton(page);
|
||||||
await dragTo(page, dragItem, collection);
|
await dragTo(page, dragItem, collection);
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
await collection.getByTestId('fav-collapsed-button').click();
|
await collection.getByTestId('fav-collapsed-button').click();
|
||||||
const collectionPage = page.getByTestId('collection-page');
|
const collectionPage = page.getByTestId('collection-page');
|
||||||
expect(collectionPage).not.toBeUndefined();
|
await expect(collectionPage).toBeVisible();
|
||||||
return collectionPage;
|
return collectionPage;
|
||||||
};
|
};
|
||||||
|
|
||||||
const dragToTrash = async (page: Page, title: string, dragItem: Locator) => {
|
const dragToTrash = async (page: Page, title: string, dragItem: Locator) => {
|
||||||
// drag to trash
|
// drag to trash
|
||||||
await dragTo(page, dragItem, page.getByTestId('trash-page'));
|
await dragTo(page, dragItem, page.getByTestId('trash-page'));
|
||||||
const confirmTip = page.getByText('Delete page?');
|
const confirmTip = page.getByText('Delete doc?');
|
||||||
expect(confirmTip).not.toBeUndefined();
|
await expect(confirmTip).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Delete' }).click();
|
await page.getByRole('button', { name: 'Delete' }).click();
|
||||||
|
|
||||||
@@ -60,16 +82,17 @@ const dragToTrash = async (page: Page, title: string, dragItem: Locator) => {
|
|||||||
).toHaveCount(1);
|
).toHaveCount(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await openHomePage(page);
|
||||||
|
await waitForEditorLoad(page);
|
||||||
|
});
|
||||||
|
|
||||||
test('drag a page from "All pages" list to favourites, then drag to trash', async ({
|
test('drag a page from "All pages" list to favourites, then drag to trash', async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const title = 'this is a new page to drag';
|
const title = 'this is a new page to drag';
|
||||||
{
|
await waitForEditorLoad(page);
|
||||||
await openHomePage(page);
|
await createPage(page, title);
|
||||||
await waitForEditorLoad(page);
|
|
||||||
await clickNewPageButton(page);
|
|
||||||
await getBlockSuiteEditorTitle(page).fill(title);
|
|
||||||
}
|
|
||||||
const pageId = page.url().split('/').reverse()[0];
|
const pageId = page.url().split('/').reverse()[0];
|
||||||
await clickSideBarAllPageButton(page);
|
await clickSideBarAllPageButton(page);
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
@@ -87,12 +110,8 @@ test('drag a page from "All pages" list to collections, then drag to trash', asy
|
|||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const title = 'this is a new page to drag';
|
const title = 'this is a new page to drag';
|
||||||
{
|
await waitForEditorLoad(page);
|
||||||
await openHomePage(page);
|
await createPage(page, title);
|
||||||
await waitForEditorLoad(page);
|
|
||||||
await clickNewPageButton(page);
|
|
||||||
await getBlockSuiteEditorTitle(page).fill(title);
|
|
||||||
}
|
|
||||||
await clickSideBarAllPageButton(page);
|
await clickSideBarAllPageButton(page);
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
@@ -106,12 +125,8 @@ test('drag a page from "All pages" list to collections, then drag to trash', asy
|
|||||||
|
|
||||||
test('drag a page from "All pages" list to trash', async ({ page }) => {
|
test('drag a page from "All pages" list to trash', async ({ page }) => {
|
||||||
const title = 'this is a new page to drag';
|
const title = 'this is a new page to drag';
|
||||||
{
|
await createPage(page, title);
|
||||||
await openHomePage(page);
|
|
||||||
await waitForEditorLoad(page);
|
|
||||||
await clickNewPageButton(page);
|
|
||||||
await getBlockSuiteEditorTitle(page).fill(title);
|
|
||||||
}
|
|
||||||
await clickSideBarAllPageButton(page);
|
await clickSideBarAllPageButton(page);
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
@@ -124,12 +139,8 @@ test('drag a page from "All pages" list to trash', async ({ page }) => {
|
|||||||
|
|
||||||
test('drag a page from favourites to collection', async ({ page }) => {
|
test('drag a page from favourites to collection', async ({ page }) => {
|
||||||
const title = 'this is a new page to drag';
|
const title = 'this is a new page to drag';
|
||||||
{
|
await createPage(page, title);
|
||||||
await openHomePage(page);
|
|
||||||
await waitForEditorLoad(page);
|
|
||||||
await clickNewPageButton(page);
|
|
||||||
await getBlockSuiteEditorTitle(page).fill(title);
|
|
||||||
}
|
|
||||||
const pageId = page.url().split('/').reverse()[0];
|
const pageId = page.url().split('/').reverse()[0];
|
||||||
await clickSideBarAllPageButton(page);
|
await clickSideBarAllPageButton(page);
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
@@ -144,3 +155,68 @@ test('drag a page from favourites to collection', async ({ page }) => {
|
|||||||
// drag to collections
|
// drag to collections
|
||||||
await dragToCollection(page, favouritePage);
|
await dragToCollection(page, favouritePage);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('drag a collection to favourites', async ({ page }) => {
|
||||||
|
await clickSideBarAllPageButton(page);
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
const collection = await createCollection(page, 'test collection');
|
||||||
|
const collectionId = (await collection.getAttribute(
|
||||||
|
'data-collection-id'
|
||||||
|
)) as string;
|
||||||
|
await dragToFavourites(page, collection, collectionId, 'collection');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('items in favourites can be reordered by dragging', async ({ page }) => {
|
||||||
|
const title0 = 'this is a new page to drag';
|
||||||
|
await createPage(page, title0);
|
||||||
|
await page.getByTestId('pin-button').click();
|
||||||
|
|
||||||
|
const title1 = 'this is another new page to drag';
|
||||||
|
await createPage(page, title1);
|
||||||
|
await page.getByTestId('pin-button').click();
|
||||||
|
|
||||||
|
{
|
||||||
|
const collection = await createCollection(page, 'test collection');
|
||||||
|
const collectionId = (await collection.getAttribute(
|
||||||
|
'data-collection-id'
|
||||||
|
)) as string;
|
||||||
|
await dragToFavourites(page, collection, collectionId, 'collection');
|
||||||
|
}
|
||||||
|
|
||||||
|
// assert the order of the items in favourites
|
||||||
|
await expect(
|
||||||
|
page.getByTestId('favourites').locator('[data-draggable]')
|
||||||
|
).toHaveCount(3);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByTestId('favourites').locator('[data-draggable]').first()
|
||||||
|
).toHaveText(title0);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByTestId('favourites').locator('[data-draggable]').last()
|
||||||
|
).toHaveText('test collection');
|
||||||
|
|
||||||
|
// drag the first item to the last
|
||||||
|
const firstItem = page
|
||||||
|
.getByTestId('favourites')
|
||||||
|
.locator('[data-draggable]')
|
||||||
|
.first();
|
||||||
|
const lastItem = page
|
||||||
|
.getByTestId('favourites')
|
||||||
|
.locator('[data-draggable]')
|
||||||
|
.last();
|
||||||
|
await dragTo(page, firstItem, lastItem);
|
||||||
|
|
||||||
|
// now check the order again
|
||||||
|
await expect(
|
||||||
|
page.getByTestId('favourites').locator('[data-draggable]')
|
||||||
|
).toHaveCount(3);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByTestId('favourites').locator('[data-draggable]').first()
|
||||||
|
).toHaveText(title1);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByTestId('favourites').locator('[data-draggable]').last()
|
||||||
|
).toHaveText(title0);
|
||||||
|
});
|
||||||
|
|||||||
@@ -144,8 +144,8 @@ test('Add new favorite page via sidebar', async ({ page }) => {
|
|||||||
// enter random page title
|
// enter random page title
|
||||||
await getBlockSuiteEditorTitle(page).fill('this is a new fav page');
|
await getBlockSuiteEditorTitle(page).fill('this is a new fav page');
|
||||||
// check if the page title is shown in the favorite list
|
// check if the page title is shown in the favorite list
|
||||||
const favItem = page.locator(
|
const favItem = page
|
||||||
'[data-type=favourite-list-item] >> text=this is a new fav page'
|
.getByTestId('favourites')
|
||||||
);
|
.locator('[data-draggable] >> text=this is a new fav page');
|
||||||
await expect(favItem).toBeVisible();
|
await expect(favItem).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -337,6 +337,7 @@ __metadata:
|
|||||||
"@dnd-kit/core": "npm:^6.1.0"
|
"@dnd-kit/core": "npm:^6.1.0"
|
||||||
"@dnd-kit/modifiers": "npm:^7.0.0"
|
"@dnd-kit/modifiers": "npm:^7.0.0"
|
||||||
"@dnd-kit/sortable": "npm:^8.0.0"
|
"@dnd-kit/sortable": "npm:^8.0.0"
|
||||||
|
"@dnd-kit/utilities": "npm:^3.2.2"
|
||||||
"@emotion/cache": "npm:^11.11.0"
|
"@emotion/cache": "npm:^11.11.0"
|
||||||
"@emotion/react": "npm:^11.11.3"
|
"@emotion/react": "npm:^11.11.3"
|
||||||
"@emotion/server": "npm:^11.11.0"
|
"@emotion/server": "npm:^11.11.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user