fix: app mode widgets disappear after hard refresh (#9621)

## Summary

Fix all app mode widgets (including seed) disappearing after hard
refresh due to a race condition in `pruneLinearData` and a missing
reactivity dependency in `mappedSelections`.

## Changes

- **What**: Guard `pruneLinearData` with `!ChangeTracker.isLoadingGraph`
so inputs are preserved while `rootGraph.configure()` hasn't populated
nodes yet. Add `graphNodes` dependency to `mappedSelections` computed in
`LinearControls.vue` so it re-evaluates when the graph finishes
configuring.

## Review Focus

The core fix is a one-line guard change: `app.rootGraph &&
!ChangeTracker.isLoadingGraph` instead of just `app.rootGraph`. The
previous guard failed because `rootGraph` exists as an empty graph
during loading — `resolveNode()` returns `undefined` for all nodes and
everything gets filtered out.

Fixes COM-16193

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9621-fix-app-mode-widgets-disappear-after-hard-refresh-31d6d73d3650811193f5e1bc8f3c15c8)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
Christian Byrne
2026-03-11 23:54:27 -07:00
committed by GitHub
parent f1fc5fa9b3
commit 4c00d39ade
3 changed files with 32 additions and 1 deletions

View File

@@ -64,6 +64,7 @@ useEventListener(
)
const mappedSelections = computed(() => {
void graphNodes.value
let unprocessedInputs = appModeStore.selectedInputs.flatMap(
([nodeId, widgetName]) => {
const [node, widget] = resolveNodeWidget(nodeId, widgetName)

View File

@@ -25,7 +25,7 @@ const mockEmptyWorkflowDialog = vi.hoisted(() => {
vi.mock('@/scripts/app', () => ({
app: {
rootGraph: { extra: {}, nodes: [{ id: 1 }] }
rootGraph: { extra: {}, nodes: [{ id: 1 }], events: new EventTarget() }
}
}))
@@ -242,6 +242,29 @@ describe('appModeStore', () => {
expect(store.selectedOutputs).toEqual([1])
})
it('reloads selections on configured event', async () => {
const node1 = mockNode(1)
// Initially nodes are not resolvable — pruning removes them
mockResolveNode.mockReturnValue(undefined)
workflowStore.activeWorkflow = workflowWithLinearData([[1, 'seed']], [1])
await nextTick()
expect(store.selectedInputs).toEqual([])
expect(store.selectedOutputs).toEqual([])
// After graph configures, nodes become resolvable
mockResolveNode.mockImplementation((id) =>
id == 1 ? (node1 as unknown as LGraphNode) : undefined
)
;(app.rootGraph.events as EventTarget).dispatchEvent(
new Event('configured')
)
await nextTick()
expect(store.selectedInputs).toEqual([[1, 'seed']])
expect(store.selectedOutputs).toEqual([1])
})
it('hasOutputs is false when all output nodes are deleted', async () => {
mockResolveNode.mockReturnValue(undefined)

View File

@@ -1,5 +1,6 @@
import { defineStore } from 'pinia'
import { reactive, computed, watch } from 'vue'
import { useEventListener } from '@vueuse/core'
import { useEmptyWorkflowDialog } from '@/components/builder/useEmptyWorkflowDialog'
import { useAppMode } from '@/composables/useAppMode'
@@ -76,6 +77,12 @@ export const useAppModeStore = defineStore('appMode', () => {
{ immediate: true }
)
useEventListener(
() => app.rootGraph?.events,
'configured',
resetSelectedToWorkflow
)
watch(
() =>
isBuilderMode.value