mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-22 19:53:48 +08:00
feat: fav page references (#2422)
Co-authored-by: Himself65 <himself65@outlook.com>
This commit is contained in:
@@ -1,40 +1,103 @@
|
|||||||
import { MenuLinkItem } from '@affine/component/app-sidebar';
|
import { MenuLinkItem } from '@affine/component/app-sidebar';
|
||||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
||||||
|
import type { PageMeta, Workspace } from '@blocksuite/store';
|
||||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||||
|
import { useBlockSuitePageReferences } from '@toeverything/hooks/use-block-suite-page-references';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useMemo } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { workspacePreferredModeAtom } from '../../../../atoms';
|
import { workspacePreferredModeAtom } from '../../../../atoms';
|
||||||
import type { FavoriteListProps } from '../index';
|
import type { FavoriteListProps } from '../index';
|
||||||
import EmptyItem from './empty-item';
|
import EmptyItem from './empty-item';
|
||||||
export const FavoriteList = ({ currentWorkspace }: FavoriteListProps) => {
|
|
||||||
|
interface FavoriteMenuItemProps {
|
||||||
|
workspace: Workspace;
|
||||||
|
pageId: string;
|
||||||
|
metaMapping: Record<string, PageMeta>;
|
||||||
|
parentIds: Set<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FavoriteMenuItem({
|
||||||
|
workspace,
|
||||||
|
pageId,
|
||||||
|
metaMapping,
|
||||||
|
parentIds,
|
||||||
|
}: FavoriteMenuItemProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const record = useAtomValue(workspacePreferredModeAtom);
|
const record = useAtomValue(workspacePreferredModeAtom);
|
||||||
const pageMeta = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
|
const active = router.query.pageId === pageId;
|
||||||
const workspaceId = currentWorkspace.id;
|
const icon = record[pageId] === 'edgeless' ? <EdgelessIcon /> : <PageIcon />;
|
||||||
|
const references = useBlockSuitePageReferences(workspace, pageId);
|
||||||
|
const referencesToShow = useMemo(() => {
|
||||||
|
return [...new Set(references.filter(ref => !parentIds.has(ref)))];
|
||||||
|
}, [references, parentIds]);
|
||||||
|
const [collapsed, setCollapsed] = useState(true);
|
||||||
|
const collapsible = referencesToShow.length > 0 && parentIds.size === 0;
|
||||||
|
const showReferences = collapsible ? !collapsed : referencesToShow.length > 0;
|
||||||
|
const nestedItem = parentIds.size > 0;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MenuLinkItem
|
||||||
|
style={{
|
||||||
|
marginLeft: nestedItem ? '12px' : undefined,
|
||||||
|
width: nestedItem ? 'calc(100% - 12px)' : undefined,
|
||||||
|
}}
|
||||||
|
data-type="favorite-list-item"
|
||||||
|
data-testid={`favorite-list-item-${pageId}`}
|
||||||
|
active={active}
|
||||||
|
href={`/workspace/${workspace.id}/${pageId}`}
|
||||||
|
icon={icon}
|
||||||
|
collapsed={collapsible ? collapsed : undefined}
|
||||||
|
onCollapsedChange={setCollapsed}
|
||||||
|
>
|
||||||
|
<span>{metaMapping[pageId]?.title || 'Untitled'}</span>
|
||||||
|
</MenuLinkItem>
|
||||||
|
{showReferences &&
|
||||||
|
referencesToShow.map(ref => {
|
||||||
|
return (
|
||||||
|
<FavoriteMenuItem
|
||||||
|
key={ref}
|
||||||
|
workspace={workspace}
|
||||||
|
pageId={ref}
|
||||||
|
metaMapping={metaMapping}
|
||||||
|
parentIds={new Set([...parentIds, pageId])}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FavoriteList = ({ currentWorkspace }: FavoriteListProps) => {
|
||||||
|
const metas = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
|
||||||
|
|
||||||
const favoriteList = useMemo(
|
const favoriteList = useMemo(
|
||||||
() => pageMeta.filter(p => p.favorite && !p.trash),
|
() => metas.filter(p => p.favorite && !p.trash),
|
||||||
[pageMeta]
|
[metas]
|
||||||
|
);
|
||||||
|
|
||||||
|
const metaMapping = useMemo(
|
||||||
|
() =>
|
||||||
|
metas.reduce((acc, meta) => {
|
||||||
|
acc[meta.id] = meta;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, PageMeta>),
|
||||||
|
[metas]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{favoriteList.map((pageMeta, index) => {
|
{favoriteList.map((pageMeta, index) => {
|
||||||
const active = router.query.pageId === pageMeta.id;
|
|
||||||
const icon =
|
|
||||||
record[pageMeta.id] === 'edgeless' ? <EdgelessIcon /> : <PageIcon />;
|
|
||||||
return (
|
return (
|
||||||
<MenuLinkItem
|
<FavoriteMenuItem
|
||||||
key={`${pageMeta}-${index}`}
|
key={`${pageMeta}-${index}`}
|
||||||
data-testid={`favorite-list-item-${pageMeta.id}`}
|
metaMapping={metaMapping}
|
||||||
active={active}
|
pageId={pageMeta.id}
|
||||||
href={`/workspace/${workspaceId}/${pageMeta.id}`}
|
// memo?
|
||||||
icon={icon}
|
parentIds={new Set()}
|
||||||
>
|
workspace={currentWorkspace.blockSuiteWorkspace}
|
||||||
<span>{pageMeta.title || 'Untitled'}</span>
|
/>
|
||||||
</MenuLinkItem>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{favoriteList.length === 0 && <EmptyItem />}
|
{favoriteList.length === 0 && <EmptyItem />}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
} from '@blocksuite/icons';
|
} from '@blocksuite/icons';
|
||||||
import type { Meta, StoryFn } from '@storybook/react';
|
import type { Meta, StoryFn } from '@storybook/react';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import type { PropsWithChildren } from 'react';
|
import { type PropsWithChildren, useState } from 'react';
|
||||||
|
|
||||||
import { AppSidebar, AppSidebarFallback, appSidebarOpenAtom } from '.';
|
import { AppSidebar, AppSidebarFallback, appSidebarOpenAtom } from '.';
|
||||||
import { AddPageButton } from './add-page-button';
|
import { AddPageButton } from './add-page-button';
|
||||||
@@ -79,6 +79,7 @@ export const Fallback = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const WithItems: StoryFn = () => {
|
export const WithItems: StoryFn = () => {
|
||||||
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<AppSidebar>
|
<AppSidebar>
|
||||||
@@ -111,11 +112,22 @@ export const WithItems: StoryFn = () => {
|
|||||||
<SidebarScrollableContainer>
|
<SidebarScrollableContainer>
|
||||||
<CategoryDivider label="Favorites" />
|
<CategoryDivider label="Favorites" />
|
||||||
<MenuLinkItem
|
<MenuLinkItem
|
||||||
|
collapsed={collapsed}
|
||||||
|
onCollapsedChange={setCollapsed}
|
||||||
icon={<SettingsIcon />}
|
icon={<SettingsIcon />}
|
||||||
href="/test"
|
href="/test"
|
||||||
onClick={() => alert('opened')}
|
onClick={() => alert('opened')}
|
||||||
>
|
>
|
||||||
Settings
|
Collapsible Item
|
||||||
|
</MenuLinkItem>
|
||||||
|
<MenuLinkItem
|
||||||
|
collapsed={!collapsed}
|
||||||
|
onCollapsedChange={setCollapsed}
|
||||||
|
icon={<SettingsIcon />}
|
||||||
|
href="/test"
|
||||||
|
onClick={() => alert('opened')}
|
||||||
|
>
|
||||||
|
Collapsible Item
|
||||||
</MenuLinkItem>
|
</MenuLinkItem>
|
||||||
<MenuLinkItem
|
<MenuLinkItem
|
||||||
icon={<SettingsIcon />}
|
icon={<SettingsIcon />}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const root = style({
|
|||||||
minHeight: '30px',
|
minHeight: '30px',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
padding: '0 12px',
|
padding: '0 8px 0 12px',
|
||||||
fontSize: 'var(--affine-font-sm)',
|
fontSize: 'var(--affine-font-sm)',
|
||||||
selectors: {
|
selectors: {
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
@@ -27,6 +27,11 @@ export const root = style({
|
|||||||
// make this a variable?
|
// make this a variable?
|
||||||
'linear-gradient(0deg, rgba(0, 0, 0, 0.04), rgba(0, 0, 0, 0.04)), rgba(0, 0, 0, 0.04);',
|
'linear-gradient(0deg, rgba(0, 0, 0, 0.04), rgba(0, 0, 0, 0.04)), rgba(0, 0, 0, 0.04);',
|
||||||
},
|
},
|
||||||
|
'&[data-collapsible="true"]': {
|
||||||
|
width: 'calc(100% + 8px)',
|
||||||
|
transform: 'translateX(-8px)',
|
||||||
|
paddingLeft: '8px',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -37,11 +42,49 @@ export const content = style({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const icon = style({
|
export const icon = style({
|
||||||
marginRight: '14px',
|
|
||||||
color: 'var(--affine-icon-color)',
|
color: 'var(--affine-icon-color)',
|
||||||
fontSize: '20px',
|
fontSize: '20px',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const collapsedIconContainer = style({
|
||||||
|
width: '12px',
|
||||||
|
height: '12px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
borderRadius: '2px',
|
||||||
|
transition: 'transform 0.2s',
|
||||||
|
selectors: {
|
||||||
|
'&[data-collapsed="true"]': {
|
||||||
|
transform: 'rotate(-90deg)',
|
||||||
|
},
|
||||||
|
'&:hover': {
|
||||||
|
background: 'var(--affine-hover-color)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const iconsContainer = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
width: '28px',
|
||||||
|
selectors: {
|
||||||
|
'&[data-collapsible="true"]': {
|
||||||
|
width: '40px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const collapsedIcon = style({
|
||||||
|
transition: 'transform 0.2s ease-in-out',
|
||||||
|
selectors: {
|
||||||
|
'&[data-collapsed="true"]': {
|
||||||
|
transform: 'rotate(-90deg)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const spacer = style({
|
export const spacer = style({
|
||||||
flex: 1,
|
flex: 1,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { SettingsIcon } from '@blocksuite/icons';
|
import { SettingsIcon } from '@blocksuite/icons';
|
||||||
import type { Meta, StoryFn } from '@storybook/react';
|
import type { Meta, StoryFn } from '@storybook/react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { MenuItem, MenuLinkItem } from '.';
|
import { MenuItem, MenuLinkItem } from '.';
|
||||||
|
|
||||||
@@ -9,6 +10,7 @@ export default {
|
|||||||
} satisfies Meta;
|
} satisfies Meta;
|
||||||
|
|
||||||
export const Default: StoryFn = () => {
|
export const Default: StoryFn = () => {
|
||||||
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
return (
|
return (
|
||||||
<main style={{ width: '240px' }}>
|
<main style={{ width: '240px' }}>
|
||||||
<MenuItem icon={<SettingsIcon />} onClick={() => alert('opened')}>
|
<MenuItem icon={<SettingsIcon />} onClick={() => alert('opened')}>
|
||||||
@@ -29,6 +31,14 @@ export const Default: StoryFn = () => {
|
|||||||
>
|
>
|
||||||
Primary Item
|
Primary Item
|
||||||
</MenuLinkItem>
|
</MenuLinkItem>
|
||||||
|
<MenuItem
|
||||||
|
collapsed={collapsed}
|
||||||
|
onCollapsedChange={setCollapsed}
|
||||||
|
icon={<SettingsIcon />}
|
||||||
|
onClick={() => alert('opened')}
|
||||||
|
>
|
||||||
|
Collapsible Item
|
||||||
|
</MenuItem>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ArrowDownSmallIcon } from '@blocksuite/icons';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import type { LinkProps } from 'next/link';
|
import type { LinkProps } from 'next/link';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
@@ -9,6 +10,8 @@ interface MenuItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
|||||||
icon?: React.ReactElement;
|
icon?: React.ReactElement;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
collapsed?: boolean; // true, false, undefined. undefined means no collapse
|
||||||
|
onCollapsedChange?: (collapsed: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MenuLinkItemProps extends MenuItemProps, Pick<LinkProps, 'href'> {}
|
interface MenuLinkItemProps extends MenuItemProps, Pick<LinkProps, 'href'> {}
|
||||||
@@ -19,8 +22,14 @@ export function MenuItem({
|
|||||||
active,
|
active,
|
||||||
children,
|
children,
|
||||||
disabled,
|
disabled,
|
||||||
|
collapsed,
|
||||||
|
onCollapsedChange,
|
||||||
...props
|
...props
|
||||||
}: MenuItemProps) {
|
}: MenuItemProps) {
|
||||||
|
const collapsible = collapsed !== undefined;
|
||||||
|
if (collapsible && !onCollapsedChange) {
|
||||||
|
throw new Error('onCollapsedChange is required when collapsed is defined');
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
{...props}
|
{...props}
|
||||||
@@ -28,11 +37,31 @@ export function MenuItem({
|
|||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
data-active={active}
|
data-active={active}
|
||||||
data-disabled={disabled}
|
data-disabled={disabled}
|
||||||
|
data-collapsible={collapsible}
|
||||||
>
|
>
|
||||||
{icon &&
|
<div className={styles.iconsContainer} data-collapsible={collapsible}>
|
||||||
React.cloneElement(icon, {
|
{collapsible && (
|
||||||
className: clsx([styles.icon, icon.props.className]),
|
<div
|
||||||
})}
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault(); // for links
|
||||||
|
onCollapsedChange?.(!collapsed);
|
||||||
|
}}
|
||||||
|
data-testid="fav-collapsed-button"
|
||||||
|
className={styles.collapsedIconContainer}
|
||||||
|
>
|
||||||
|
<ArrowDownSmallIcon
|
||||||
|
className={styles.collapsedIcon}
|
||||||
|
data-collapsed={collapsed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{icon &&
|
||||||
|
React.cloneElement(icon, {
|
||||||
|
className: clsx([styles.icon, icon.props.className]),
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={styles.content}>{children}</div>
|
<div className={styles.content}>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const root = style({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const icon = style({
|
export const icon = style({
|
||||||
marginRight: '14px',
|
marginRight: '8px',
|
||||||
color: 'var(--affine-icon-color)',
|
color: 'var(--affine-icon-color)',
|
||||||
fontSize: '20px',
|
fontSize: '20px',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import type { Page, Workspace } from '@blocksuite/store';
|
||||||
|
import { atom, useAtomValue } from 'jotai';
|
||||||
|
import { atomFamily } from 'jotai/utils';
|
||||||
|
|
||||||
|
import { useBlockSuiteWorkspacePage } from './use-block-suite-workspace-page';
|
||||||
|
|
||||||
|
function getPageReferences(page: Page): string[] {
|
||||||
|
// todo: is there a way to use page indexer to get all references?
|
||||||
|
return page
|
||||||
|
.getBlockByFlavour('affine:paragraph')
|
||||||
|
.flatMap(b => b.text?.toDelta())
|
||||||
|
.map(v => v?.attributes?.reference?.pageId)
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageReferencesAtomFamily = atomFamily((page: Page | null) => {
|
||||||
|
if (page === null) {
|
||||||
|
return atom([]);
|
||||||
|
}
|
||||||
|
const baseAtom = atom<string[]>(getPageReferences(page));
|
||||||
|
baseAtom.onMount = set => {
|
||||||
|
const dispose = page.slots.yUpdated.on(() => {
|
||||||
|
set(getPageReferences(page));
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
dispose.dispose();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return baseAtom;
|
||||||
|
});
|
||||||
|
|
||||||
|
export function useBlockSuitePageReferences(
|
||||||
|
blockSuiteWorkspace: Workspace,
|
||||||
|
pageId: string
|
||||||
|
): string[] {
|
||||||
|
const page = useBlockSuiteWorkspacePage(blockSuiteWorkspace, pageId);
|
||||||
|
return useAtomValue(pageReferencesAtomFamily(page));
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Page } from '@playwright/test';
|
import type { Page } from '@playwright/test';
|
||||||
|
import { expect } from '@playwright/test';
|
||||||
|
|
||||||
export async function waitMarkdownImported(page: Page) {
|
export async function waitMarkdownImported(page: Page) {
|
||||||
await page.waitForSelector('v-line');
|
await page.waitForSelector('v-line');
|
||||||
@@ -16,6 +17,30 @@ export function getBlockSuiteEditorTitle(page: Page) {
|
|||||||
return page.locator('v-line').nth(0);
|
return page.locator('v-line').nth(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function type(page: Page, content: string, delay = 50) {
|
||||||
|
await page.keyboard.type(content, { delay });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function pressEnter(page: Page) {
|
||||||
|
// avoid flaky test by simulate real user input
|
||||||
|
await page.keyboard.press('Enter', { delay: 50 });
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createLinkedPage = async (page: Page, pageName?: string) => {
|
||||||
|
await page.keyboard.type('@', { delay: 50 });
|
||||||
|
const linkedPagePopover = page.locator('.linked-page-popover');
|
||||||
|
await expect(linkedPagePopover).toBeVisible();
|
||||||
|
if (pageName) {
|
||||||
|
await type(page, pageName);
|
||||||
|
} else {
|
||||||
|
pageName = 'Untitled';
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.keyboard.press('ArrowUp');
|
||||||
|
await page.keyboard.press('ArrowUp');
|
||||||
|
await page.keyboard.press('Enter', { delay: 50 });
|
||||||
|
};
|
||||||
|
|
||||||
export async function clickPageMoreActions(page: Page) {
|
export async function clickPageMoreActions(page: Page) {
|
||||||
return page
|
return page
|
||||||
.getByTestId('editor-header-items')
|
.getByTestId('editor-header-items')
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { expect } from '@playwright/test';
|
|||||||
import { openHomePage } from '../libs/load-page';
|
import { openHomePage } from '../libs/load-page';
|
||||||
import {
|
import {
|
||||||
clickPageMoreActions,
|
clickPageMoreActions,
|
||||||
|
createLinkedPage,
|
||||||
getBlockSuiteEditorTitle,
|
getBlockSuiteEditorTitle,
|
||||||
newPage,
|
newPage,
|
||||||
waitMarkdownImported,
|
waitMarkdownImported,
|
||||||
@@ -34,28 +35,39 @@ test('Show favorite items in sidebar', async ({ page }) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Show favorite items in favorite list', async ({ page }) => {
|
test('Show favorite reference in sidebar', async ({ page }) => {
|
||||||
await openHomePage(page);
|
await openHomePage(page);
|
||||||
await waitMarkdownImported(page);
|
await waitMarkdownImported(page);
|
||||||
await newPage(page);
|
await newPage(page);
|
||||||
await getBlockSuiteEditorTitle(page).click();
|
await getBlockSuiteEditorTitle(page).click();
|
||||||
await getBlockSuiteEditorTitle(page).fill('this is a new page to favorite');
|
await getBlockSuiteEditorTitle(page).fill('this is a new page to favorite');
|
||||||
await page.getByTestId('all-pages').click();
|
|
||||||
const cell = page.getByRole('cell', {
|
// goes to main content
|
||||||
name: 'this is a new page to favorite',
|
await page.keyboard.press('Enter', { delay: 50 });
|
||||||
});
|
|
||||||
expect(cell).not.toBeUndefined();
|
await createLinkedPage(page, 'Another page');
|
||||||
await cell.click();
|
|
||||||
|
const newPageId = page.url().split('/').reverse()[0];
|
||||||
|
|
||||||
await clickPageMoreActions(page);
|
await clickPageMoreActions(page);
|
||||||
|
|
||||||
const favoriteBtn = page.getByTestId('editor-option-menu-favorite');
|
const favoriteBtn = page.getByTestId('editor-option-menu-favorite');
|
||||||
await favoriteBtn.click();
|
await favoriteBtn.click();
|
||||||
|
|
||||||
await page.getByTestId('all-pages').click();
|
const favItemTestId = 'favorite-list-item-' + newPageId;
|
||||||
|
|
||||||
expect(
|
const favoriteListItemInSidebar = page.getByTestId(favItemTestId);
|
||||||
page.getByRole('cell', { name: 'this is a new page to favorite' })
|
expect(await favoriteListItemInSidebar.textContent()).toBe(
|
||||||
).not.toBeUndefined();
|
'this is a new page to favorite'
|
||||||
|
);
|
||||||
|
|
||||||
await page.getByRole('cell').getByRole('button').nth(0).click();
|
const collapseButton = favoriteListItemInSidebar.locator(
|
||||||
|
'[data-testid="fav-collapsed-button"]'
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(collapseButton).toBeVisible();
|
||||||
|
await collapseButton.click();
|
||||||
|
await expect(
|
||||||
|
page.locator('[data-type="favorite-list-item"] >> text=Another page')
|
||||||
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user