fix(admin): scope remembered list state to the signed-in admin and recover from bad restored queries

- Store list queries under summer-admin.list:<admin id>:<controller>; me()
  and setCurrentUser set the owner, which removes other admins' keys and
  the unscoped keys of older builds; nothing is read or written without one
- clearUser removes every stored query, so logout, session expiry and an
  unrecoverable 401 (expireSession, now used by main.ts) all clear them
- A list request built from a restored query that fails for any reason
  (422, 500, network) forgets the query and loads the plain list once
- A failing URL query keeps the error and offers a reset button
  (backend::lang.list.reset_list, en and pl)
- Document remembered list state in docs/backend/lists-and-filters.md
- Rebuild modules/boardwalk/dist
This commit is contained in:
2026-10-09 23:53:19 +02:00
parent 5be98a0c83
commit cd25af0878
15 changed files with 594 additions and 75 deletions

View File

@@ -28,6 +28,10 @@ import { showToast } from '../state/useToasts'
// kept per list in browser storage (listState.ts, D-01): entered without list
// params, the list restores the stored query into the URL before its first
// list request; a URL with list params wins and overwrites the stored query.
// A restored query the admin did not ask for never breaks the list: if its
// load fails for any reason, the stored query is forgotten and the plain list
// loads once instead (WR-02). A failed URL query keeps the error, with a reset
// back to the plain list next to it.
const SEARCH_DEBOUNCE = 300
const route = useRoute()
@@ -124,21 +128,47 @@ function replaceQuery(next: ListQuery): void {
const EMPTY_QUERY_KEY = listQueryKey({})
let generation = 0
const hasQuery = computed(() => listQueryKey(query.value) !== EMPTY_QUERY_KEY)
async function loadList(): Promise<void> {
let generation = 0
// Key of the query the watch restored from storage and has not loaded yet.
let restoredKey: string | null = null
/** Drops every list param: the stored query is forgotten, the plain list loads. */
function resetQuery(): void {
replaceQuery({})
}
/**
* Loads the rows of the URL query. restored marks the first load of a query
* restored from storage: if it fails for any reason (422, 500, network), the
* stored query is forgotten and the plain list loads. The plain list is never
* restored again, so this happens at most once per entry.
*/
async function loadList(restored = false): Promise<void> {
const current = ++generation
loading.value = true
listFailed.value = false
const result = await api.GET('/{vendor}/{plugin}/{controller}', { params: { path, query: query.value } })
let result: Awaited<ReturnType<typeof fetchList>> | null = null
try {
result = await fetchList()
} catch {
result = null
}
if (current !== generation) {
return
}
rows.value = result.data?.data ?? []
meta.value = result.data?.meta ?? null
listFailed.value = !result.data
if (!result?.data && restored) {
// Keeps loading until the plain list answers; no error flashes.
writeListState(controllerId, {})
void router.replace({ path: route.path, query: {} })
return
}
rows.value = result?.data?.data ?? []
meta.value = result?.data?.meta ?? null
listFailed.value = !result?.data
loading.value = false
if (result.response?.status === 422) {
if (result?.response?.status === 422) {
// A stale stored filter or sort never sticks.
writeListState(controllerId, {})
return
@@ -150,6 +180,10 @@ async function loadList(): Promise<void> {
}
}
function fetchList() {
return api.GET('/{vendor}/{plugin}/{controller}', { params: { path, query: query.value } })
}
async function load(): Promise<void> {
const result = await api.GET('/{vendor}/{plugin}/{controller}/schema/list', { params: { path } })
schema.value = result.data?.data ?? null
@@ -170,15 +204,19 @@ watch(
// restore the stored query into the URL and let this watch load it.
if (key === EMPTY_QUERY_KEY) {
const stored = readListState(controllerId)
if (listQueryKey(stored) !== EMPTY_QUERY_KEY) {
const storedKey = listQueryKey(stored)
if (storedKey !== EMPTY_QUERY_KEY) {
restoredKey = storedKey
void router.replace({ path: route.path, query: toListQuery(stored) })
return
}
}
const restored = restoredKey !== null && restoredKey === key
restoredKey = null
writeListState(controllerId, query.value)
selected.value = []
searchText.value = query.value.search ?? ''
void loadList()
void loadList(restored)
},
{ immediate: true },
)
@@ -417,9 +455,16 @@ async function onAction(name: string): Promise<void> {
:source="path"
@update="onFilter"
/>
<p v-if="failed && !loading" role="alert" class="border-t border-border px-5 py-4 text-danger">
{{ t('backend::lang.list.load_failed') }}
</p>
<div
v-if="failed && !loading"
data-list-failed
class="flex flex-wrap items-center justify-between gap-3 border-t border-border px-5 py-4"
>
<p role="alert" class="text-danger">{{ t('backend::lang.list.load_failed') }}</p>
<Button v-if="listFailed && hasQuery" data-action="reset-list" @click="resetQuery">
{{ t('backend::lang.list.reset_list') }}
</Button>
</div>
<DataTable
v-else
:columns="schema?.columns ?? []"