feat(playlists): act on the selected rows from the toolbar, and undo either

While rows are selected the toolbar's second row is the band: the count,
the last target as one click, Move to, Remove, Clear selection. Both write
in one request and undo back to the places; a video the target held before
stays there. Delete takes the selection when there is one. A failure puts
the rows back ticked.

A row's own move goes through the same write, so it undoes too, and the
single-row move route and service function are gone. A playlist row drops
its grip below a 24rem table: the row menu keeps the moves.
This commit is contained in:
2026-11-01 22:07:09 +01:00
parent 3b85881af4
commit efbbb23525
24 changed files with 1114 additions and 236 deletions
+28 -2
View File
@@ -10,9 +10,15 @@ import { useActiveScopes } from "./keymap";
afterEach(cleanup);
function Rows({ selection }: { selection?: NavSelection }) {
function Rows({
selection,
onDelete,
}: {
selection?: NavSelection;
onDelete?: (index: number) => void;
}) {
const ref = useRef<HTMLDivElement>(null);
useListNav(ref, { enabled: true, selection });
useListNav(ref, { enabled: true, selection, onDelete });
return (
<div ref={ref}>
{[0, 1, 2].map((i) => (
@@ -39,6 +45,7 @@ function press(key: string, init: KeyboardEventInit = {}): KeyboardEvent {
const selection = (cleared = true) => ({
toggle: vi.fn<(index: number) => void>(),
clear: vi.fn(() => cleared),
removeSelected: vi.fn(() => false),
});
describe("useListNav · selecting", () => {
@@ -108,6 +115,25 @@ describe("useListNav · selecting", () => {
expect(idle.clear).toHaveBeenCalledTimes(1);
});
it("has Delete go to the selection first, wherever the focus is", () => {
const s = selection();
s.removeSelected.mockReturnValue(true);
const onDelete = vi.fn();
render(<Rows selection={s} onDelete={onDelete} />);
expect(press("Delete").defaultPrevented).toBe(true);
expect(s.removeSelected).toHaveBeenCalledTimes(1);
expect(onDelete).not.toHaveBeenCalled();
});
it("has Delete go on to the walked row when there is no selection", () => {
const s = selection();
const onDelete = vi.fn();
render(<Rows selection={s} onDelete={onDelete} />);
press("j");
press("Delete");
expect(onDelete).toHaveBeenCalledExactlyOnceWith(0);
});
it("leaves both keys alone in a list that takes no selection", () => {
render(<Rows />);
press("j");