fix(core): select all in page list group header does not need to enable selection (#4869)
This commit is contained in:
@@ -3,6 +3,7 @@ import * as Toolbar from '@radix-ui/react-toolbar';
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import {
|
import {
|
||||||
type CSSProperties,
|
type CSSProperties,
|
||||||
|
type HTMLAttributes,
|
||||||
type MouseEventHandler,
|
type MouseEventHandler,
|
||||||
type PropsWithChildren,
|
type PropsWithChildren,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
@@ -16,13 +17,11 @@ interface FloatingToolbarProps {
|
|||||||
open?: boolean;
|
open?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FloatingToolbarButtonProps {
|
interface FloatingToolbarButtonProps extends HTMLAttributes<HTMLButtonElement> {
|
||||||
icon: ReactNode;
|
icon: ReactNode;
|
||||||
onClick: MouseEventHandler;
|
onClick: MouseEventHandler;
|
||||||
type?: 'danger' | 'default';
|
type?: 'danger' | 'default';
|
||||||
label?: ReactNode;
|
label?: ReactNode;
|
||||||
className?: string;
|
|
||||||
style?: CSSProperties;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FloatingToolbarItemProps {}
|
interface FloatingToolbarItemProps {}
|
||||||
@@ -40,7 +39,12 @@ export function FloatingToolbar({
|
|||||||
<Popover.Portal>
|
<Popover.Portal>
|
||||||
{/* always pop up on top for now */}
|
{/* always pop up on top for now */}
|
||||||
<Popover.Content side="top" className={styles.popoverContent}>
|
<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.Content>
|
||||||
</Popover.Portal>
|
</Popover.Portal>
|
||||||
</Popover.Root>
|
</Popover.Root>
|
||||||
@@ -62,6 +66,7 @@ export function FloatingToolbarButton({
|
|||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
label,
|
label,
|
||||||
|
...props
|
||||||
}: FloatingToolbarButtonProps) {
|
}: FloatingToolbarButtonProps) {
|
||||||
return (
|
return (
|
||||||
<Toolbar.Button
|
<Toolbar.Button
|
||||||
@@ -72,6 +77,7 @@ export function FloatingToolbarButton({
|
|||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
style={style}
|
style={style}
|
||||||
|
{...props}
|
||||||
>
|
>
|
||||||
<div className={styles.buttonIcon}>{icon}</div>
|
<div className={styles.buttonIcon}>{icon}</div>
|
||||||
{label}
|
{label}
|
||||||
|
|||||||
@@ -119,19 +119,18 @@ export const PageGroupHeader = ({ id, items, label }: PageGroupProps) => {
|
|||||||
[id, setCollapseState]
|
[id, setCollapseState]
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectionState = useAtomValue(selectionStateAtom);
|
const [selectionState, setSelectionActive] = useAtom(selectionStateAtom);
|
||||||
const selectedItems = useMemo(() => {
|
const selectedItems = useMemo(() => {
|
||||||
const selectedPageIds = selectionState.selectedPageIds ?? [];
|
const selectedPageIds = selectionState.selectedPageIds ?? [];
|
||||||
return items.filter(item => selectedPageIds.includes(item.id));
|
return items.filter(item => selectedPageIds.includes(item.id));
|
||||||
}, [items, selectionState.selectedPageIds]);
|
}, [items, selectionState.selectedPageIds]);
|
||||||
|
|
||||||
const allSelected = useMemo(() => {
|
const allSelected = selectedItems.length === items.length;
|
||||||
return items.every(
|
|
||||||
item => selectionState.selectedPageIds?.includes(item.id)
|
|
||||||
);
|
|
||||||
}, [items, selectionState.selectedPageIds]);
|
|
||||||
|
|
||||||
const onSelectAll = useCallback(() => {
|
const onSelectAll = useCallback(() => {
|
||||||
|
// also enable selection active
|
||||||
|
setSelectionActive(true);
|
||||||
|
|
||||||
const nonCurrentGroupIds =
|
const nonCurrentGroupIds =
|
||||||
selectionState.selectedPageIds?.filter(
|
selectionState.selectedPageIds?.filter(
|
||||||
id => !items.map(item => item.id).includes(id)
|
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)];
|
: [...nonCurrentGroupIds, ...items.map(item => item.id)];
|
||||||
|
|
||||||
selectionState.onSelectedPageIdsChange?.(newSelectedPageIds);
|
selectionState.onSelectedPageIdsChange?.(newSelectedPageIds);
|
||||||
}, [items, selectionState, allSelected]);
|
}, [setSelectionActive, selectionState, allSelected, items]);
|
||||||
|
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
|
||||||
return label ? (
|
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
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
onClick={onExpandedClicked}
|
onClick={onExpandedClicked}
|
||||||
@@ -166,7 +171,6 @@ export const PageGroupHeader = ({ id, items, label }: PageGroupProps) => {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className={styles.spacer} />
|
<div className={styles.spacer} />
|
||||||
{selectionState.selectionActive ? (
|
|
||||||
<button className={styles.selectAllButton} onClick={onSelectAll}>
|
<button className={styles.selectAllButton} onClick={onSelectAll}>
|
||||||
{t[
|
{t[
|
||||||
allSelected
|
allSelected
|
||||||
@@ -174,7 +178,6 @@ export const PageGroupHeader = ({ id, items, label }: PageGroupProps) => {
|
|||||||
: 'com.affine.page.group-header.select-all'
|
: 'com.affine.page.group-header.select-all'
|
||||||
]()}
|
]()}
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ const PageListHeaderCheckbox = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
data-testid="page-list-header-selection-checkbox"
|
||||||
className={styles.headerTitleSelectionIconWrapper}
|
className={styles.headerTitleSelectionIconWrapper}
|
||||||
onClick={onActivateSelection}
|
onClick={onActivateSelection}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ const PageListFloatingToolbar = ({
|
|||||||
onClick={handleMultiDelete}
|
onClick={handleMultiDelete}
|
||||||
icon={<DeleteIcon />}
|
icon={<DeleteIcon />}
|
||||||
type="danger"
|
type="danger"
|
||||||
|
data-testid="page-list-toolbar-delete"
|
||||||
/>
|
/>
|
||||||
</FloatingToolbar>
|
</FloatingToolbar>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -194,3 +194,124 @@ test('allow creation of filters by tags', async ({ page }) => {
|
|||||||
await selectTag(page, 'B');
|
await selectTag(page, 'B');
|
||||||
expect(await getPagesCount(page)).toBe(pageCount + 1);
|
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