fix: route unassigned widgets to default zone and fix CI test failures

Widgets without explicit zone assignments now fall back to the first
non-output zone, fixing empty app mode when no builder layout is saved.
Also fixes CodeRabbit findings: aria-label, splice index, pointercancel,
duplicate i18n key, and subgraph widget matching.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Koshi
2026-03-21 00:22:24 +01:00
parent 6fb8004829
commit 2eb2514d25
8 changed files with 64 additions and 31 deletions

View File

@@ -85,11 +85,13 @@ test.describe('App mode dropdown clipping', { tag: '@ui' }, () => {
timeout: 5000
})
// Scroll to bottom so the codec widget is at the clipping edge
// Scroll to bottom so the codec widget is at the clipping edge.
// In the zone layout, overflow-y-auto is on the inner zone div.
const widgetList = comfyPage.appMode.linearWidgets
await widgetList.evaluate((el) =>
el.scrollTo({ top: el.scrollHeight, behavior: 'instant' })
)
await widgetList.evaluate((el) => {
const scrollable = el.querySelector('[class*="overflow-y"]') ?? el
scrollable.scrollTo({ top: scrollable.scrollHeight, behavior: 'instant' })
})
// Click the codec select (combobox role with aria-label from WidgetSelectDefault)
const codecSelect = widgetList.getByRole('combobox', { name: 'codec' })
@@ -129,11 +131,13 @@ test.describe('App mode dropdown clipping', { tag: '@ui' }, () => {
timeout: 5000
})
// Scroll to bottom so the image widget is at the clipping edge
// Scroll to bottom so the image widget is at the clipping edge.
// In the zone layout, overflow-y-auto is on the inner zone div.
const widgetList = comfyPage.appMode.linearWidgets
await widgetList.evaluate((el) =>
el.scrollTo({ top: el.scrollHeight, behavior: 'instant' })
)
await widgetList.evaluate((el) => {
const scrollable = el.querySelector('[class*="overflow-y"]') ?? el
scrollable.scrollTo({ top: scrollable.scrollHeight, behavior: 'instant' })
})
// Click the FormDropdown trigger button for the image widget.
// The button emits 'select-click' which toggles the Popover.

View File

