feat(catalog): persist bounds filter state in URL
All checks were successful
Build Docker Image / build (push) Successful in 4m14s

- Add urlBounds and filterByBounds computed from URL query
- Add setBoundsInUrl and clearBoundsFromUrl actions
- Update index.vue to use URL-based bounds state
- Bounds written to URL as comma-separated values (west,south,east,north)

This enables sharing links with map viewport bounds filter.
This commit is contained in:
Ruslan Bakiev
2026-01-26 21:40:44 +07:00
parent f9eb027ebd
commit 6545eeabea
2 changed files with 47 additions and 6 deletions

View File

@@ -84,6 +84,18 @@ export function useCatalogSearch() {
const hubId = computed(() => route.query.hub as string | undefined) const hubId = computed(() => route.query.hub as string | undefined)
const quantity = computed(() => route.query.qty as string | undefined) const quantity = computed(() => route.query.qty as string | undefined)
// Map bounds from URL (format: west,south,east,north)
const urlBounds = computed(() => {
const b = route.query.bounds as string | undefined
if (!b) return null
const parts = b.split(',').map(Number)
if (parts.length !== 4 || parts.some(isNaN)) return null
return { west: parts[0], south: parts[1], east: parts[2], north: parts[3] }
})
// Filter by bounds checkbox state from URL
const filterByBounds = computed(() => route.query.bounds !== undefined)
// Get label for a filter (from cache or fallback to ID) // Get label for a filter (from cache or fallback to ID)
const getLabel = (type: string, id: string | undefined): string | null => { const getLabel = (type: string, id: string | undefined): string | null => {
if (!id) return null if (!id) return null
@@ -237,6 +249,21 @@ export function useCatalogSearch() {
updateQuery({ qty }) updateQuery({ qty })
} }
// Set map bounds in URL (for filter by map feature)
const setBoundsInUrl = (bounds: { west: number; south: number; east: number; north: number } | null) => {
if (bounds) {
const boundsStr = `${bounds.west.toFixed(4)},${bounds.south.toFixed(4)},${bounds.east.toFixed(4)},${bounds.north.toFixed(4)}`
updateQuery({ bounds: boundsStr })
} else {
updateQuery({ bounds: null })
}
}
// Clear bounds from URL
const clearBoundsFromUrl = () => {
updateQuery({ bounds: null })
}
const openInfo = (type: InfoEntityType, uuid: string) => { const openInfo = (type: InfoEntityType, uuid: string) => {
updateQuery({ info: `${type}:${uuid}`, select: null, infoTab: null, infoProduct: null }) updateQuery({ info: `${type}:${uuid}`, select: null, infoTab: null, infoProduct: null })
} }
@@ -350,6 +377,8 @@ export function useCatalogSearch() {
quantity, quantity,
searchQuery, searchQuery,
mapViewMode, mapViewMode,
urlBounds,
filterByBounds,
// Drawer state // Drawer state
isDrawerOpen, isDrawerOpen,
@@ -373,6 +402,8 @@ export function useCatalogSearch() {
removeFilter, removeFilter,
editFilter, editFilter,
setQuantity, setQuantity,
setBoundsInUrl,
clearBoundsFromUrl,
openInfo, openInfo,
closeInfo, closeInfo,
setInfoTab, setInfoTab,

View File

@@ -13,7 +13,7 @@
:related-points="relatedPoints" :related-points="relatedPoints"
@select="onMapSelect" @select="onMapSelect"
@bounds-change="onBoundsChange" @bounds-change="onBoundsChange"
@update:filter-by-bounds="filterByBounds = $event" @update:filter-by-bounds="$event ? setBoundsInUrl(currentMapBounds) : clearBoundsFromUrl()"
> >
<!-- Panel slot - shows selection list OR info OR quote results --> <!-- Panel slot - shows selection list OR info OR quote results -->
<template #panel> <template #panel>
@@ -82,8 +82,7 @@ const localePath = useLocalePath()
// Ref to CatalogPage for accessing bounds // Ref to CatalogPage for accessing bounds
const catalogPageRef = ref<{ currentBounds: Ref<MapBounds | null> } | null>(null) const catalogPageRef = ref<{ currentBounds: Ref<MapBounds | null> } | null>(null)
// Filter by map bounds state // Current map bounds (local state, updated when map moves)
const filterByBounds = ref(false)
const currentMapBounds = ref<MapBounds | null>(null) const currentMapBounds = ref<MapBounds | null>(null)
// Hovered item for map highlight // Hovered item for map highlight
@@ -103,6 +102,10 @@ const currentSelectionItems = computed(() => {
// Handle bounds change from map // Handle bounds change from map
const onBoundsChange = (bounds: MapBounds) => { const onBoundsChange = (bounds: MapBounds) => {
currentMapBounds.value = bounds currentMapBounds.value = bounds
// If filter by bounds is enabled, write to URL
if (filterByBounds.value) {
setBoundsInUrl(bounds)
}
} }
const { const {
@@ -122,7 +125,11 @@ const {
openInfo, openInfo,
closeInfo, closeInfo,
setInfoProduct, setInfoProduct,
setLabel setLabel,
urlBounds,
filterByBounds,
setBoundsInUrl,
clearBoundsFromUrl
} = useCatalogSearch() } = useCatalogSearch()
// Info panel composable // Info panel composable
@@ -230,12 +237,15 @@ watch(productId, (newProductId) => {
}, { immediate: true }) }, { immediate: true })
// Apply bounds filter when "filter by map bounds" is enabled // Apply bounds filter when "filter by map bounds" is enabled
watch([filterByBounds, currentMapBounds], ([enabled, bounds]) => { // Use URL bounds if available, otherwise use current map bounds
watch([filterByBounds, urlBounds, currentMapBounds], ([enabled, urlB, mapB]) => {
// Use URL bounds if available, otherwise current map bounds
const bounds = urlB || mapB
const boundsToApply = enabled && bounds ? bounds : null const boundsToApply = enabled && bounds ? bounds : null
setHubBoundsFilter(boundsToApply) setHubBoundsFilter(boundsToApply)
setSupplierBoundsFilter(boundsToApply) setSupplierBoundsFilter(boundsToApply)
setProductBoundsFilter(boundsToApply) setProductBoundsFilter(boundsToApply)
}) }, { immediate: true })
// Watch infoId to load info data // Watch infoId to load info data
watch(infoId, async (info) => { watch(infoId, async (info) => {