Merge pull request #263 from Pouzor/feat/design-url-sync
feat: sync active design to URL for refresh/share
This commit is contained in:
+14
-2
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user