mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-17 01:56:27 +08:00
fix(core): select all in page list group header does not need to enable selection (#4869)
This commit is contained in:
+10
-4
@@ -3,6 +3,7 @@ import * as Toolbar from '@radix-ui/react-toolbar';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
type CSSProperties,
|
||||
type HTMLAttributes,
|
||||
type MouseEventHandler,
|
||||
type PropsWithChildren,
|
||||
type ReactNode,
|
||||
@@ -16,13 +17,11 @@ interface FloatingToolbarProps {
|
||||
open?: boolean;
|
||||
}
|
||||
|
||||
interface FloatingToolbarButtonProps {
|
||||
interface FloatingToolbarButtonProps extends HTMLAttributes<HTMLButtonElement> {
|
||||
icon: ReactNode;
|
||||
onClick: MouseEventHandler;
|
||||
type?: 'danger' | 'default';
|
||||
label?: ReactNode;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
interface FloatingToolbarItemProps {}
|
||||
@@ -40,7 +39,12 @@ export function FloatingToolbar({
|
||||
<Popover.Portal>
|
||||
{/* always pop up on top for now */}
|
||||
<Popover.Content side="top" className={styles.popoverContent}>
|
||||
<Toolbar.Root className={clsx(styles.root)}>{children}</Toolbar.Root>
|
||||
<Toolbar.Root
|
||||
data-testid="floating-toolbar"
|
||||
className={clsx(styles.root)}
|
||||
>
|
||||
{children}
|
||||
</Toolbar.Root>
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
</Popover.Root>
|
||||
@@ -62,6 +66,7 @@ export function FloatingToolbarButton({
|
||||
className,
|
||||
style,
|
||||
label,
|
||||
...props
|
||||
}: FloatingToolbarButtonProps) {
|
||||
return (
|
||||
<Toolbar.Button
|
||||
@@ -72,6 +77,7 @@ export function FloatingToolbarButton({
|
||||
className
|
||||
)}
|
||||
style={style}
|
||||
{...props}
|
||||
>
|
||||
<div className={styles.buttonIcon}>{icon}</div>
|
||||
{label}
|
||||
|
||||
@@ -119,19 +119,18 @@ export const PageGroupHeader = ({ id, items, label }: PageGroupProps) => {
|
||||
[id, setCollapseState]
|
||||
);
|
||||
|
||||
const selectionState = useAtomValue(selectionStateAtom);
|
||||
const [selectionState, setSelectionActive] = useAtom(selectionStateAtom);
|
||||
const selectedItems = useMemo(() => {
|
||||
const selectedPageIds = selectionState.selectedPageIds ?? [];
|
||||
return items.filter(item => selectedPageIds.includes(item.id));
|
||||
}, [items, selectionState.selectedPageIds]);
|
||||
|
||||
const allSelected = useMemo(() => {
|
||||
return items.every(
|
||||
item => selectionState.selectedPageIds?.includes(item.id)
|
||||
);
|
||||
}, [items, selectionState.selectedPageIds]);
|
||||
const allSelected = selectedItems.length === items.length;
|
||||
|
||||
const onSelectAll = useCallback(() => {
|
||||
// also enable selection active
|
||||
setSelectionActive(true);
|
||||
|
||||
const nonCurrentGroupIds =
|
||||
selectionState.selectedPageIds?.filter(
|
||||
id => !items.map(item => item.id).includes(id)
|
||||
@@ -142,12 +141,18 @@ export const PageGroupHeader = ({ id, items, label }: PageGroupProps) => {
|
||||
: [...nonCurrentGroupIds, ...items.map(item => item.id)];
|
||||
|
||||
selectionState.onSelectedPageIdsChange?.(newSelectedPageIds);
|
||||
}, [items, selectionState, allSelected]);
|
||||
}, [setSelectionActive, selectionState, allSelected, items]);
|
||||
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
return label ? (
|
||||
<div data-testid="page-list-group-header" className={styles.header}>
|
||||
<div
|
||||
data-testid="page-list-group-header"
|
||||
className={styles.header}
|
||||
data-group-id={id}
|
||||
data-group-items-count={items.length}
|
||||
data-group-selected-items-count={selectedItems.length}
|
||||
>
|
||||
<div
|
||||
role="button"
|
||||
onClick={onExpandedClicked}
|
||||
@@ -166,15 +171,13 @@ export const PageGroupHeader = ({ id, items, label }: PageGroupProps) => {
|
||||
</div>
|
||||
) : null}
|
||||
<div className={styles.spacer} />
|
||||
{selectionState.selectionActive ? (
|
||||
<button className={styles.selectAllButton} onClick={onSelectAll}>
|
||||
{t[
|
||||
allSelected
|
||||
? 'com.affine.page.group-header.clear'
|
||||
: 'com.affine.page.group-header.select-all'
|
||||
]()}
|
||||
</button>
|
||||
) : null}
|
||||
<button className={styles.selectAllButton} onClick={onSelectAll}>
|
||||
{t[
|
||||
allSelected
|
||||
? 'com.affine.page.group-header.clear'
|
||||
: 'com.affine.page.group-header.select-all'
|
||||
]()}
|
||||
</button>
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
||||
@@ -101,6 +101,7 @@ const PageListHeaderCheckbox = () => {
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="page-list-header-selection-checkbox"
|
||||
className={styles.headerTitleSelectionIconWrapper}
|
||||
onClick={onActivateSelection}
|
||||
>
|
||||
|
||||
@@ -184,6 +184,7 @@ const PageListFloatingToolbar = ({
|
||||
onClick={handleMultiDelete}
|
||||
icon={<DeleteIcon />}
|
||||
type="danger"
|
||||
data-testid="page-list-toolbar-delete"
|
||||
/>
|
||||
</FloatingToolbar>
|
||||
);
|
||||
|
||||
@@ -194,3 +194,124 @@ test('allow creation of filters by tags', async ({ page }) => {
|
||||
await selectTag(page, 'B');
|
||||
expect(await getPagesCount(page)).toBe(pageCount + 1);
|
||||
});
|
||||
|
||||
test('enable selection and use ESC to disable selection', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitForEditorLoad(page);
|
||||
await clickSideBarAllPageButton(page);
|
||||
await waitForAllPagesLoad(page);
|
||||
|
||||
// there should be no checkbox in the page list by default
|
||||
expect(
|
||||
await page
|
||||
.locator('[data-testid="page-list-item"] [data-testid="affine-checkbox"]')
|
||||
.count()
|
||||
).toBe(0);
|
||||
|
||||
// by clicking [data-testid="page-list-header-selection-checkbox"], checkboxes should appear
|
||||
await page
|
||||
.locator('[data-testid="page-list-header-selection-checkbox"]')
|
||||
.click();
|
||||
|
||||
// there should be checkboxes in the page list now
|
||||
expect(
|
||||
await page
|
||||
.locator('[data-testid="page-list-item"] [data-testid="affine-checkbox"]')
|
||||
.count()
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
// by ESC, checkboxes should NOT disappear (because it is too early)
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
expect(
|
||||
await page
|
||||
.locator('[data-testid="page-list-item"] [data-testid="affine-checkbox"]')
|
||||
.count()
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
// wait for 300ms
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// esc again, checkboxes should disappear
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
expect(
|
||||
await page
|
||||
.locator('[data-testid="page-list-item"] [data-testid="affine-checkbox"]')
|
||||
.count()
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
test('select two pages and delete', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitForEditorLoad(page);
|
||||
await clickSideBarAllPageButton(page);
|
||||
await waitForAllPagesLoad(page);
|
||||
|
||||
const pageCount = await getPagesCount(page);
|
||||
|
||||
// by clicking [data-testid="page-list-header-selection-checkbox"], checkboxes should appear
|
||||
await page
|
||||
.locator('[data-testid="page-list-header-selection-checkbox"]')
|
||||
.click();
|
||||
|
||||
// select the first two pages
|
||||
await page
|
||||
.locator('[data-testid="page-list-item"] [data-testid="affine-checkbox"]')
|
||||
.nth(0)
|
||||
.click();
|
||||
|
||||
await page
|
||||
.locator('[data-testid="page-list-item"] [data-testid="affine-checkbox"]')
|
||||
.nth(1)
|
||||
.click();
|
||||
|
||||
// the floating popover should appear
|
||||
await expect(page.locator('[data-testid="floating-toolbar"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="floating-toolbar"]')).toHaveText(
|
||||
'2 selected'
|
||||
);
|
||||
|
||||
// click delete button
|
||||
await page.locator('[data-testid="page-list-toolbar-delete"]').click();
|
||||
|
||||
// the confirm dialog should appear
|
||||
await expect(page.getByText('Delete 2 pages?')).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
|
||||
// check the page count again
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
expect(await getPagesCount(page)).toBe(pageCount - 2);
|
||||
});
|
||||
|
||||
test('select a group of items by clicking "Select All" in group header', async ({
|
||||
page,
|
||||
}) => {
|
||||
await openHomePage(page);
|
||||
await waitForEditorLoad(page);
|
||||
await clickSideBarAllPageButton(page);
|
||||
await waitForAllPagesLoad(page);
|
||||
|
||||
// Select All will appear when hovering the header
|
||||
await page.hover('[data-testid="page-list-group-header"]');
|
||||
|
||||
// click Select All
|
||||
await page.getByRole('button', { name: 'Select All' }).click();
|
||||
|
||||
const selectedItemCount = await page
|
||||
.locator('[data-testid="page-list-group-header"]')
|
||||
.getAttribute('data-group-selected-items-count');
|
||||
|
||||
const selectedGroupItemTotalCount = await page
|
||||
.locator('[data-testid="page-list-group-header"]')
|
||||
.getAttribute('data-group-items-count');
|
||||
|
||||
expect(selectedItemCount).toBe(selectedGroupItemTotalCount);
|
||||
|
||||
// check the selected count is equal to the one displayed in the floating toolbar
|
||||
await expect(page.locator('[data-testid="floating-toolbar"]')).toHaveText(
|
||||
`${selectedItemCount} selected`
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user