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 ed2f47fdc8
commit 53d28c2282
3 changed files with 36 additions and 12 deletions

View file

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

View file

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

View file

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