Compare commits

..

3 Commits

Author SHA1 Message Date
Subagent 5
f20b19a814 fix: prevent keybinding warning flash by checking dialog visibility and command ownership
Amp-Thread-ID: https://ampcode.com/threads/T-019c07ff-3277-70f9-a664-d2ebd3d5228f
Co-authored-by: Amp <amp@ampcode.com>
2026-01-28 21:33:19 -08:00
Subagent 5
329e7c5caf fix: prevent keybinding warning flash on save
- Close dialog before updating store to prevent computed re-evaluation during exit animation
- Refactor watchEffect to watch for focus logic
- Add comprehensive tests for save behavior

Amp-Thread-ID: https://ampcode.com/threads/T-019c07cd-b968-73bd-9b53-e1bdaf7f51dc
Co-authored-by: Amp <amp@ampcode.com>
2026-01-28 20:12:56 -08:00
Christian Byrne
2103dcc788 fix: increase Vue node resize handle size for better usability (#8391)
## Summary

Increases the resize handle size on Vue nodes to improve usability,
especially when nodes are selected.

## Changes

- **What**: Increased resize handle from 12px to 20px and offset it
slightly outside the node boundary to avoid overlap with selection
outline

## Review Focus

The resize handle was too small and became harder to grab when the node
was selected (the 2px outline rendered outside the box, visually
obscuring the corner). This fix increases the hit area and positions it
to extend beyond the node edge.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8391-fix-increase-Vue-node-resize-handle-size-for-better-usability-2f76d73d36508136b2aac51bc0d53551)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: GitHub Action <action@github.com>
2026-01-28 19:15:40 -08:00
3 changed files with 32 additions and 121 deletions

View File

