fix(workspace): reset doc state on navigation (bbox / analysis state leak)

Quand l'utilisateur naviguait d'un doc à un autre via /docs/:id, Vue
réutilisait l'instance du composant — onMounted ne se redéclenchait pas,
laissant l'analyse précédente (et donc les bbox du StructureViewer)
visibles tant que le nouveau fetch n'était pas terminé.

- DocWorkspacePage: watch props.id pour recharger le doc + :key sur les
  onglets (forçe un remount propre, reset l'état interne du
  StructureViewer comme selectedPage/hiddenTypes).
- DocInspectTab / DocAskTab: watch props.docId avec immediate:true,
  reset l'état au début du load + guard de race condition (ignore les
  réponses pour un docId obsolète).
This commit is contained in:
Pier-Jean Malandrino 2026-04-30 15:30:23 +02:00
parent ed50756ccd
commit 3c80d8baf0
3 changed files with 36 additions and 12 deletions

View file

@ -40,7 +40,7 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ref, computed, watch } from 'vue'
import { RouterLink } from 'vue-router'
import type { Analysis, Page } from '../shared/types'
import { fetchDocumentAnalyses } from '../features/analysis/api'
@ -73,13 +73,18 @@ const visitedBySelfRef = computed<Map<string, number>>(() => new Map())
async function loadAnalysis(): Promise<void> {
loading.value = true
error.value = null
analysis.value = null
focusedSelfRef.value = null
const requestedId = props.docId
try {
const analyses = await fetchDocumentAnalyses(props.docId)
const analyses = await fetchDocumentAnalyses(requestedId)
if (requestedId !== props.docId) return
analysis.value = analyses.find((a) => a.status === 'COMPLETED') ?? null
} catch (e) {
if (requestedId !== props.docId) return
error.value = (e as Error).message || 'Failed to load analysis'
} finally {
loading.value = false
if (requestedId === props.docId) loading.value = false
}
}
@ -91,7 +96,7 @@ function onSectionFocus(sectionRef: string): void {
}
}
onMounted(loadAnalysis)
watch(() => props.docId, loadAnalysis, { immediate: true })
</script>
<style scoped>

View file

@ -26,7 +26,7 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, watch } from 'vue'
import { RouterLink } from 'vue-router'
import type { Analysis } from '../shared/types'
import { fetchDocumentAnalyses } from '../features/analysis/api'
@ -45,17 +45,21 @@ const analysis = ref<Analysis | null>(null)
async function load(): Promise<void> {
loading.value = true
error.value = null
analysis.value = null
const requestedId = props.docId
try {
const analyses = await fetchDocumentAnalyses(props.docId)
const analyses = await fetchDocumentAnalyses(requestedId)
if (requestedId !== props.docId) return
analysis.value = analyses.find((a) => a.status === 'COMPLETED') ?? null
} catch (e) {
if (requestedId !== props.docId) return
error.value = (e as Error).message || 'Failed to load analysis'
} finally {
loading.value = false
if (requestedId === props.docId) loading.value = false
}
}
onMounted(load)
watch(() => props.docId, load, { immediate: true })
</script>
<style scoped>

View file

@ -35,16 +35,19 @@
</div>
<!-- Tab content lazy loaded (#216) -->
<!-- :key on docId forces a clean remount when navigating to a different doc,
preventing stale state (bbox, selectedPage, etc.) from leaking. -->
<div class="tab-content" role="tabpanel" data-e2e="tab-content">
<Suspense>
<DocChunksTab
v-if="activeMode === 'chunks'"
:key="id"
:doc-id="id"
:available-stores="doc.stores ?? []"
:store-links="doc.storeLinks"
/>
<DocInspectTab v-else-if="activeMode === 'inspect'" :doc-id="id" />
<DocAskTab v-else-if="activeMode === 'ask'" :doc-id="id" />
<DocInspectTab v-else-if="activeMode === 'inspect'" :key="id" :doc-id="id" />
<DocAskTab v-else-if="activeMode === 'ask'" :key="id" :doc-id="id" />
</Suspense>
</div>
</template>
@ -106,12 +109,17 @@ function switchMode(m: DocMode): void {
async function loadDoc(): Promise<void> {
loadingDoc.value = true
docError.value = null
doc.value = null
const requestedId = props.id
try {
doc.value = await fetchDocument(props.id)
const fetched = await fetchDocument(requestedId)
if (requestedId !== props.id) return
doc.value = fetched
} catch (e) {
if (requestedId !== props.id) return
docError.value = (e as Error).message || 'Failed to load document'
} finally {
loadingDoc.value = false
if (requestedId === props.id) loadingDoc.value = false
}
}
@ -140,6 +148,13 @@ watch(
if (resolved) activeMode.value = resolved
},
)
watch(
() => props.id,
(newId, oldId) => {
if (newId !== oldId) loadDoc()
},
)
</script>
<style scoped>