Merge pull request #263 from Pouzor/feat/design-url-sync

feat: sync active design to URL for refresh/share
This commit is contained in:
Pouzor - Rémy Jardient
2026-07-09 17:09:27 +02:00
committed by GitHub
3 changed files with 80 additions and 2 deletions
+14 -2
View File
@@ -8,6 +8,7 @@ import { getCenteredPosition } from '@/utils/viewportCenter'
import { resolveVirtualEdgeParent } from '@/utils/virtualEdgeParent'
import { generateMarkdownTable } from '@/utils/exportMarkdown'
import { copyToClipboard } from '@/utils/clipboard'
import { getDesignIdFromUrl, setDesignIdInUrl } from '@/utils/designUrl'
import { ExportModal } from '@/components/modals/ExportModal'
import { exportCanvasToYaml, downloadYaml } from '@/utils/exportYaml'
import { parseYamlToCanvas } from '@/utils/importYaml'
@@ -171,10 +172,15 @@ export default function App() {
}, [loadCanvas, setTheme, setCustomStyle, setFloorMap])
const loadDesignsAndCanvas = useCallback(async () => {
// Prefer a design id explicitly requested via the URL (?design=<id>), so a
// refresh or shared link opens that design. Ignore it when it doesn't match
// a known design and fall back to the current/default one.
const urlDesignId = getDesignIdFromUrl()
if (STANDALONE) {
const designs = standaloneStorage.ensureSeed()
setDesigns(designs)
const targetId = activeDesignId ?? designs[0]?.id
const fromUrl = urlDesignId && designs.some((d) => d.id === urlDesignId) ? urlDesignId : null
const targetId = fromUrl ?? activeDesignId ?? designs[0]?.id
if (targetId) {
setActiveDesign(targetId)
loadStandaloneCanvas(targetId)
@@ -185,7 +191,8 @@ export default function App() {
const res = await designsApi.list()
const loadedDesigns = res.data
setDesigns(loadedDesigns)
const targetId = activeDesignId ?? loadedDesigns[0]?.id
const fromUrl = urlDesignId && loadedDesigns.some((d) => d.id === urlDesignId) ? urlDesignId : null
const targetId = fromUrl ?? activeDesignId ?? loadedDesigns[0]?.id
if (targetId) {
setActiveDesign(targetId)
await loadCanvasFromApi(targetId)
@@ -260,6 +267,11 @@ export default function App() {
}
}, [activeDesignId])
// Reflect the active design into the URL so refresh/share reopens it.
useEffect(() => {
if (activeDesignId) setDesignIdInUrl(activeDesignId)
}, [activeDesignId])
// Keep refs for store actions so keydown handler is always up-to-date without re-registering
const undoRef = useRef(undo)
const redoRef = useRef(redo)
@@ -0,0 +1,44 @@
import { describe, it, expect, beforeEach } from 'vitest'
import { getDesignIdFromUrl, setDesignIdInUrl } from '@/utils/designUrl'
describe('designUrl', () => {
beforeEach(() => {
window.history.replaceState(null, '', '/')
})
it('returns null when no design param is present', () => {
expect(getDesignIdFromUrl()).toBeNull()
})
it('reads the design id from the URL', () => {
window.history.replaceState(null, '', '/?design=abc-123')
expect(getDesignIdFromUrl()).toBe('abc-123')
})
it('writes the design id into the URL without adding history', () => {
const before = window.history.length
setDesignIdInUrl('design-42')
expect(getDesignIdFromUrl()).toBe('design-42')
expect(window.history.length).toBe(before)
})
it('drops the param when passed null', () => {
setDesignIdInUrl('design-42')
setDesignIdInUrl(null)
expect(getDesignIdFromUrl()).toBeNull()
expect(window.location.search).toBe('')
})
it('preserves other query params when updating design', () => {
window.history.replaceState(null, '', '/?key=secret')
setDesignIdInUrl('design-42')
const params = new URLSearchParams(window.location.search)
expect(params.get('key')).toBe('secret')
expect(params.get('design')).toBe('design-42')
})
it('round-trips: written id is read back', () => {
setDesignIdInUrl('xyz')
expect(getDesignIdFromUrl()).toBe('xyz')
})
})
+22
View File
@@ -0,0 +1,22 @@
// Sync the active design id with the browser URL (`?design=<id>`), so the
// current design survives a page refresh and can be shared/opened via URL.
// When no `design` param is present, callers fall back to the default design.
const DESIGN_PARAM = 'design'
/** Read the design id from the current URL, or null when absent. */
export function getDesignIdFromUrl(): string | null {
if (typeof window === 'undefined') return null
return new URLSearchParams(window.location.search).get(DESIGN_PARAM)
}
/**
* Reflect the active design id into the URL without adding a history entry
* (replaceState). Pass null to drop the param (back to the default design).
*/
export function setDesignIdInUrl(id: string | null): void {
if (typeof window === 'undefined') return
const url = new URL(window.location.href)
if (id) url.searchParams.set(DESIGN_PARAM, id)
else url.searchParams.delete(DESIGN_PARAM)
window.history.replaceState(window.history.state, '', url)
}