This commit is contained in:
2026-03-27 23:27:03 -03:00
parent 1c6af767eb
commit 886720c3ce
3 changed files with 74 additions and 58 deletions

View File

@@ -16,8 +16,10 @@ import { usePipelineStore } from './stores/pipeline'
import { useSSEConnection } from './composables/useSSEConnection' import { useSSEConnection } from './composables/useSSEConnection'
import { useCheckpointLoader } from './composables/useCheckpointLoader' import { useCheckpointLoader } from './composables/useCheckpointLoader'
import { useEditorState } from './composables/useEditorState' import { useEditorState } from './composables/useEditorState'
import { useHashRouter } from './composables/useHashRouter'
const pipeline = usePipelineStore() const pipeline = usePipelineStore()
useHashRouter()
const logPanel = ref<{ clear: () => void } | null>(null) const logPanel = ref<{ clear: () => void } | null>(null)
// SSE connection + pipeline status // SSE connection + pipeline status

View File

@@ -0,0 +1,70 @@
import { watch, onUnmounted } from 'vue'
import { usePipelineStore } from '../stores/pipeline'
/**
* Bidirectional sync between URL hash and pipeline store layout state.
*
* Hash format:
* #/ → normal dashboard
* #/editor/<stage> → bbox/region editor for that stage
* #/config/<stage> → stage config editor
* #/source → source selector
*
* Call once in App.vue. Handles popstate (back/forward) and
* updates the hash when store state changes.
*/
export function useHashRouter() {
const pipeline = usePipelineStore()
function parseHash(hash: string) {
const path = hash.replace(/^#\/?/, '')
if (!path || path === 'dashboard') {
return { mode: 'normal', stage: null as string | null }
}
if (path === 'source') {
return { mode: 'source_selector', stage: null as string | null }
}
const editorMatch = path.match(/^editor\/(.+)$/)
if (editorMatch) {
return { mode: 'bbox_editor', stage: editorMatch[1] }
}
const configMatch = path.match(/^config\/(.+)$/)
if (configMatch) {
return { mode: 'stage_editor', stage: configMatch[1] }
}
return { mode: 'normal', stage: null as string | null }
}
function applyHash() {
const { mode, stage } = parseHash(window.location.hash)
pipeline.layoutMode = mode
pipeline.editorStage = stage
}
function updateHash() {
let hash = '#/'
if (pipeline.layoutMode === 'bbox_editor' && pipeline.editorStage) {
hash = `#/editor/${pipeline.editorStage}`
} else if (pipeline.layoutMode === 'stage_editor' && pipeline.editorStage) {
hash = `#/config/${pipeline.editorStage}`
} else if (pipeline.layoutMode === 'source_selector') {
hash = '#/source'
}
if (window.location.hash !== hash) {
window.history.pushState(null, '', hash)
}
}
// Sync hash → state on load
applyHash()
// Sync hash → state on back/forward
window.addEventListener('popstate', applyHash)
onUnmounted(() => window.removeEventListener('popstate', applyHash))
// Sync state → hash when layout changes
watch(
() => [pipeline.layoutMode, pipeline.editorStage],
updateHash,
)
}

View File

@@ -1,17 +1,12 @@
/** /**
* Pipeline store — run state, transport controls, checkpoint status. * Pipeline store — run state, transport controls, checkpoint status.
* *
* Layout is driven by URL hash:
* #/ → normal dashboard
* #/editor/<stage> → bbox/region editor for that stage
* #/config/<stage> → stage config editor
* #/source → source selector
*
* State shape defined in types/store-state.ts. * State shape defined in types/store-state.ts.
* Hash routing is handled by useHashRouter composable, not here.
*/ */
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref, computed, watch } from 'vue' import { ref, computed } from 'vue'
import type { NodeState } from '../types/store-state' import type { NodeState } from '../types/store-state'
import type { CheckpointInfo } from '../types/sse-contract' import type { CheckpointInfo } from '../types/sse-contract'
@@ -26,7 +21,6 @@ export const usePipelineStore = defineStore('pipeline', () => {
const checkpoints = ref<CheckpointInfo[]>([]) const checkpoints = ref<CheckpointInfo[]>([])
const error = ref<string | null>(null) const error = ref<string | null>(null)
// Layout mode — synced with URL hash
const layoutMode = ref<string>('normal') const layoutMode = ref<string>('normal')
const editorStage = ref<string | null>(null) const editorStage = ref<string | null>(null)
@@ -35,56 +29,6 @@ export const usePipelineStore = defineStore('pipeline', () => {
const canReplay = computed(() => checkpoints.value.length > 0) const canReplay = computed(() => checkpoints.value.length > 0)
const isEditing = computed(() => layoutMode.value !== 'normal') const isEditing = computed(() => layoutMode.value !== 'normal')
// --- Hash routing ---
function parseHash(hash: string) {
const path = hash.replace(/^#\/?/, '')
if (!path || path === 'dashboard') {
return { mode: 'normal', stage: null }
}
if (path === 'source') {
return { mode: 'source_selector', stage: null }
}
const editorMatch = path.match(/^editor\/(.+)$/)
if (editorMatch) {
return { mode: 'bbox_editor', stage: editorMatch[1] }
}
const configMatch = path.match(/^config\/(.+)$/)
if (configMatch) {
return { mode: 'stage_editor', stage: configMatch[1] }
}
return { mode: 'normal', stage: null }
}
function applyHash() {
const { mode, stage } = parseHash(window.location.hash)
layoutMode.value = mode
editorStage.value = stage
}
function updateHash() {
let hash = '#/'
if (layoutMode.value === 'bbox_editor' && editorStage.value) {
hash = `#/editor/${editorStage.value}`
} else if (layoutMode.value === 'stage_editor' && editorStage.value) {
hash = `#/config/${editorStage.value}`
} else if (layoutMode.value === 'source_selector') {
hash = '#/source'
}
if (window.location.hash !== hash) {
window.history.pushState(null, '', hash)
}
}
// Sync hash → state on load and popstate (back/forward)
applyHash()
window.addEventListener('popstate', applyHash)
// Sync state → hash when layout changes
watch([layoutMode, editorStage], updateHash)
// --- Actions ---
function setJob(id: string) { function setJob(id: string) {
jobId.value = id jobId.value = id
} }