106 lines
3.9 KiB
TypeScript
106 lines
3.9 KiB
TypeScript
import { useCallback, useEffect, useState } from "react";
|
|
|
|
/**
|
|
* Read the current `?ack=…` query param off `window.location.search`.
|
|
* Returns `null` when the param is absent or empty.
|
|
*
|
|
* `URLSearchParams` is the standard, locale-free way to parse query
|
|
* strings in the browser. Using it (rather than hand-rolled string
|
|
* slicing) means we correctly handle multiple params and percent-encoded
|
|
* characters in ack ids without surprises.
|
|
*
|
|
* Param name is `?ack=` — chosen to mirror the existing `?provider=…`,
|
|
* `?claim=…`, and `?remit=…` drilldown conventions (one-word token,
|
|
* alphabetical brevity, no collision with the existing `Acks` table
|
|
* columns). The id value is treated as a string in the URL so deep
|
|
* links (`/acks?ack=42`) round-trip identically across the app, even
|
|
* though the backend `Ack.id` is a numeric — `AckDrawer` does the
|
|
* `Number()` coercion when calling `api.getAck`.
|
|
*/
|
|
function readAckId(): string | null {
|
|
const params = new URLSearchParams(window.location.search);
|
|
const value = params.get("ack");
|
|
return value === "" ? null : value;
|
|
}
|
|
|
|
/**
|
|
* Build the URL we want to push/replace into history.
|
|
*
|
|
* - `ackId === null` → drop the `?ack=` param, preserving any
|
|
* other params (e.g. `?page=2&ack=…` keeps `page=2`).
|
|
* - `ackId !== null` → set the param to the new id, also preserving
|
|
* any other params.
|
|
*
|
|
* We return `pathname + search + hash` (a relative URL) rather than the
|
|
* full href — `history.pushState` accepts a relative URL and rewriting
|
|
* only the relative form keeps the document's origin stable.
|
|
*/
|
|
function buildUrl(ackId: string | null): string {
|
|
const url = new URL(window.location.href);
|
|
if (ackId === null) {
|
|
url.searchParams.delete("ack");
|
|
} else {
|
|
url.searchParams.set("ack", ackId);
|
|
}
|
|
return url.pathname + url.search + url.hash;
|
|
}
|
|
|
|
/**
|
|
* Per-ack detail drawer URL state (AckDrawer).
|
|
*
|
|
* Mirrors `useProviderDrawerUrlState` / `useRemitDrawerUrlState` but
|
|
* for the 999-ACK drawer — reads `?ack=` from the URL on mount and
|
|
* keeps the value in sync with history as the drawer is opened and
|
|
* closed.
|
|
*
|
|
* - `ackId`: the id parsed from the URL (or `null` when the param
|
|
* is absent). React state so consumers re-render on changes.
|
|
* - `open(id)`: pushes a NEW history entry with `?ack={id}` — so the
|
|
* browser Back button returns to the previous page (e.g. the
|
|
* acks list) and not just to the previously-open ack.
|
|
* - `setAckId(id)`: REPLACES the current history entry — kept for
|
|
* symmetry with the other drawer hooks so a future j/k nav
|
|
* handler doesn't have to special-case ack ids.
|
|
* - `close()`: pushes a NEW entry that strips the param, so Back
|
|
* from the closed drawer returns to whatever page the user was
|
|
* on before opening the drawer.
|
|
*
|
|
* The hook subscribes to `popstate` so that browser Back/Forward
|
|
* (which fire popstate rather than our own pushState) propagate into
|
|
* the React state. Without this, hitting Back would change the URL
|
|
* but leave the drawer open on the stale id.
|
|
*/
|
|
export function useAckDrawerUrlState(): {
|
|
ackId: string | null;
|
|
open: (id: string) => void;
|
|
close: () => void;
|
|
setAckId: (id: string) => void;
|
|
} {
|
|
const [ackId, setAckIdState] = useState<string | null>(() => readAckId());
|
|
|
|
const open = useCallback((id: string) => {
|
|
window.history.pushState(null, "", buildUrl(id));
|
|
setAckIdState(id);
|
|
}, []);
|
|
|
|
const setAckId = useCallback((id: string) => {
|
|
window.history.replaceState(null, "", buildUrl(id));
|
|
setAckIdState(id);
|
|
}, []);
|
|
|
|
const close = useCallback(() => {
|
|
window.history.pushState(null, "", buildUrl(null));
|
|
setAckIdState(null);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const onPopState = () => {
|
|
setAckIdState(readAckId());
|
|
};
|
|
window.addEventListener("popstate", onPopState);
|
|
return () => window.removeEventListener("popstate", onPopState);
|
|
}, []);
|
|
|
|
return { ackId, open, close, setAckId };
|
|
}
|