feat(design): a channel's and a Plex playlist's one action list (C1) — the row's "…", Delete + undo, an action toast's 8 s floor (E28 S3e2)
- Channels: the row's full-history, hide and unsubscribe buttons become its "…" at the row end (useChannelActions: open, pin, YouTube, copy name / link, full history or the admin reset, hide with an undo toast, unsubscribe behind its danger confirm); the card layout docks the "…" beside the name (ChannelLayoutGrid moreKey). The admin reset's confirm now names the reset. - Plex playlist page: a title, a show group and a season each open the page's one row menu (a title's is its usePlexCardActions list plus the removal); no X at rest; removals take an undo toast whose Undo puts every row back at its own index in one request (bulk add `positions`); j/k walk the visible rows, Delete removes the walked one; rename and delete in the header's "…". - notify: a toast with an action lives at least 8 s (the owner's call), whatever the toast setting.
This commit is contained in:
@@ -558,13 +558,16 @@ export const plexApi = {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ title, item_rating_key }),
|
||||
}),
|
||||
/** `positions` (one per key, an index over the ordered rows) puts removed rows back where they
|
||||
* stood — the list page's Undo, in one request; without it the keys are appended. */
|
||||
plexPlaylistAddBulk: (
|
||||
id: number,
|
||||
itemRks: string[],
|
||||
positions?: number[],
|
||||
): Promise<{ added: number; item_count: number }> =>
|
||||
req(`/api/plex/playlists/${id}/items/bulk`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ item_rating_keys: itemRks }),
|
||||
body: JSON.stringify({ item_rating_keys: itemRks, ...(positions ? { positions } : {}) }),
|
||||
}),
|
||||
plexPlaylistRemoveBulk: (
|
||||
id: number,
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// @vitest-environment jsdom
|
||||
//
|
||||
// A toast with an action (the list page's "Undo") outlives a short toast setting: its button is the
|
||||
// only way back, so it gets ACTION_TTL_MIN at least (owner, E28 S3e2). The setting still wins when it
|
||||
// is longer, "never" still means never, and a plain toast keeps the reader's time.
|
||||
import { describe, it, expect, afterEach, beforeEach, vi } from "vitest";
|
||||
import {
|
||||
ACTION_TTL_MIN,
|
||||
clearAll,
|
||||
configureNotifications,
|
||||
getActiveToasts,
|
||||
notify,
|
||||
} from "./notifications";
|
||||
|
||||
beforeEach(() => vi.useFakeTimers());
|
||||
afterEach(() => {
|
||||
clearAll();
|
||||
configureNotifications({ durationMs: 6000 });
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
const undo = { label: "Undo", onClick: () => {} };
|
||||
const shown = (id: number) => getActiveToasts().some((n) => n.id === id);
|
||||
|
||||
describe("an action toast's lifetime", () => {
|
||||
it("stays for the floor when the reader's toasts last 3 s", () => {
|
||||
configureNotifications({ durationMs: 3000 });
|
||||
const plain = notify({ message: "plain" });
|
||||
const withUndo = notify({ message: "removed", action: undo });
|
||||
vi.advanceTimersByTime(3001);
|
||||
expect(shown(plain)).toBe(false);
|
||||
expect(shown(withUndo)).toBe(true);
|
||||
vi.advanceTimersByTime(ACTION_TTL_MIN - 3001);
|
||||
expect(shown(withUndo)).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps a longer setting", () => {
|
||||
configureNotifications({ durationMs: 12000 });
|
||||
const id = notify({ message: "removed", action: undo });
|
||||
vi.advanceTimersByTime(ACTION_TTL_MIN + 1);
|
||||
expect(shown(id)).toBe(true);
|
||||
vi.advanceTimersByTime(12000 - ACTION_TTL_MIN);
|
||||
expect(shown(id)).toBe(false);
|
||||
});
|
||||
|
||||
it("never dismisses when the reader chose never", () => {
|
||||
configureNotifications({ durationMs: 0 });
|
||||
const id = notify({ message: "removed", action: undo });
|
||||
vi.advanceTimersByTime(60000);
|
||||
expect(shown(id)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -92,6 +92,10 @@ const HISTORY_KEY = LS.notifHistory;
|
||||
const SETTINGS_KEY = LS.notifSettings;
|
||||
const MAX_HISTORY = 100;
|
||||
const ERROR_TTL = 15000;
|
||||
// A toast that carries an action (an "Undo") lives at least this long, whatever the reader's toast
|
||||
// time: its button is the only way back, and a 3 s toast leaves no time to reach it (owner, E28).
|
||||
// The setting still decides for longer, and "never" still means never.
|
||||
export const ACTION_TTL_MIN = 8000;
|
||||
|
||||
// 6b: per-account notification settings (persisted by the Settings panel into the
|
||||
// server `preferences`, mirrored to localStorage so they apply before login).
|
||||
@@ -269,7 +273,9 @@ export function notify(input: NotifyInput): number {
|
||||
: level === "error" || level === "fatal"
|
||||
? ERROR_TTL
|
||||
: config.durationMs > 0
|
||||
? config.durationMs
|
||||
? input.action
|
||||
? Math.max(config.durationMs, ACTION_TTL_MIN)
|
||||
: config.durationMs
|
||||
: undefined;
|
||||
|
||||
// Coalesce a rapid burst of identical notifications (same level+title+message within the
|
||||
|
||||
Reference in New Issue
Block a user