@@ -167,8 +167,7 @@ function nodeToNodeData(node: LGraphNode) {
icon: 'icon-[lucide--x]',
command: () => {
const idx = appModeStore.selectedInputs.findIndex(
([nId, wName]) =>
nId === action.node.id && wName === action.widget.name
([nId, wName]) => `${nId}:${wName}` === key
)
if (idx !== -1) appModeStore.selectedInputs.splice(idx, 1)
}

View File

@@ -187,6 +187,12 @@ function getOrderedItems(zoneId: string) {
? t('linearMode.arrange.alignToBottom')
: t('linearMode.arrange.alignToTop')
"
type="button"
:aria-label="
(appModeStore.zoneAlign[zone.id] ?? 'top') === 'top'
? t('linearMode.arrange.alignToBottom')
: t('linearMode.arrange.alignToTop')
"
class="flex size-5 cursor-pointer items-center justify-center border-0 bg-transparent p-0"
@click="appModeStore.toggleZoneAlign(zone.id)"
>

View File

@@ -65,11 +65,19 @@ function onPointerDown(e: PointerEvent) {
emit('resizeEnd', latestFractions)
}
function onPointerCancel() {
dragging.value = false
cleanupDrag?.()
cleanupDrag = null
}
el.addEventListener('pointermove', onPointerMove)
el.addEventListener('pointerup', onPointerUp)
el.addEventListener('pointercancel', onPointerCancel)
cleanupDrag = () => {
el.removeEventListener('pointermove', onPointerMove)
el.removeEventListener('pointerup', onPointerUp)
el.removeEventListener('pointercancel', onPointerCancel)
}
}
</script>
@@ -78,7 +86,7 @@ function onPointerDown(e: PointerEvent) {
<div
:class="
cn(
'absolute z-10 flex transition-opacity hover:opacity-100',
'absolute z-10 flex touch-none transition-opacity hover:opacity-100',
dragging ? 'opacity-100' : 'opacity-30',
direction === 'column'
? 'top-0 h-full w-4 -translate-x-1/2 cursor-col-resize justify-center'

View File

@@ -75,7 +75,8 @@ export const vZoneReorderDropTarget: Directive<HTMLElement, string> = {
const newOrder = [...order]
newOrder.splice(fromIdx, 1)
newOrder.splice(toIdx, 0, data.zoneId)
const insertIdx = fromIdx < toIdx ? toIdx - 1 : toIdx
newOrder.splice(insertIdx, 0, data.zoneId)
appModeStore.setGridOverrides({
...appModeStore.gridOverrides,

View File

@@ -77,6 +77,23 @@ describe('useZoneWidgets', () => {
expect(result).toEqual([[1, 'prompt']])
})
it('routes unassigned inputs to defaultZoneId when provided', () => {
const getZone = makeGetZone({ '1:prompt': 'z1' })
const z1 = inputsForZone(inputs, getZone, 'z1', 'z1')
const z2 = inputsForZone(inputs, getZone, 'z2', 'z1')
// 1:prompt is explicitly z1; unassigned ones also go to z1 (default)
expect(z1).toEqual([
[1, 'prompt'],
[2, 'width'],
[1, 'steps'],
[3, 'seed']
])
// z2 gets nothing since unassigned defaults to z1
expect(z2).toEqual([])
})
it('filters non-contiguous inputs for the same node across zones', () => {
const getZone = makeGetZone({
'1:prompt': 'z1',

View File

@@ -37,11 +37,14 @@ export interface EnrichedNodeData extends VueNodeData {
export function inputsForZone(
selectedInputs: [NodeId, string][],
getZone: (nodeId: NodeId, widgetName: string) => string | undefined,
zoneId: string
zoneId: string,
defaultZoneId?: string
): [NodeId, string][] {
return selectedInputs.filter(
([nodeId, widgetName]) => getZone(nodeId, widgetName) === zoneId
)
return selectedInputs.filter(([nodeId, widgetName]) => {
const assigned = getZone(nodeId, widgetName)
if (assigned) return assigned === zoneId
return defaultZoneId ? zoneId === defaultZoneId : false
})
}
/**
@@ -57,12 +60,16 @@ export function useArrangeZoneWidgets() {
return computed(() => {
const map = new Map<string, ResolvedArrangeWidget[]>()
const defaultZoneId =
template.value.zones.find((z) => !z.isOutput)?.id ??
template.value.zones[0]?.id
for (const zone of template.value.zones) {
const inputs = inputsForZone(
appModeStore.selectedInputs,
appModeStore.getZone,
zone.id
zone.id,
defaultZoneId
)
const resolved = inputs
.map(([nodeId, widgetName]) => {
@@ -102,12 +109,16 @@ export function useAppZoneWidgets() {
return computed(() => {
const map = new Map<string, EnrichedNodeData[]>()
const defaultZoneId =
template.value.zones.find((z) => !z.isOutput)?.id ??
template.value.zones[0]?.id
for (const zone of template.value.zones) {
const inputs = inputsForZone(
appModeStore.selectedInputs,
appModeStore.getZone,
zone.id
zone.id,
defaultZoneId
)
map.set(
zone.id,

View File

@@ -3337,19 +3337,6 @@
"sidebar": "Sidebar"
}
},
"error": {
"header": "Something went wrong",
"requiresGraph": "This error needs to be resolved in the node graph.",
"promptVisitGraph": "Switch to graph mode to inspect and fix the issue.",
"mobileFixable": "Some inputs need attention. Go to {0} to fix them.",
"promptShow": "Show error details",
"getHelp": "Need help? Check the {0}, report on {1}, or ask for {2}.",
"guide": "troubleshooting guide",
"github": "GitHub",
"support": "support",
"log": "Error log",
"goto": "Go to errors"
},
"builder": {
"title": "App builder mode",
"exit": "Exit builder",