fix(lib): remove UUID from URL when popup closes (#435)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: antontranelis <31516529+antontranelis@users.noreply.github.com>
Co-authored-by: Anton Tranelis <mail@antontranelis.de>
Co-authored-by: mahula <lenzmath@posteo.de>
This commit is contained in:
Copilot 2025-11-18 18:59:55 +00:00 committed by GitHub
parent 6aa9f014d7
commit d5080e2bc9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 127 additions and 42 deletions

View File

@ -5,7 +5,6 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */ /* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */
/* eslint-disable @typescript-eslint/prefer-optional-chain */ /* eslint-disable @typescript-eslint/prefer-optional-chain */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { LatLng } from 'leaflet' import { LatLng } from 'leaflet'
import { forwardRef, useState } from 'react' import { forwardRef, useState } from 'react'
@ -17,6 +16,7 @@ import { useRemoveItem, useUpdateItem } from '#components/Map/hooks/useItems'
import { usePopupForm } from '#components/Map/hooks/usePopupForm' import { usePopupForm } from '#components/Map/hooks/usePopupForm'
import { useSetSelectPosition } from '#components/Map/hooks/useSelectPosition' import { useSetSelectPosition } from '#components/Map/hooks/useSelectPosition'
import { timeAgo } from '#utils/TimeAgo' import { timeAgo } from '#utils/TimeAgo'
import { removeItemFromUrl } from '#utils/UrlHelper'
import { HeaderView } from './ItemPopupComponents/HeaderView' import { HeaderView } from './ItemPopupComponents/HeaderView'
import { TextView } from './ItemPopupComponents/TextView' import { TextView } from './ItemPopupComponents/TextView'
@ -80,8 +80,7 @@ export const ItemViewPopup = forwardRef((props: ItemViewPopupProps, ref: any) =>
} }
setLoading(false) setLoading(false)
map.closePopup() map.closePopup()
const params = new URLSearchParams(window.location.search) removeItemFromUrl()
window.history.pushState({}, '', '/' + `${params ? `?${params}` : ''}`)
navigate('/') navigate('/')
} }

View File

