Persist list pagination and search in the URL
List pages kept the current page in local state, so clicking into an asset and hitting Back remounted the list at page 1. A shared useListQuery composable now mirrors the page (and search term) into the URL query via router.replace across all 18 list pages, so Back restores the page you were on and lists are deep-linkable. Page 1 with no search stays a bare path; changing a filter resets to page 1; unrelated query keys are preserved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
102
frontend/src/composables/listQuery.js
Normal file
102
frontend/src/composables/listQuery.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
// Keep a list page's current page and search term in the URL query so the
|
||||
// browser Back button restores them. Without this a list keeps page in local
|
||||
// state and remounts at page 1 after visiting a detail page and going Back.
|
||||
//
|
||||
// Usage in a list view:
|
||||
// const { page, search, setPage, setSearch } = useListQuery({ onChange: loadRows })
|
||||
// goToPage(p) -> setPage(p); loadRows()
|
||||
// debouncedSearch -> setSearch(search.value); loadRows()
|
||||
// filter reset -> setPage(1); loadRows()
|
||||
//
|
||||
// Options:
|
||||
// onChange callback run when the query changes from outside (Back/Forward,
|
||||
// deep link) so the list reloads at the restored page.
|
||||
// extraKeys optional extra query keys to persist (e.g. ['typeid']); each
|
||||
// gets a ref exposed under the returned `extras` object.
|
||||
export function useListQuery(options = {}) {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const onChange = options.onChange || (() => {})
|
||||
const extraKeys = options.extraKeys || []
|
||||
|
||||
const page = ref(parseInt(route.query.page, 10) || 1)
|
||||
const search = ref(route.query.q || '')
|
||||
|
||||
const extras = {}
|
||||
for (const key of extraKeys) {
|
||||
extras[key] = ref(route.query[key] || '')
|
||||
}
|
||||
|
||||
// Build the next query. Keep unrelated keys intact. Write page only when > 1
|
||||
// and search only when non-empty so the clean state is a bare path.
|
||||
function buildQuery() {
|
||||
const query = { ...route.query }
|
||||
if (page.value > 1) query.page = String(page.value)
|
||||
else delete query.page
|
||||
if (search.value) query.q = search.value
|
||||
else delete query.q
|
||||
for (const key of extraKeys) {
|
||||
if (extras[key].value) query[key] = String(extras[key].value)
|
||||
else delete query[key]
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
// replace (not push) so paging does not spam history; Back leaves the list.
|
||||
function syncUrl() {
|
||||
const query = buildQuery()
|
||||
const current = route.query
|
||||
const keys = new Set([...Object.keys(query), ...Object.keys(current)])
|
||||
let same = true
|
||||
for (const key of keys) {
|
||||
if (String(query[key] ?? '') !== String(current[key] ?? '')) {
|
||||
same = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!same) router.replace({ query })
|
||||
}
|
||||
|
||||
function setPage(newPage) {
|
||||
page.value = newPage
|
||||
syncUrl()
|
||||
}
|
||||
|
||||
// Changing the search resets to page 1.
|
||||
function setSearch(term) {
|
||||
search.value = term
|
||||
page.value = 1
|
||||
syncUrl()
|
||||
}
|
||||
|
||||
// Changing an extra filter resets to page 1.
|
||||
function setExtra(key, value) {
|
||||
extras[key].value = value
|
||||
page.value = 1
|
||||
syncUrl()
|
||||
}
|
||||
|
||||
// Re-sync refs when the query changes from outside (Back/Forward, deep link).
|
||||
// Fire onChange only when page/search/extras actually changed so a self
|
||||
// syncUrl() call does not trigger a redundant reload.
|
||||
watch(() => route.query, (newQuery) => {
|
||||
const newPage = parseInt(newQuery.page, 10) || 1
|
||||
const newSearch = newQuery.q || ''
|
||||
let changed = false
|
||||
if (newPage !== page.value) { page.value = newPage; changed = true }
|
||||
if (newSearch !== search.value) { search.value = newSearch; changed = true }
|
||||
for (const key of extraKeys) {
|
||||
const newValue = newQuery[key] || ''
|
||||
if (newValue !== String(extras[key].value || '')) {
|
||||
extras[key].value = newValue
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if (changed) onChange()
|
||||
})
|
||||
|
||||
return { page, search, extras, setPage, setSearch, setExtra }
|
||||
}
|
||||
Reference in New Issue
Block a user