feat(nav): tabs, list views and name searches are named in the address
Settings, Configuration and Users name their tab (stab/cfgtab/utab); a link's tab wins and a tab pre-selected while its page is on screen now switches it. A playlist's sort, filters and grouping (plsort/plf/plgroup), the Files sort (fsort, always written) and the name searches of Channels, Files, Playlists and Plex (cq/fq/plq/pq+pqf) reopen from the address; the searches are written once the typing pauses.
This commit is contained in:
@@ -35,6 +35,7 @@ vi.mock("../../lib/notifications", async (importOriginal) => ({
|
||||
vi.mock("../../lib/errorDialog", () => ({ reportError: vi.fn() }));
|
||||
|
||||
import AdminUsers from "./AdminUsers";
|
||||
import { PageToolbarSlot } from "../../components/ui/pageToolbarSlot";
|
||||
import { api, type AdminUserRow } from "../../lib/api";
|
||||
import { relativeTime } from "../../lib/format";
|
||||
import i18n from "../../i18n";
|
||||
@@ -190,3 +191,32 @@ describe("a user action that fails", () => {
|
||||
expect(notify.mock.calls.filter(([n]) => n.level === "error")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// The URL is the view: the tab on screen is named in the address, so a bookmark opens it.
|
||||
describe("the tab in the address", () => {
|
||||
afterEach(() => window.history.replaceState(null, "", "/"));
|
||||
const utab = () => new URLSearchParams(window.location.search).get("utab");
|
||||
const selected = () =>
|
||||
screen.getAllByRole("tab").find((el) => el.getAttribute("aria-selected") === "true")
|
||||
?.textContent;
|
||||
|
||||
it("opens on the tab a link names, and names the one picked", () => {
|
||||
window.history.replaceState(null, "", "/?page=users&utab=storage");
|
||||
vi.spyOn(api, "adminUsers").mockResolvedValue([row] as never);
|
||||
// The tab bar portals into the shell's toolbar band.
|
||||
const slot = document.body.appendChild(document.createElement("div"));
|
||||
render(
|
||||
<QueryClientProvider
|
||||
client={new QueryClient({ defaultOptions: { queries: { retry: false } } })}
|
||||
>
|
||||
<PageToolbarSlot.Provider value={slot}>
|
||||
<AdminUsers />
|
||||
</PageToolbarSlot.Provider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
expect(selected()).toBe("users.tabs.storage");
|
||||
expect(utab()).toBe("storage");
|
||||
fireEvent.click(screen.getByRole("tab", { name: "users.tabs.roles" }));
|
||||
expect(utab()).toBe("roles");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@ import { displayName } from "../../lib/api/account";
|
||||
import { notify } from "../../lib/notifications";
|
||||
import { formatDateTime, relativeTime } from "../../lib/format";
|
||||
import { invalidateUserAccess } from "../../lib/adminUsers";
|
||||
import { ADMIN_USERS_TAB_KEY } from "../../lib/adminUsersTab";
|
||||
import { ADMIN_USERS_TAB_KEY, USERS_TAB_URL_KEY } from "../../lib/adminUsersTab";
|
||||
import { Badge } from "../../components/ui/Badge";
|
||||
import { Button } from "../../components/ui/Button";
|
||||
import { IconButton } from "../../components/ui/IconButton";
|
||||
@@ -42,6 +42,7 @@ import { useConfirm } from "../../components/ui/confirmContext";
|
||||
import Tabs from "../../components/ui/Tabs";
|
||||
import { useTabPanel } from "../../components/ui/tabPanel";
|
||||
import { useTabPersistedState } from "../../lib/storage";
|
||||
import { useUrlMirror } from "../../lib/useUrlMirror";
|
||||
import { PageToolbar } from "../../components/ui/PageShell";
|
||||
import { useMe } from "../../lib/useMe";
|
||||
import { notifyActionError, notifyKnownFailure } from "../../lib/actionErrors";
|
||||
@@ -71,7 +72,7 @@ const ROW_OPEN =
|
||||
export default function AdminUsers() {
|
||||
const me = useMe();
|
||||
const { t } = useTranslation();
|
||||
const [tab, setTab] = useTabPersistedState(ADMIN_USERS_TAB_KEY, "roles");
|
||||
const [tab, setTab] = useTabPersistedState(ADMIN_USERS_TAB_KEY, "roles", USERS_TAB_URL_KEY);
|
||||
const panel = useTabPanel();
|
||||
const tabs = [
|
||||
{ id: "roles", label: t("users.tabs.roles") },
|
||||
@@ -87,6 +88,8 @@ export default function AdminUsers() {
|
||||
{ id: "storage", label: t("users.tabs.storage") },
|
||||
];
|
||||
const active = tabs.some((x) => x.id === tab) ? tab : "roles";
|
||||
// The address names the tab on screen (always: see SettingsPanel).
|
||||
useUrlMirror({ [USERS_TAB_URL_KEY]: active });
|
||||
return (
|
||||
<>
|
||||
{/* The tab bar is fixed chrome; the tab content scrolls under it. */}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Users } from "lucide-react";
|
||||
import { lazyWithPreload } from "../../lib/lazyWithPreload";
|
||||
import type { ModuleDef } from "../../lib/moduleDef";
|
||||
import { USERS_TAB_URL_KEY } from "../../lib/adminUsersTab";
|
||||
|
||||
// This page as the shell and the navigation know it (lib/moduleDef). Its page loads lazily.
|
||||
export const usersModule: ModuleDef = {
|
||||
@@ -13,5 +14,5 @@ export const usersModule: ModuleDef = {
|
||||
gate: (me) => me.role === "admin",
|
||||
Page: lazyWithPreload(() => import("./AdminUsers")),
|
||||
maxWidth: "48rem",
|
||||
urlKeys: [],
|
||||
urlKeys: [USERS_TAB_URL_KEY],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user