@@ -145,7 +145,7 @@ import InputText from 'primevue/inputtext'
import Message from 'primevue/message'
import Tag from 'primevue/tag'
import { useToast } from 'primevue/usetoast'
import { computed, ref, watchEffect } from 'vue'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import SearchBox from '@/components/common/SearchBox.vue'
@@ -194,27 +194,23 @@ const selectedCommandData = ref<ICommandData | null>(null)
const editDialogVisible = ref(false)
const newBindingKeyCombo = ref<KeyComboImpl | null>(null)
const currentEditingCommand = ref<ICommandData | null>(null)
const keybindingInput = ref<InstanceType<typeof InputText> | null>(null)
const keybindingInput = ref()
const existingKeybindingOnCombo = computed<KeybindingImpl | null>(() => {
if (!currentEditingCommand.value) {
return null
}
// If the new keybinding is the same as the current editing command, then don't show the error
if (
currentEditingCommand.value.keybinding?.combo?.equals(
newBindingKeyCombo.value
)
!editDialogVisible.value ||
!currentEditingCommand.value ||
!newBindingKeyCombo.value
) {
return null
}
if (!newBindingKeyCombo.value) {
const existing = keybindingStore.getKeybinding(newBindingKeyCombo.value)
if (!existing || existing.commandId === currentEditingCommand.value.id) {
return null
}
return keybindingStore.getKeybinding(newBindingKeyCombo.value)
return existing
})
function editKeybinding(commandData: ICommandData) {
@@ -225,11 +221,9 @@ function editKeybinding(commandData: ICommandData) {
editDialogVisible.value = true
}
watchEffect(() => {
if (editDialogVisible.value) {
// nextTick doesn't work here, so we use a timeout instead
watch(editDialogVisible, (visible) => {
if (visible) {
setTimeout(() => {
// @ts-expect-error - $el is an internal property of the InputText component
keybindingInput.value?.$el?.focus()
}, 300)
}
@@ -265,18 +259,20 @@ function cancelEdit() {
}
async function saveKeybinding() {
if (currentEditingCommand.value && newBindingKeyCombo.value) {
const updated = keybindingStore.updateKeybindingOnCommand(
new KeybindingImpl({
commandId: currentEditingCommand.value.id,
combo: newBindingKeyCombo.value
})
)
if (updated) {
await keybindingService.persistUserKeybindings()
}
}
if (!currentEditingCommand.value || !newBindingKeyCombo.value) return
const updated = keybindingStore.updateKeybindingOnCommand(
new KeybindingImpl({
commandId: currentEditingCommand.value.id,
combo: newBindingKeyCombo.value
})
)
cancelEdit()
if (updated) {
await keybindingService.persistUserKeybindings()
}
}
async function resetKeybinding(commandData: ICommandData) {

View File

@@ -10,17 +10,8 @@ import { NodeSlot } from '@/lib/litegraph/src/node/NodeSlot'
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
import type {
IBaseWidget,
IWidgetAssetOptions,
TWidgetValue
} from '@/lib/litegraph/src/types/widgets'
import { useAssetBrowserDialog } from '@/platform/assets/composables/useAssetBrowserDialog'
import {
assetFilenameSchema,
assetItemSchema
} from '@/platform/assets/schemas/assetSchema'
import { assetService } from '@/platform/assets/services/assetService'
import { getAssetFilename } from '@/platform/assets/utils/assetMetadataUtils'
import { isCloud } from '@/platform/distribution/types'
import type { InputSpec } from '@/schemas/nodeDefSchema'
import { app } from '@/scripts/app'
import {
@@ -28,10 +19,10 @@ import {
addValueControlWidgets,
isValidWidgetType
} from '@/scripts/widgets'
import { isPrimitiveNode } from '@/renderer/utils/nodeTypeGuards'
import { CONFIG, GET_CONFIG } from '@/services/litegraphService'
import { mergeInputSpec } from '@/utils/nodeDefUtil'
import { applyTextReplacements } from '@/utils/searchAndReplace'
import { isPrimitiveNode } from '@/renderer/utils/nodeTypeGuards'
const replacePropertyName = 'Run widget replace on values'
export class PrimitiveNode extends LGraphNode {
@@ -237,20 +228,6 @@ export class PrimitiveNode extends LGraphNode {
// Store current size as addWidget resizes the node
const [oldWidth, oldHeight] = this.size
let widget: IBaseWidget
// Cloud: Use asset widget for model-eligible inputs
if (isCloud && type === 'COMBO') {
const isEligible = assetService.isAssetBrowserEligible(
node.comfyClass,
widgetName
)
if (isEligible) {
widget = this.#createAssetWidget(node, widgetName, inputData)
this.#finalizeWidget(widget, oldWidth, oldHeight, recreating)
return
}
}
if (isValidWidgetType(type)) {
widget = (ComfyWidgets[type](this, 'value', inputData, app) || {}).widget
} else {
@@ -300,84 +277,20 @@ export class PrimitiveNode extends LGraphNode {
}
}
this.#finalizeWidget(widget, oldWidth, oldHeight, recreating)
}
#createAssetWidget(
targetNode: LGraphNode,
widgetName: string,
inputData: InputSpec
): IBaseWidget {
const defaultValue = inputData[1]?.default as string | undefined
const assetBrowserDialog = useAssetBrowserDialog()
const openModal = async (widget: IBaseWidget) => {
await assetBrowserDialog.show({
nodeType: targetNode.comfyClass ?? '',
inputName: widgetName,
currentValue: widget.value as string,
onAssetSelected: (asset) => {
const validatedAsset = assetItemSchema.safeParse(asset)
if (!validatedAsset.success) {
console.error('Invalid asset item:', validatedAsset.error.errors)
return
}
const filename = getAssetFilename(validatedAsset.data)
const validatedFilename = assetFilenameSchema.safeParse(filename)
if (!validatedFilename.success) {
console.error(
'Invalid asset filename:',
validatedFilename.error.errors
)
return
}
const oldValue = widget.value
widget.value = validatedFilename.data
widget.callback?.(
widget.value,
app.canvas,
this,
app.canvas.graph_mouse,
{} as CanvasPointerEvent
)
this.onWidgetChanged?.(
widget.name,
validatedFilename.data,
oldValue,
widget
)
}
})
}
const options: IWidgetAssetOptions = { openModal }
return this.addWidget(
'asset',
'value',
defaultValue ?? '',
() => {},
options
)
}
#finalizeWidget(
widget: IBaseWidget,
oldWidth: number,
oldHeight: number,
recreating: boolean
) {
// When our value changes, update other widgets to reflect our changes
// e.g. so LoadImage shows correct image
widget.callback = useChainCallback(widget.callback, () => {
this.applyToGraph()
})
// Use the biggest dimensions in case the widgets caused the node to grow
this.setSize([
Math.max(this.size[0], oldWidth),
Math.max(this.size[1], oldHeight)
])
if (!recreating) {
// Grow our node more if required
const sz = this.computeSize()
if (this.size[0] < sz[0]) {
this.size[0] = sz[0]

View File

@@ -150,7 +150,9 @@
v-if="!isCollapsed && nodeData.resizable !== false"
role="button"
:aria-label="t('g.resizeFromBottomRight')"
:class="cn(baseResizeHandleClasses, 'right-0 bottom-0 cursor-se-resize')"
:class="
cn(baseResizeHandleClasses, '-right-1 -bottom-1 cursor-se-resize')
"
@pointerdown.stop="handleResizePointerDown"
/>
</div>
@@ -344,7 +346,7 @@ function initSizeStyles() {
}
const baseResizeHandleClasses =
'absolute h-3 w-3 opacity-0 pointer-events-auto focus-visible:outline focus-visible:outline-2 focus-visible:outline-white/40'
'absolute h-5 w-5 opacity-0 pointer-events-auto focus-visible:outline focus-visible:outline-2 focus-visible:outline-white/40'
const MIN_NODE_WIDTH = 225