mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)"
|
||||
>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user