();
export const FavoriteList = ({ workspace }: FavoriteListProps) => {
const metas = useBlockSuitePageMeta(workspace);
+ const dropItemId = getDropItemId('favorites');
const favoriteList = useMemo(
() => metas.filter(p => p.favorite && !p.trash),
@@ -28,11 +32,20 @@ export const FavoriteList = ({ workspace }: FavoriteListProps) => {
[metas]
);
+ const { setNodeRef, isOver } = useDroppable({
+ id: dropItemId,
+ });
+
return (
- <>
+
{favoriteList.map((pageMeta, index) => {
return (
- {
);
})}
{favoriteList.length === 0 && }
- >
+
);
};
diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/favourite-page.tsx b/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/favourite-page.tsx
new file mode 100644
index 0000000000..bc32f778c5
--- /dev/null
+++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/favourite-page.tsx
@@ -0,0 +1,113 @@
+import { MenuLinkItem } from '@affine/component/app-sidebar';
+import { useAFFiNEI18N } from '@affine/i18n/hooks';
+import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
+import { useDraggable } from '@dnd-kit/core';
+import * as Collapsible from '@radix-ui/react-collapsible';
+import { useBlockSuitePageReferences } from '@toeverything/hooks/use-block-suite-page-references';
+import { useAtomValue } from 'jotai/index';
+import { useMemo, useState } from 'react';
+import { useParams } from 'react-router-dom';
+
+import { pageSettingFamily } from '../../../../atoms';
+import { getDragItemId } from '../../../../hooks/affine/use-sidebar-drag';
+import { DragMenuItemOverlay } from '../components/drag-menu-item-overlay';
+import { PostfixItem } from '../components/postfix-item';
+import {
+ ReferencePage,
+ type ReferencePageProps,
+} from '../components/reference-page';
+import * as styles from './styles.css';
+
+export const FavouritePage = ({
+ workspace,
+ pageId,
+ metaMapping,
+ parentIds,
+}: ReferencePageProps) => {
+ const t = useAFFiNEI18N();
+ const params = useParams();
+ const active = params.pageId === pageId;
+ const dragItemId = getDragItemId('favouritePage', pageId);
+
+ const setting = useAtomValue(pageSettingFamily(pageId));
+ const icon = useMemo(() => {
+ return setting?.mode === 'edgeless' ? : ;
+ }, [setting?.mode]);
+
+ const references = useBlockSuitePageReferences(workspace, pageId);
+ const referencesToShow = useMemo(() => {
+ return [
+ ...new Set(
+ references.filter(ref => metaMapping[ref] && !metaMapping[ref]?.trash)
+ ),
+ ];
+ }, [references, metaMapping]);
+
+ 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 ;
+ }, [icon, pageTitle]);
+
+ const { setNodeRef, attributes, listeners, isDragging } = useDraggable({
+ id: dragItemId,
+ data: {
+ pageId,
+ pageTitle: pageTitleElement,
+ },
+ });
+
+ return (
+
+
+ }
+ >
+
+ {pageTitle}
+
+
+
+ {referencesToShow.map(id => {
+ return (
+
+ );
+ })}
+
+
+ );
+};
diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/styles.css.ts b/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/styles.css.ts
index be889f6b87..db163395a7 100644
--- a/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/styles.css.ts
+++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/favorite/styles.css.ts
@@ -1,4 +1,4 @@
-import { keyframes, style } from '@vanilla-extract/css';
+import { globalStyle, keyframes, style } from '@vanilla-extract/css';
export const label = style({
selectors: {
@@ -57,3 +57,89 @@ 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: 'var(--affine-placeholder-color)',
+ 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',
+ background: 'var(--affine-hover-color-filled)',
+ boxShadow: 'var(--affine-menu-shadow)',
+ minHeight: '30px',
+ maxWidth: '360px',
+ width: '100%',
+ fontSize: 'var(--affine-font-sm)',
+ gap: '8px',
+ padding: '4px',
+ borderRadius: '4px',
+ cursor: 'grabbing',
+});
+
+globalStyle(`${dragPageItemOverlay} svg`, {
+ width: '20px',
+ height: '20px',
+ color: 'var(--affine-icon-color)',
+});
+
+globalStyle(`${dragPageItemOverlay} span`, {
+ textOverflow: 'ellipsis',
+ whiteSpace: 'nowrap',
+ overflow: 'hidden',
+});
+
+export const favoriteList = style({
+ selectors: {
+ '&[data-over="true"]': {
+ background: 'var(--affine-hover-color-filled)',
+ borderRadius: '4px',
+ },
+ },
+});
+
+export const favoritePostfixItem = style({
+ display: 'flex',
+ alignItems: 'center',
+});
+
+export const menuItem = style({
+ gap: '8px',
+});
+
+globalStyle(`${menuItem} svg`, {
+ width: '20px',
+ height: '20px',
+ color: 'var(--affine-icon-color)',
+});
+globalStyle(`${menuItem}.danger:hover svg`, {
+ color: 'var(--affine-error-color)',
+});
diff --git a/packages/frontend/core/src/components/root-app-sidebar/index.tsx b/packages/frontend/core/src/components/root-app-sidebar/index.tsx
index ea4e0e9572..76dd5b15cc 100644
--- a/packages/frontend/core/src/components/root-app-sidebar/index.tsx
+++ b/packages/frontend/core/src/components/root-app-sidebar/index.tsx
@@ -36,6 +36,7 @@ import { useHistoryAtom } from '../../atoms/history';
import { useAppSettingHelper } from '../../hooks/affine/use-app-setting-helper';
import { useDeleteCollectionInfo } from '../../hooks/affine/use-delete-collection-info';
import { useGeneralShortcuts } from '../../hooks/affine/use-shortcuts';
+import { getDropItemId } from '../../hooks/affine/use-sidebar-drag';
import { useTrashModalHelper } from '../../hooks/affine/use-trash-modal-helper';
import { useRegisterBrowserHistoryCommands } from '../../hooks/use-browser-history-commands';
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
@@ -89,9 +90,6 @@ const RouteMenuLinkItem = forwardRef<
});
RouteMenuLinkItem.displayName = 'RouteMenuLinkItem';
-// Unique droppable IDs
-export const DROPPABLE_SIDEBAR_TRASH = 'trash-folder';
-
/**
* This is for the whole affine app sidebar.
* This component wraps the app sidebar in `@affine/component` with logic and data.
@@ -170,8 +168,9 @@ export const RootAppSidebar = ({
};
}, [history, setHistory]);
+ const dropItemId = getDropItemId('trash');
const trashDroppable = useDroppable({
- id: DROPPABLE_SIDEBAR_TRASH,
+ id: dropItemId,
});
const closeUserWorkspaceList = useCallback(() => {
setOpenUserWorkspaceList(false);
diff --git a/packages/frontend/core/src/hooks/affine/use-sidebar-drag.ts b/packages/frontend/core/src/hooks/affine/use-sidebar-drag.ts
new file mode 100644
index 0000000000..367354e0cf
--- /dev/null
+++ b/packages/frontend/core/src/hooks/affine/use-sidebar-drag.ts
@@ -0,0 +1,175 @@
+import { toast } from '@affine/component';
+import type { DraggableTitleCellData } from '@affine/component/page-list';
+import { useAFFiNEI18N } from '@affine/i18n/hooks';
+import type { DragEndEvent, UniqueIdentifier } from '@dnd-kit/core';
+import { usePageMetaHelper } from '@toeverything/hooks/use-block-suite-page-meta';
+import { useCallback } from 'react';
+
+import { useCurrentWorkspace } from '../current/use-current-workspace';
+import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
+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] = useCurrentWorkspace();
+ const workspace = currentWorkspace.blockSuiteWorkspace;
+ const { setTrashModal } = useTrashModalHelper(workspace);
+ const { addToFavorite, removeFromFavorite } =
+ useBlockSuiteMetaHelper(workspace);
+ const { getPageMeta } = usePageMetaHelper(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 = getPageMeta(pageId)?.title ?? t['Untitled']();
+ processDrag(e, DropPrefix.SidebarTrash, pageId => {
+ setTrashModal({
+ open: true,
+ pageIds: [pageId],
+ pageTitles: [pageTitle],
+ });
+ });
+ },
+ [getPageMeta, processDrag, setTrashModal, t]
+ );
+
+ const processFavouritesDrag = useCallback(
+ (e: DragEndEvent) => {
+ const { pageId } = e.active.data.current as DraggableTitleCellData;
+ const isFavourited = getPageMeta(pageId)?.favorite;
+ 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 => {
+ addToFavorite(pageId);
+ toast(t['com.affine.cmdk.affine.editor.add-to-favourites']());
+ });
+ },
+ [getPageMeta, processDrag, addToFavorite, t]
+ );
+
+ const processRemoveDrag = useCallback(
+ (e: DragEndEvent) => {
+ if (e.over) {
+ return;
+ }
+
+ if (String(e.active.id).startsWith(DragPrefix.FavouriteListItem)) {
+ const pageId = e.active.data.current?.pageId;
+ removeFromFavorite(pageId);
+ 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?.();
+ }
+ },
+
+ [removeFromFavorite, t]
+ );
+
+ return useCallback(
+ (e: DragEndEvent) => {
+ processCollectionsDrag(e);
+ processFavouritesDrag(e);
+ processMoveToTrashDrag(e);
+ processRemoveDrag(e);
+ },
+ [
+ processCollectionsDrag,
+ processFavouritesDrag,
+ processMoveToTrashDrag,
+ processRemoveDrag,
+ ]
+ );
+};
diff --git a/packages/frontend/core/src/layouts/workspace-layout.tsx b/packages/frontend/core/src/layouts/workspace-layout.tsx
index ab848eeb00..f3d482f720 100644
--- a/packages/frontend/core/src/layouts/workspace-layout.tsx
+++ b/packages/frontend/core/src/layouts/workspace-layout.tsx
@@ -7,11 +7,9 @@ import {
PageListDragOverlay,
} from '@affine/component/page-list';
import { MainContainer, WorkspaceFallback } from '@affine/component/workspace';
-import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { getBlobEngine } from '@affine/workspace/manager';
import { assertExists } from '@blocksuite/global/utils';
-import type { DragEndEvent } from '@dnd-kit/core';
import {
DndContext,
DragOverlay,
@@ -35,14 +33,10 @@ import { AdapterProviderWrapper } from '../components/adapter-worksapce-wrapper'
import { AppContainer } from '../components/affine/app-container';
import { SyncAwareness } from '../components/affine/awareness';
import { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils';
-import { processCollectionsDrag } from '../components/pure/workspace-slider-bar/collections';
-import {
- DROPPABLE_SIDEBAR_TRASH,
- RootAppSidebar,
-} from '../components/root-app-sidebar';
+import { RootAppSidebar } from '../components/root-app-sidebar';
import { WorkspaceUpgrade } from '../components/workspace-upgrade';
import { useAppSettingHelper } from '../hooks/affine/use-app-setting-helper';
-import { useBlockSuiteMetaHelper } from '../hooks/affine/use-block-suite-meta-helper';
+import { useSidebarDrag } from '../hooks/affine/use-sidebar-drag';
import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
import { useNavigateHelper } from '../hooks/use-navigate-helper';
import { useRegisterWorkspaceCommands } from '../hooks/use-register-workspace-commands';
@@ -51,7 +45,6 @@ import {
CurrentWorkspaceModals,
} from '../providers/modal-provider';
import { pathGenerator } from '../shared';
-import { toast } from '../utils';
const CMDKQuickSearchModal = lazy(() =>
import('../components/pure/cmdk').then(module => ({
@@ -169,7 +162,6 @@ export const WorkspaceLayoutInner = ({
const [currentWorkspace] = useCurrentWorkspace();
const { openPage } = useNavigateHelper();
const pageHelper = usePageHelper(currentWorkspace.blockSuiteWorkspace);
- const t = useAFFiNEI18N();
useRegisterWorkspaceCommands();
@@ -223,28 +215,7 @@ export const WorkspaceLayoutInner = ({
})
);
- const { removeToTrash: moveToTrash } = useBlockSuiteMetaHelper(
- currentWorkspace.blockSuiteWorkspace
- );
-
- const handleDragEnd = useCallback(
- (e: DragEndEvent) => {
- // Drag page into trash folder
- if (
- e.over?.id === DROPPABLE_SIDEBAR_TRASH &&
- String(e.active.id).startsWith('page-list-item-')
- ) {
- const { pageId } = e.active.data.current as DraggableTitleCellData;
- // TODO-Doma
- // Co-locate `moveToTrash` with the toast for reuse, as they're always used together
- moveToTrash(pageId);
- toast(t['com.affine.toastMessage.successfullyDeleted']());
- }
- // Drag page into Collections
- processCollectionsDrag(e);
- },
- [moveToTrash, t]
- );
+ const handleDragEnd = useSidebarDrag();
const { appSettings } = useAppSettingHelper();
const location = useLocation();
diff --git a/packages/frontend/core/src/pages/workspace/trash-page.tsx b/packages/frontend/core/src/pages/workspace/trash-page.tsx
index 82f2a87f29..a4b4d5b044 100644
--- a/packages/frontend/core/src/pages/workspace/trash-page.tsx
+++ b/packages/frontend/core/src/pages/workspace/trash-page.tsx
@@ -1,5 +1,6 @@
import { toast } from '@affine/component';
import {
+ currentCollectionAtom,
TrashOperationCell,
VirtualizedPageList,
} from '@affine/component/page-list';
@@ -8,7 +9,10 @@ import { assertExists } from '@blocksuite/global/utils';
import { DeleteIcon } from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
+import { getCurrentStore } from '@toeverything/infra/atom';
import { useCallback } from 'react';
+import { type LoaderFunction } from 'react-router-dom';
+import { NIL } from 'uuid';
import { usePageHelper } from '../../components/blocksuite/block-suite-page-list/utils';
import { Header } from '../../components/pure/header';
@@ -41,23 +45,34 @@ const TrashHeader = () => {
);
};
+export const loader: LoaderFunction = async () => {
+ // to fix the bug that the trash page list is not updated when route from collection to trash
+ // but it's not a good solution, the page will jitter when collection and trash are switched between each other.
+ // TODO: fix this bug
+
+ const rootStore = getCurrentStore();
+ rootStore.set(currentCollectionAtom, NIL);
+ return null;
+};
+
export const TrashPage = () => {
const [currentWorkspace] = useCurrentWorkspace();
// todo(himself65): refactor to plugin
const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace;
assertExists(blockSuiteWorkspace);
- const pageMetas = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
+
+ const pageMetas = useBlockSuitePageMeta(blockSuiteWorkspace);
const filteredPageMetas = useFilteredPageMetas(
'trash',
pageMetas,
- currentWorkspace.blockSuiteWorkspace
+ blockSuiteWorkspace
);
+
const { restoreFromTrash, permanentlyDeletePage } =
useBlockSuiteMetaHelper(blockSuiteWorkspace);
- const { isPreferredEdgeless } = usePageHelper(
- currentWorkspace.blockSuiteWorkspace
- );
+ const { isPreferredEdgeless } = usePageHelper(blockSuiteWorkspace);
const t = useAFFiNEI18N();
+
const pageOperationsRenderer = useCallback(
(page: PageMeta) => {
const onRestorePage = () => {
@@ -81,6 +96,7 @@ export const TrashPage = () => {
},
[permanentlyDeletePage, restoreFromTrash, t]
);
+
return (
diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json
index 1cfacda972..8bfa822cfc 100644
--- a/packages/frontend/i18n/src/resources/en.json
+++ b/packages/frontend/i18n/src/resources/en.json
@@ -541,6 +541,7 @@
"com.affine.collection-bar.action.tooltip.unpin": "Unpin",
"com.affine.collection.addPage.alreadyExists": "Page already exists",
"com.affine.collection.addPage.success": "Added successfully",
+ "com.affine.collection.removePage.success": "Removed successfully",
"com.affine.collection.addPages": "Add Pages",
"com.affine.collection.addPages.tips": "<0>Add pages:0> You can freely select pages and add them to the collection.",
"com.affine.collection.addRules": "Add Rules",
@@ -920,6 +921,8 @@
"com.affine.toastMessage.removedFavorites": "Removed from Favourites",
"com.affine.toastMessage.restored": "{{title}} restored",
"com.affine.toastMessage.successfullyDeleted": "Successfully deleted",
+ "com.affine.toastMessage.rename": "Successfully renamed",
+ "com.affine.toastMessage.addLinkedPage": "Successfully added linked page",
"com.affine.today": "Today",
"com.affine.trashOperation.delete": "Delete",
"com.affine.trashOperation.delete.description": "Once deleted, you can't undo this action. Do you confirm?",
@@ -1007,5 +1010,6 @@
"com.affine.history.empty-prompt.description": "This document is such a spring chicken, it hasn't sprouted a single historical sprig yet!",
"com.affine.history.confirm-restore-modal.restore": "Restore",
"com.affine.history.confirm-restore-modal.hint": "You are about to restore the current version of the page to the latest version available. This action will overwrite any changes made prior to the latest version.",
- "com.affine.share-page.header.present": "Present"
+ "com.affine.share-page.header.present": "Present",
+ "com.affine.page-operation.add-linked-page": "Add linked page"
}
diff --git a/tests/affine-local/e2e/drag-page-to-trash-folder.spec.ts b/tests/affine-local/e2e/drag-page-to-trash-folder.spec.ts
deleted file mode 100644
index d3cd38c7c8..0000000000
--- a/tests/affine-local/e2e/drag-page-to-trash-folder.spec.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { test } from '@affine-test/kit/playwright';
-import { openHomePage } from '@affine-test/kit/utils/load-page';
-import { dragTo, waitForEditorLoad } from '@affine-test/kit/utils/page-logic';
-import { expect } from '@playwright/test';
-
-test('drag a page from "All pages" list onto the "Trash" folder in the sidebar to move it to trash list', async ({
- page,
-}) => {
- // TODO-Doma
- // Init test db with known workspaces and open "All Pages" page via url directly
- {
- await openHomePage(page);
- await waitForEditorLoad(page);
- await page.getByTestId('app-sidebar').getByText('All Pages').click();
- await page.waitForTimeout(500);
- }
-
- const title = 'AFFiNE - not just a note-taking app';
-
- await dragTo(
- page,
- page.locator(`[role="button"]:has-text("${title}")`),
- page.getByTestId('app-sidebar').getByText('Trash')
- );
-
- await expect(
- page.getByText('Successfully deleted'),
- 'A toast containing success message is shown'
- ).toBeVisible();
-
- await expect(
- page.getByText(title),
- 'The deleted post is no longer on the All Page list'
- ).toHaveCount(0);
-
- // TODO-Doma
- // Visit trash page via url
- await page.getByText('Trash', { exact: true }).click();
- await expect(
- page.getByText(title),
- 'The deleted post exists in the Trash list'
- ).toHaveCount(1);
-});
diff --git a/tests/affine-local/e2e/drag-page.spec.ts b/tests/affine-local/e2e/drag-page.spec.ts
new file mode 100644
index 0000000000..c0ac7e4869
--- /dev/null
+++ b/tests/affine-local/e2e/drag-page.spec.ts
@@ -0,0 +1,145 @@
+import { test } from '@affine-test/kit/playwright';
+import { openHomePage } from '@affine-test/kit/utils/load-page';
+import {
+ clickNewPageButton,
+ dragTo,
+ getBlockSuiteEditorTitle,
+ waitForEditorLoad,
+} from '@affine-test/kit/utils/page-logic';
+import { clickSideBarAllPageButton } from '@affine-test/kit/utils/sidebar';
+import { expect, type Locator, type Page } from '@playwright/test';
+
+const dragToFavourites = async (
+ page: Page,
+ dragItem: Locator,
+ pageId: string
+) => {
+ const favourites = page.getByTestId('favourites');
+ await dragTo(page, dragItem, favourites);
+ const favouritePage = page.getByTestId(`favourite-page-${pageId}`);
+ expect(favouritePage).not.toBeUndefined();
+ return favouritePage;
+};
+
+const dragToCollection = async (page: Page, dragItem: Locator) => {
+ await page.getByTestId('slider-bar-add-collection-button').click();
+ const input = page.getByTestId('input-collection-title');
+ await input.isVisible();
+ await input.fill('test collection');
+ await page.getByTestId('save-collection').click();
+ const collection = page.getByTestId('collection-item');
+ expect(collection).not.toBeUndefined();
+ await clickSideBarAllPageButton(page);
+ await dragTo(page, dragItem, collection);
+ await page.waitForTimeout(500);
+ await collection.getByTestId('fav-collapsed-button').click();
+ const collectionPage = page.getByTestId('collection-page');
+ expect(collectionPage).not.toBeUndefined();
+ return collectionPage;
+};
+
+const dragToTrash = async (page: Page, title: string, dragItem: Locator) => {
+ // drag to trash
+ await dragTo(page, dragItem, page.getByTestId('trash-page'));
+ const confirmTip = page.getByText('Delete page?');
+ expect(confirmTip).not.toBeUndefined();
+
+ await page.getByRole('button', { name: 'Delete' }).click();
+
+ await expect(
+ page.getByText(title),
+ 'The deleted post is no longer on the All Page list'
+ ).toHaveCount(0);
+ await page.waitForTimeout(500);
+ await page.getByTestId('trash-page').click();
+
+ await expect(
+ page.getByText(title),
+ 'The deleted post exists in the Trash list'
+ ).toHaveCount(1);
+};
+
+test('drag a page from "All pages" list to favourites, then drag to trash', async ({
+ page,
+}) => {
+ const title = 'this is a new page to drag';
+ {
+ await openHomePage(page);
+ await waitForEditorLoad(page);
+ await clickNewPageButton(page);
+ await getBlockSuiteEditorTitle(page).fill(title);
+ }
+ const pageId = page.url().split('/').reverse()[0];
+ await clickSideBarAllPageButton(page);
+ await page.waitForTimeout(500);
+
+ const favouritePage = await dragToFavourites(
+ page,
+ page.locator(`[role="button"]:has-text("${title}")`),
+ pageId
+ );
+
+ await dragToTrash(page, title, favouritePage);
+});
+
+test('drag a page from "All pages" list to collections, then drag to trash', async ({
+ page,
+}) => {
+ const title = 'this is a new page to drag';
+ {
+ await openHomePage(page);
+ await waitForEditorLoad(page);
+ await clickNewPageButton(page);
+ await getBlockSuiteEditorTitle(page).fill(title);
+ }
+ await clickSideBarAllPageButton(page);
+ await page.waitForTimeout(500);
+
+ const collectionPage = await dragToCollection(
+ page,
+ page.locator(`[role="button"]:has-text("${title}")`)
+ );
+
+ await dragToTrash(page, title, collectionPage);
+});
+
+test('drag a page from "All pages" list to trash', async ({ page }) => {
+ const title = 'this is a new page to drag';
+ {
+ await openHomePage(page);
+ await waitForEditorLoad(page);
+ await clickNewPageButton(page);
+ await getBlockSuiteEditorTitle(page).fill(title);
+ }
+ await clickSideBarAllPageButton(page);
+ await page.waitForTimeout(500);
+
+ await dragToTrash(
+ page,
+ title,
+ page.locator(`[role="button"]:has-text("${title}")`)
+ );
+});
+
+test('drag a page from favourites to collection', async ({ page }) => {
+ const title = 'this is a new page to drag';
+ {
+ await openHomePage(page);
+ await waitForEditorLoad(page);
+ await clickNewPageButton(page);
+ await getBlockSuiteEditorTitle(page).fill(title);
+ }
+ const pageId = page.url().split('/').reverse()[0];
+ await clickSideBarAllPageButton(page);
+ await page.waitForTimeout(500);
+
+ // drag to favourites
+ const favouritePage = await dragToFavourites(
+ page,
+ page.locator(`[role="button"]:has-text("${title}")`),
+ pageId
+ );
+
+ // drag to collections
+ await dragToCollection(page, favouritePage);
+});
diff --git a/tests/affine-local/e2e/local-first-collections-items.spec.ts b/tests/affine-local/e2e/local-first-collections-items.spec.ts
index b59187f108..ada071d86a 100644
--- a/tests/affine-local/e2e/local-first-collections-items.spec.ts
+++ b/tests/affine-local/e2e/local-first-collections-items.spec.ts
@@ -60,18 +60,16 @@ test('Show collections items in sidebar', async ({ page }) => {
const collectionPage = collections.getByTestId('collection-page').nth(0);
expect(await collectionPage.textContent()).toBe('test page');
await collectionPage.hover();
- await collectionPage.getByTestId('collection-page-options').click();
- const deletePage = page
- .getByTestId('collection-page-option')
- .getByText('Delete');
+ await collectionPage
+ .getByTestId('left-sidebar-page-operation-button')
+ .click();
+ const deletePage = page.getByText('Delete');
await deletePage.click();
await page.getByTestId('confirm-delete-page').click();
expect(await collections.getByTestId('collection-page').count()).toBe(0);
await first.hover();
await first.getByTestId('collection-options').click();
- const deleteCollection = page
- .getByTestId('collection-option')
- .getByText('Delete');
+ const deleteCollection = page.getByText('Delete');
await deleteCollection.click();
await page.waitForTimeout(50);
expect(await items.count()).toBe(0);
@@ -100,13 +98,10 @@ test('edit collection', async ({ page }) => {
const first = items.first();
await first.hover();
await first.getByTestId('collection-options').click();
- const editCollection = page
- .getByTestId('collection-option')
- .getByText('Rename');
+ const editCollection = page.getByText('Rename');
await editCollection.click();
- const title = page.getByTestId('input-collection-title');
- await title.fill('123');
- await page.getByTestId('save-collection').click();
+ await page.getByTestId('rename-modal-input').fill('123');
+ await page.keyboard.press('Enter');
await page.waitForTimeout(100);
expect(await first.textContent()).toBe('123');
});
@@ -123,9 +118,8 @@ test('edit collection and change filter date', async ({ page }) => {
.getByTestId('collection-option')
.getByText('Rename');
await editCollection.click();
- const title = page.getByTestId('input-collection-title');
- await title.fill('123');
- await page.getByTestId('save-collection').click();
+ await page.getByTestId('rename-modal-input').fill('123');
+ await page.keyboard.press('Enter');
await page.waitForTimeout(100);
expect(await first.textContent()).toBe('123');
});
diff --git a/tests/affine-local/e2e/local-first-favorites-items.spec.ts b/tests/affine-local/e2e/local-first-favorites-items.spec.ts
index 296b47e640..64e31bfd3e 100644
--- a/tests/affine-local/e2e/local-first-favorites-items.spec.ts
+++ b/tests/affine-local/e2e/local-first-favorites-items.spec.ts
@@ -26,7 +26,7 @@ test('Show favorite items in sidebar', async ({ page, workspace }) => {
const favoriteBtn = page.getByTestId('editor-option-menu-favorite');
await favoriteBtn.click();
const favoriteListItemInSidebar = page.getByTestId(
- 'favorite-list-item-' + newPageId
+ 'favourite-page-' + newPageId
);
expect(await favoriteListItemInSidebar.textContent()).toBe(
'this is a new page to favorite'
@@ -55,7 +55,7 @@ test('Show favorite reference in sidebar', async ({ page, workspace }) => {
const favoriteBtn = page.getByTestId('editor-option-menu-favorite');
await favoriteBtn.click();
- const favItemTestId = 'favorite-list-item-' + newPageId;
+ const favItemTestId = 'favourite-page-' + newPageId;
const favoriteListItemInSidebar = page.getByTestId(favItemTestId);
expect(await favoriteListItemInSidebar.textContent()).toBe(
@@ -69,7 +69,7 @@ test('Show favorite reference in sidebar', async ({ page, workspace }) => {
await expect(collapseButton).toBeVisible();
await collapseButton.click();
await expect(
- page.locator('[data-type="favorite-list-item"] >> text=Another page')
+ page.locator('[data-type="reference-page"] >> text=Another page')
).toBeVisible();
const currentWorkspace = await workspace.current();
@@ -110,7 +110,7 @@ test("Deleted page's reference will not be shown in sidebar", async ({
// confirm delete
await page.locator('button >> text=Delete').click();
- const favItemTestId = 'favorite-list-item-' + newPageId;
+ const favItemTestId = 'favourite-page-' + newPageId;
const favoriteListItemInSidebar = page.getByTestId(favItemTestId);
expect(await favoriteListItemInSidebar.textContent()).toBe(
@@ -137,7 +137,7 @@ test('Add new favorite page via sidebar', async ({ page }) => {
await getBlockSuiteEditorTitle(page).fill('this is a new fav page');
// check if the page title is shown in the favorite list
const favItem = page.locator(
- '[data-type=favorite-list-item] >> text=this is a new fav page'
+ '[data-type=favourite-list-item] >> text=this is a new fav page'
);
await expect(favItem).toBeVisible();
});
diff --git a/tests/storybook/src/stories/quick-search/quick-search-main.stories.tsx b/tests/storybook/src/stories/quick-search/quick-search-main.stories.tsx
index e528784b37..e99b4d1e2a 100644
--- a/tests/storybook/src/stories/quick-search/quick-search-main.stories.tsx
+++ b/tests/storybook/src/stories/quick-search/quick-search-main.stories.tsx
@@ -64,6 +64,7 @@ function useRegisterCommands() {
createPage: createMockedPage,
importFile: () => Promise.resolve(),
isPreferredEdgeless: () => false,
+ createLinkedPage: createMockedPage,
},
}),
registerAffineLayoutCommands({