// Per-list query memory (D-01). The list query (search, sort, dir, page, // per_page and filter[]) lives in the URL; every query a list loads is // also kept in localStorage, one key per signed-in admin and list: // LIST_STATE_PREFIX + ":" + controller id. A list opened without // list params (navigation, breadcrumb, a form's back, cancel, save or delete) // can then restore it. Only these list query parameters are stored, never a // token or record data. Search terms can hold personal data, so stored // queries never outlive the session or cross admins (T-16.1-03): useAuth sets // the owner when the profile loads, which removes every list key of any other // admin and the unscoped keys of older builds, and clears the owner on every // session end (logout, expiry, a 401 the refresh cannot recover), which // removes every list key. Without an owner nothing is read or written. // Disabled or full storage is a no-op: the URL still carries the state. import type { ListQuery } from '../api/types' import { listQueryKey, parseListQuery, toListQuery } from '../app/listQuery' export const LIST_STATE_PREFIX = 'summer-admin.list:' const EMPTY_KEY = listQueryKey({}) // The signed-in admin's id as a string, or null when nobody is signed in. let owner: string | null = null function storage(): Storage | null { try { return typeof window === 'undefined' ? null : window.localStorage } catch { return null } } /** The key prefix of one admin's lists. */ export function listStateOwnerPrefix(adminId: number | string): string { return `${LIST_STATE_PREFIX}${adminId}:` } function keyOf(controllerId: string): string | null { return owner === null ? null : listStateOwnerPrefix(owner) + controllerId } function remove(key: string): void { try { storage()?.removeItem(key) } catch { // Storage may be disabled; nothing was kept then. } } /** Removes every list key for which drop answers true. */ function removeListKeys(drop: (key: string) => boolean): void { const store = storage() if (!store) { return } try { const keys: string[] = [] for (let i = 0; i < store.length; i++) { const key = store.key(i) if (key !== null && key.startsWith(LIST_STATE_PREFIX) && drop(key)) { keys.push(key) } } for (const key of keys) { store.removeItem(key) } } catch { // Storage may be disabled; nothing was kept then. } } /** * Sets the admin whose list queries are read and written. A signed-in admin * id removes every list key that is not theirs: another admin's queries and * the unscoped `summer-admin.list:` keys of older builds. Null * (signed out) removes every list key. */ export function setListStateOwner(adminId: number | string | null): void { const next = adminId === null || String(adminId) === '' ? null : String(adminId) owner = next if (next === null) { removeListKeys(() => true) return } const prefix = listStateOwnerPrefix(next) removeListKeys((key) => !key.startsWith(prefix)) } /** The id whose list queries are kept, or null when nobody is signed in. */ export function listStateOwner(): string | null { return owner } /** * The stored query of a list. Stored values pass through parseListQuery like * a hand-edited URL; anything that is not an object of strings is removed and * reads as the empty query. Without an owner it is always the empty query. */ export function readListState(controllerId: string): ListQuery { const key = keyOf(controllerId) if (key === null) { return {} } let raw: string | null = null try { raw = storage()?.getItem(key) ?? null } catch { return {} } if (raw === null) { return {} } let value: unknown try { value = JSON.parse(raw) } catch { remove(key) return {} } if ( typeof value !== 'object' || value === null || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype || !Object.values(value).every((item) => typeof item === 'string') ) { remove(key) return {} } return parseListQuery(value as Record) } /** * Stores a list's query for the owner; the empty query removes the key * instead. Without an owner nothing is stored. */ export function writeListState(controllerId: string, query: ListQuery): void { const key = keyOf(controllerId) if (key === null) { return } if (listQueryKey(query) === EMPTY_KEY) { remove(key) return } try { storage()?.setItem(key, JSON.stringify(toListQuery(query))) } catch { // Storage may be full or disabled; the URL still carries the state. } } /** Removes every stored list query of every admin, keeping the owner. */ export function clearListStates(): void { removeListKeys(() => true) }