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:
@@ -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 ?? []"
|
||||
|
||||
Reference in New Issue
Block a user