Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | 27x 9x 12x 12x 12x 12x 14x 43x 42x 14x 12x 16x 15x 15x 3x 12x 1057x 1057x 1057x 1057x 44x 44x 2x 1x 1057x | import { useState } from "react";
import { redirect, useNavigate, useSearch } from "@tanstack/react-router";
/**
* Filters for the admin Pros and Projects lists (#1153).
*
* Each filter is local state seeded from the URL and written back to it with
* `replace`, so browser Back and a reload land on the same filtered list.
*
* Back arrows, breadcrumbs, post-delete redirects and the sidebar all go to
* the bare list path. So the list route remembers the last search it showed
* and puts it back on any bare URL the filters did not write themselves. The
* filters tag their own writes in history state; that is what lets an admin
* clear every filter on purpose. It also covers a sidebar click while already
* on the list: the page stays mounted with its filters, and the restore keeps
* the URL in step with them.
*
* The memory lasts only while the admin stays inside that list's pages (the
* list, a detail, a sub-page).
* Going anywhere else clears it, so a dashboard tile or the sidebar opened
* from elsewhere still shows an unfiltered list.
*/
export type AdminListPath = "/admin/pros" | "/admin/projects";
export type AdminListSearch = {
q?: string;
status?: string;
incomplete?: boolean;
/** Column key; a leading "-" means descending. */
sort?: string;
};
const text = (v: unknown) =>
(typeof v === "string" || typeof v === "number") && v !== ""
? String(v)
: undefined;
/** `validateSearch` for both list routes. Unknown or malformed values are dropped. */
export function parseAdminListSearch(
raw: Record<string, unknown>,
): AdminListSearch {
return {
q: text(raw.q),
status: text(raw.status),
incomplete: raw.incomplete === true ? true : undefined,
sort: text(raw.sort),
};
}
// ponytail: in-memory, so a full reload of a detail page forgets it — the
// list then opens unfiltered, which is the pre-#1153 behaviour.
const lastSearch = new Map<AdminListPath, AdminListSearch>();
const isEmpty = (s: AdminListSearch) =>
Object.values(s).every((v) => v === undefined);
const isUnder = (pathname: string, path: AdminListPath) =>
pathname === path || pathname.startsWith(`${path}/`);
type BeforeLoadCause = "enter" | "stay" | "preload";
/** Admin layout `beforeLoad`: forget a list's filters once the admin leaves its pages. */
export function forgetListSearchOutside(
pathname: string,
cause: BeforeLoadCause,
) {
if (cause === "preload") return;
for (const path of lastSearch.keys()) {
if (!isUnder(pathname, path)) lastSearch.delete(path);
}
}
/** History-state tag on the URL writes made by `useListFilter`. */
export const LIST_FILTER_WRITE = "adminListFilter";
/** List route `beforeLoad`: a bare URL the filters did not write gets the remembered filters back; anything else is remembered. */
export function restoreListSearch(
path: AdminListPath,
search: AdminListSearch,
cause: BeforeLoadCause,
writtenByFilter: boolean,
) {
if (cause === "preload") return;
const saved = lastSearch.get(path);
if (!writtenByFilter && isEmpty(search) && saved && !isEmpty(saved)) {
throw redirect({ to: path, search: saved, replace: true });
}
lastSearch.set(path, search);
}
/** Like `useState`, seeded from the list's URL param and mirrored back to it. */
export function useListFilter<K extends keyof AdminListSearch>(
path: AdminListPath,
key: K,
fallback: NonNullable<AdminListSearch[K]>,
) {
const navigate = useNavigate();
const fromUrl = (useSearch({ strict: false }) as AdminListSearch)[key];
const [value, setValue] = useState(fromUrl ?? fallback);
const set = (next: NonNullable<AdminListSearch[K]>) => {
setValue(next);
navigate({
to: path,
search: (prev: AdminListSearch) => ({
...prev,
[key]: next === fallback ? undefined : next,
}),
state: (prev) => ({ ...prev, [LIST_FILTER_WRITE]: true }),
replace: true,
});
};
return [value, set] as const;
}
|