@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/no-empty-function */ /* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable @typescript-eslint/restrict-plus-operands */ /* eslint-disable @typescript-eslint/restrict-plus-operands */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
@ -15,6 +14,12 @@ import { toast } from 'react-toastify'
import { useSetAppState } from '#components/AppShell/hooks/useAppState' import { useSetAppState } from '#components/AppShell/hooks/useAppState'
import { useTheme } from '#components/AppShell/hooks/useTheme' import { useTheme } from '#components/AppShell/hooks/useTheme'
import { containsUUID } from '#utils/ContainsUUID' import { containsUUID } from '#utils/ContainsUUID'
import {
removeItemFromUrl,
resetMetaTags as resetMetaTagsUtil,
setItemInUrl,
updateMetaTags,
} from '#utils/UrlHelper'
import { useClusterRef, useSetClusterRef } from './hooks/useClusterRef' import { useClusterRef, useSetClusterRef } from './hooks/useClusterRef'
import { import {
@ -152,21 +157,45 @@ export function UtopiaMapInner({
return null return null
} }
// Track if we're currently switching popups to prevent URL cleanup
const popupCloseTimeoutRef = useRef<NodeJS.Timeout | null>(null)
useMapEvents({ useMapEvents({
popupopen: (e) => { popupopen: (e) => {
const item = Object.entries(leafletRefs).find((r) => r[1].popup === e.popup)?.[1].item const item = Object.entries(leafletRefs).find((r) => r[1].popup === e.popup)?.[1].item
if (window.location.pathname.split('/')[1] !== item?.id) {
const params = new URLSearchParams(window.location.search) // Cancel any pending popup close URL cleanup - we're opening a new popup
if (!location.pathname.includes('/item/')) { if (popupCloseTimeoutRef.current) {
window.history.pushState( clearTimeout(popupCloseTimeoutRef.current)
{}, popupCloseTimeoutRef.current = null
'', }
`/${item?.id}` + `${params.toString() !== '' ? `?${params}` : ''}`,
) // Only update URL if no profile is open
if (!location.pathname.includes('/item/')) {
if (window.location.pathname.split('/')[1] !== item?.id && item?.id) {
setItemInUrl(item.id)
} }
let title = '' if (item?.name) {
if (item?.name) title = item.name updateMetaTags(item.name, item.text)
document.title = `${document.title.split('-')[0]} - ${title}` }
}
// If profile is open, don't change URL but still update meta tags
else if (item?.name) {
updateMetaTags(item.name, item.text)
}
},
popupclose: () => {
// Only remove UUID from URL if no profile is open
if (!location.pathname.includes('/item/')) {
// Wait briefly to see if another popup is being opened
// If so, the popupopen handler will cancel this timeout
popupCloseTimeoutRef.current = setTimeout(() => {
if (containsUUID(window.location.pathname)) {
removeItemFromUrl()
resetMetaTagsUtil()
}
popupCloseTimeoutRef.current = null
}, 50)
} }
}, },
}) })
@ -185,15 +214,9 @@ export function UtopiaMapInner({
clusterRef?.zoomToShowLayer(ref.marker, () => { clusterRef?.zoomToShowLayer(ref.marker, () => {
ref.marker.openPopup() ref.marker.openPopup()
}) })
let title = '' if (ref.item.name) {
if (ref.item.name) title = ref.item.name updateMetaTags(ref.item.name, ref.item.text)
document.title = `${document.title.split('-')[0]} - ${title}` }
document
.querySelector('meta[property="og:title"]')
?.setAttribute('content', ref.item.name ?? '')
document
.querySelector('meta[property="og:description"]')
?.setAttribute('content', ref.item.text ?? '')
} }
} }
} }
@ -205,18 +228,10 @@ export function UtopiaMapInner({
}, [leafletRefs, location]) }, [leafletRefs, location])
const resetMetaTags = () => { const resetMetaTags = () => {
const params = new URLSearchParams(window.location.search) if (containsUUID(window.location.pathname)) {
if (!containsUUID(window.location.pathname)) { removeItemFromUrl()
window.history.pushState({}, '', '/' + `${params.toString() !== '' ? `?${params}` : ''}`)
} }
document.title = document.title.split('-')[0] resetMetaTagsUtil()
document.querySelector('meta[property="og:title"]')?.setAttribute('content', document.title)
document
.querySelector('meta[property="og:description"]')
?.setAttribute(
'content',
`${document.querySelector('meta[name="description"]')?.getAttribute('content')}`,
)
} }
const onEachFeature = (feature: Feature<GeoJSONGeometry, any>, layer: L.Layer) => { const onEachFeature = (feature: Feature<GeoJSONGeometry, any>, layer: L.Layer) => {

View File

@ -17,6 +17,7 @@ import { ActionButton } from '#components/Profile/Subcomponents/ActionsButton'
import { LinkedItemsHeaderView } from '#components/Profile/Subcomponents/LinkedItemsHeaderView' import { LinkedItemsHeaderView } from '#components/Profile/Subcomponents/LinkedItemsHeaderView'
import { TagView } from '#components/Templates/TagView' import { TagView } from '#components/Templates/TagView'
import { timeAgo } from '#utils/TimeAgo' import { timeAgo } from '#utils/TimeAgo'
import { setUrlParam } from '#utils/UrlHelper'
import type { Item } from '#types/Item' import type { Item } from '#types/Item'
import type { Tag } from '#types/Tag' import type { Tag } from '#types/Tag'
@ -67,11 +68,7 @@ export const TabsView = ({
const updateActiveTab = useCallback( const updateActiveTab = useCallback(
(id: number) => { (id: number) => {
setActiveTab(id) setActiveTab(id)
setUrlParam('tab', `${id}`)
const params = new URLSearchParams(window.location.search)
params.set('tab', `${id}`)
const newUrl = location.pathname + '?' + params.toString()
window.history.pushState({}, '', newUrl)
}, },
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
[location.pathname], [location.pathname],

View File

@ -14,6 +14,7 @@ import { toast } from 'react-toastify'
import { encodeTag } from '#utils/FormatTags' import { encodeTag } from '#utils/FormatTags'
import { hashTagRegex } from '#utils/HashTagRegex' import { hashTagRegex } from '#utils/HashTagRegex'
import { randomColor } from '#utils/RandomColor' import { randomColor } from '#utils/RandomColor'
import { removeItemFromUrl } from '#utils/UrlHelper'
import type { FormState } from '#types/FormState' import type { FormState } from '#types/FormState'
import type { Item } from '#types/Item' import type { Item } from '#types/Item'
@ -185,8 +186,7 @@ export const handleDelete = async (
toast.success('Item deleted') toast.success('Item deleted')
map.closePopup() map.closePopup()
const params = new URLSearchParams(window.location.search) removeItemFromUrl()
window.history.pushState({}, '', '/' + `${params ? `?${params}` : ''}`)
navigate('/') navigate('/')
} catch (error) { } catch (error) {
toast.error(error instanceof Error ? error.message : String(error)) toast.error(error instanceof Error ? error.message : String(error))

View File

@ -0,0 +1,74 @@
/**
* Utility functions for managing browser URL and history
*/
/**
* Updates the browser URL with an item ID while preserving query parameters
* @param itemId - The item UUID to add to the URL
*/
export function setItemInUrl(itemId: string): void {
const params = new URLSearchParams(window.location.search)
const paramsString = params.toString()
const newUrl = `/${itemId}${paramsString !== '' ? `?${paramsString}` : ''}`
window.history.pushState({}, '', newUrl)
}
/**
* Removes the item ID from the browser URL while preserving query parameters
*/
export function removeItemFromUrl(): void {
const params = new URLSearchParams(window.location.search)
const paramsString = params.toString()
const newUrl = `/${paramsString !== '' ? `?${paramsString}` : ''}`
window.history.pushState({}, '', newUrl)
}
/**
* Updates a specific query parameter in the URL while preserving the path
* @param key - The parameter key
* @param value - The parameter value
*/
export function setUrlParam(key: string, value: string): void {
const params = new URLSearchParams(window.location.search)
params.set(key, value)
const newUrl = window.location.pathname + '?' + params.toString()
window.history.pushState({}, '', newUrl)
}
/**
* Removes a specific query parameter from the URL
* @param key - The parameter key to remove
*/
export function removeUrlParam(key: string): void {
const params = new URLSearchParams(window.location.search)
params.delete(key)
const paramsString = params.toString()
const newUrl = window.location.pathname + (paramsString !== '' ? `?${paramsString}` : '')
window.history.pushState({}, '', newUrl)
}
/**
* Resets page title and OpenGraph meta tags to default values
*/
export function resetMetaTags(): void {
document.title = document.title.split('-')[0].trim()
document.querySelector('meta[property="og:title"]')?.setAttribute('content', document.title)
const description = document.querySelector('meta[name="description"]')?.getAttribute('content')
if (description) {
document.querySelector('meta[property="og:description"]')?.setAttribute('content', description)
}
}
/**
* Updates page title and OpenGraph meta tags with item information
* @param itemName - The name of the item
* @param itemText - The text/description of the item
*/
export function updateMetaTags(itemName: string, itemText?: string): void {
const baseTitle = document.title.split('-')[0].trim()
document.title = `${baseTitle} - ${itemName}`
document.querySelector('meta[property="og:title"]')?.setAttribute('content', itemName)
if (itemText) {
document.querySelector('meta[property="og:description"]')?.setAttribute('content', itemText)
}
}