mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-03 14:54:37 +00:00
Address remaining PR feedback: type safety and robustness improvements
This commit is contained in:
@@ -82,7 +82,7 @@ watch(
|
||||
: inputValue.value
|
||||
const start = 0
|
||||
const end = fileName.length
|
||||
el.setSelectionRange?.(start, end)
|
||||
el.setSelectionRange(start, end)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
@@ -75,7 +75,11 @@ function getFormAttrs(item: FormItem) {
|
||||
: item.options
|
||||
attrs['options'] = options
|
||||
|
||||
if (options && typeof options[0] !== 'string') {
|
||||
if (
|
||||
Array.isArray(options) &&
|
||||
options.length > 0 &&
|
||||
typeof options[0] !== 'string'
|
||||
) {
|
||||
attrs['optionLabel'] = 'text'
|
||||
attrs['optionValue'] = 'value'
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ describe('TreeExplorerTreeNode', () => {
|
||||
expect(wrapper.findComponent(EditableText).props('modelValue')).toBe(
|
||||
'Test Node'
|
||||
)
|
||||
const badgeValue = wrapper.findComponent(Badge).props()['value']
|
||||
const badgeValue = wrapper.findComponent(Badge).props('value')
|
||||
expect(String(badgeValue)).toBe('3')
|
||||
})
|
||||
|
||||
|
||||
@@ -76,7 +76,9 @@ describe('MissingCoreNodesMessage', () => {
|
||||
vi.clearAllMocks()
|
||||
mockSystemStatsStore = createMockSystemStatsStore()
|
||||
vi.mocked(useSystemStatsStore).mockReturnValue(
|
||||
mockSystemStatsStore as unknown as ReturnType<typeof useSystemStatsStore>
|
||||
mockSystemStatsStore as Partial<
|
||||
ReturnType<typeof useSystemStatsStore>
|
||||
> as ReturnType<typeof useSystemStatsStore>
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -428,7 +428,12 @@ onMounted(async () => {
|
||||
|
||||
await newUserService().initializeIfNewUser(settingStore)
|
||||
|
||||
if (!canvasRef.value) return
|
||||
if (!canvasRef.value) {
|
||||
console.error(
|
||||
'[GraphCanvas] canvasRef.value is null during onMounted - comfyApp.setup was skipped'
|
||||
)
|
||||
return
|
||||
}
|
||||
await comfyApp.setup(canvasRef.value)
|
||||
canvasStore.canvas = comfyApp.canvas
|
||||
canvasStore.canvas.render_canvas_border = false
|
||||
|
||||
@@ -63,5 +63,13 @@ const closePopover = () => {
|
||||
popover.value?.hide()
|
||||
}
|
||||
|
||||
defineExpose({ popover, closePopover })
|
||||
const openPopover = (event: Event) => {
|
||||
popover.value?.show(event)
|
||||
}
|
||||
|
||||
const togglePopover = (event: Event) => {
|
||||
popover.value?.toggle(event)
|
||||
}
|
||||
|
||||
defineExpose({ closePopover, openPopover, togglePopover })
|
||||
</script>
|
||||
|
||||
@@ -36,54 +36,76 @@ interface MockStore {
|
||||
maskOpacity: number
|
||||
}
|
||||
|
||||
function getImgCanvas(): MockCanvas {
|
||||
if (!mockStore.imgCanvas) throw new Error('imgCanvas not initialized')
|
||||
return mockStore.imgCanvas
|
||||
}
|
||||
const {
|
||||
mockStore,
|
||||
getImgCanvas,
|
||||
getMaskCanvas,
|
||||
getRgbCanvas,
|
||||
getImgCtx,
|
||||
getMaskCtx,
|
||||
getRgbCtx,
|
||||
getCanvasBackground
|
||||
} = vi.hoisted(() => {
|
||||
const mockStore: MockStore = {
|
||||
imgCanvas: null,
|
||||
maskCanvas: null,
|
||||
rgbCanvas: null,
|
||||
imgCtx: null,
|
||||
maskCtx: null,
|
||||
rgbCtx: null,
|
||||
canvasBackground: null,
|
||||
maskColor: { r: 0, g: 0, b: 0 },
|
||||
maskBlendMode: 'black' as MaskBlendMode,
|
||||
maskOpacity: 0.8
|
||||
}
|
||||
|
||||
function getMaskCanvas(): MockCanvas {
|
||||
if (!mockStore.maskCanvas) throw new Error('maskCanvas not initialized')
|
||||
return mockStore.maskCanvas
|
||||
}
|
||||
function getImgCanvas(): MockCanvas {
|
||||
if (!mockStore.imgCanvas) throw new Error('imgCanvas not initialized')
|
||||
return mockStore.imgCanvas
|
||||
}
|
||||
|
||||
function getRgbCanvas(): MockCanvas {
|
||||
if (!mockStore.rgbCanvas) throw new Error('rgbCanvas not initialized')
|
||||
return mockStore.rgbCanvas
|
||||
}
|
||||
function getMaskCanvas(): MockCanvas {
|
||||
if (!mockStore.maskCanvas) throw new Error('maskCanvas not initialized')
|
||||
return mockStore.maskCanvas
|
||||
}
|
||||
|
||||
function getImgCtx(): MockContext {
|
||||
if (!mockStore.imgCtx) throw new Error('imgCtx not initialized')
|
||||
return mockStore.imgCtx
|
||||
}
|
||||
function getRgbCanvas(): MockCanvas {
|
||||
if (!mockStore.rgbCanvas) throw new Error('rgbCanvas not initialized')
|
||||
return mockStore.rgbCanvas
|
||||
}
|
||||
|
||||
function getMaskCtx(): MockContext {
|
||||
if (!mockStore.maskCtx) throw new Error('maskCtx not initialized')
|
||||
return mockStore.maskCtx
|
||||
}
|
||||
function getImgCtx(): MockContext {
|
||||
if (!mockStore.imgCtx) throw new Error('imgCtx not initialized')
|
||||
return mockStore.imgCtx
|
||||
}
|
||||
|
||||
function getRgbCtx(): MockContext {
|
||||
if (!mockStore.rgbCtx) throw new Error('rgbCtx not initialized')
|
||||
return mockStore.rgbCtx
|
||||
}
|
||||
function getMaskCtx(): MockContext {
|
||||
if (!mockStore.maskCtx) throw new Error('maskCtx not initialized')
|
||||
return mockStore.maskCtx
|
||||
}
|
||||
|
||||
function getCanvasBackground(): { style: Partial<MockCanvasStyle> } {
|
||||
if (!mockStore.canvasBackground)
|
||||
throw new Error('canvasBackground not initialized')
|
||||
return mockStore.canvasBackground
|
||||
}
|
||||
function getRgbCtx(): MockContext {
|
||||
if (!mockStore.rgbCtx) throw new Error('rgbCtx not initialized')
|
||||
return mockStore.rgbCtx
|
||||
}
|
||||
|
||||
const mockStore: MockStore = {
|
||||
imgCanvas: null,
|
||||
maskCanvas: null,
|
||||
rgbCanvas: null,
|
||||
imgCtx: null,
|
||||
maskCtx: null,
|
||||
rgbCtx: null,
|
||||
canvasBackground: null,
|
||||
maskColor: { r: 0, g: 0, b: 0 },
|
||||
maskBlendMode: MaskBlendMode.Black,
|
||||
maskOpacity: 0.8
|
||||
}
|
||||
function getCanvasBackground(): { style: Partial<MockCanvasStyle> } {
|
||||
if (!mockStore.canvasBackground)
|
||||
throw new Error('canvasBackground not initialized')
|
||||
return mockStore.canvasBackground
|
||||
}
|
||||
|
||||
return {
|
||||
mockStore,
|
||||
getImgCanvas,
|
||||
getMaskCanvas,
|
||||
getRgbCanvas,
|
||||
getImgCtx,
|
||||
getMaskCtx,
|
||||
getRgbCtx,
|
||||
getCanvasBackground
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/stores/maskEditorStore', () => ({
|
||||
useMaskEditorStore: vi.fn(() => mockStore)
|
||||
|
||||
@@ -36,7 +36,10 @@ class FilteredContextMenu<TValue = unknown> extends ContextMenu<TValue> {
|
||||
|
||||
// We must request an animation frame for the current node of the active canvas to update.
|
||||
requestAnimationFrame(() => {
|
||||
const currentNode = LGraphCanvas.active_canvas.current_node
|
||||
const activeCanvas = LGraphCanvas.active_canvas
|
||||
if (!activeCanvas) return
|
||||
|
||||
const currentNode = activeCanvas.current_node
|
||||
const clickedComboValue = currentNode?.widgets
|
||||
?.filter(
|
||||
(w) =>
|
||||
|
||||
@@ -666,9 +666,12 @@ export class GroupNodeConfig {
|
||||
unknown,
|
||||
Record<string, unknown>
|
||||
]
|
||||
// Intentional casts: widget config types are dynamic at runtime and the
|
||||
// compiler cannot infer the union shapes expected by mergeIfValid.
|
||||
const output = { widget: primitiveConfig } as unknown as Parameters<
|
||||
typeof mergeIfValid
|
||||
>[0]
|
||||
// Intentional casts for targetWidget and primitiveConfig — see mergeIfValid.
|
||||
const config = mergeIfValid(
|
||||
output,
|
||||
targetWidget as Parameters<typeof mergeIfValid>[1],
|
||||
|
||||
@@ -51,7 +51,16 @@ export interface GroupNodeInputsSpec {
|
||||
export type GroupNodeOutputType = string | (string | number)[]
|
||||
|
||||
/**
|
||||
* Partial link info used internally by group node getInputLink override.
|
||||
* Extends ILinkRouting to be compatible with the base getInputLink return type.
|
||||
* Represents a partial or synthetic link used internally by group node's
|
||||
* `getInputLink` override when resolving connections through collapsed group nodes.
|
||||
*
|
||||
* Unlike a full `ILinkRouting`, this represents a computed/virtual link that may not
|
||||
* correspond to an actual link in the graph's link registry. It's constructed on-the-fly
|
||||
* to represent the logical connection path through group node boundaries.
|
||||
*
|
||||
* This type aliases `ILinkRouting` (rather than narrowing it) because the consuming code
|
||||
* expects the same shape for both real and synthetic links. The distinction is purely
|
||||
* semantic: callers should be aware that these links are transient and may not have
|
||||
* valid `link_id` references in the global link map.
|
||||
*/
|
||||
export interface PartialLinkInfo extends ILinkRouting {}
|
||||
|
||||
@@ -37,12 +37,15 @@ app.registerExtension({
|
||||
})
|
||||
container.replaceChildren(video)
|
||||
|
||||
setTimeout(() => resolveVideo(video), 500) // Fallback as loadedmetadata doesnt fire sometimes?
|
||||
video.addEventListener(
|
||||
'loadedmetadata',
|
||||
() => resolveVideo(video),
|
||||
false
|
||||
)
|
||||
let resolved = false
|
||||
const resolve = () => {
|
||||
if (resolved) return
|
||||
resolved = true
|
||||
clearTimeout(fallbackTimeout)
|
||||
resolveVideo(video)
|
||||
}
|
||||
const fallbackTimeout = setTimeout(resolve, 500)
|
||||
video.addEventListener('loadedmetadata', resolve, { once: true })
|
||||
video.srcObject = stream
|
||||
video.play()
|
||||
} catch (error) {
|
||||
|
||||
@@ -112,8 +112,12 @@ export class PrimitiveNode extends LGraphNode {
|
||||
const newValues = rawValues.map(String)
|
||||
comboWidget.options.values = newValues
|
||||
if (!newValues.includes(String(comboWidget.value))) {
|
||||
comboWidget.value = newValues[0]
|
||||
comboWidget.callback?.(comboWidget.value)
|
||||
if (newValues.length > 0) {
|
||||
comboWidget.value = newValues[0]
|
||||
comboWidget.callback?.(comboWidget.value)
|
||||
} else {
|
||||
comboWidget.value = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -705,15 +705,17 @@ export class LGraph
|
||||
const ctorA = A.constructor
|
||||
const ctorB = B.constructor
|
||||
const priorityA =
|
||||
('priority' in ctorA && typeof ctorA.priority === 'number'
|
||||
'priority' in ctorA && typeof ctorA.priority === 'number'
|
||||
? ctorA.priority
|
||||
: 0) ??
|
||||
('priority' in A && typeof A.priority === 'number' ? A.priority : 0)
|
||||
: 'priority' in A && typeof A.priority === 'number'
|
||||
? A.priority
|
||||
: 0
|
||||
const priorityB =
|
||||
('priority' in ctorB && typeof ctorB.priority === 'number'
|
||||
'priority' in ctorB && typeof ctorB.priority === 'number'
|
||||
? ctorB.priority
|
||||
: 0) ??
|
||||
('priority' in B && typeof B.priority === 'number' ? B.priority : 0)
|
||||
: 'priority' in B && typeof B.priority === 'number'
|
||||
? B.priority
|
||||
: 0
|
||||
// if same priority, sort by order
|
||||
return priorityA == priorityB ? A.order - B.order : priorityA - priorityB
|
||||
})
|
||||
|
||||
@@ -104,6 +104,14 @@ import type { UUID } from './utils/uuid'
|
||||
import { BaseWidget } from './widgets/BaseWidget'
|
||||
import { toConcreteWidget } from './widgets/widgetMap'
|
||||
|
||||
function isContentEditable(el: HTMLElement | null): boolean {
|
||||
while (el) {
|
||||
if (el.isContentEditable) return true
|
||||
el = el.parentElement
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
interface IShowSearchOptions {
|
||||
node_to?: SubgraphOutputNode | LGraphNode | null
|
||||
node_from?: SubgraphInputNode | LGraphNode | null
|
||||
@@ -3681,6 +3689,8 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
let block_default = false
|
||||
const targetEl = e.target
|
||||
if (targetEl instanceof HTMLInputElement) return
|
||||
if (targetEl instanceof HTMLTextAreaElement) return
|
||||
if (targetEl instanceof HTMLElement && isContentEditable(targetEl)) return
|
||||
|
||||
if (e.type == 'keydown') {
|
||||
// TODO: Switch
|
||||
@@ -3717,18 +3727,13 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
this.pasteFromClipboard({ connectInputs: e.shiftKey })
|
||||
} else if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||
// delete or backspace
|
||||
if (
|
||||
!(targetEl instanceof HTMLInputElement) &&
|
||||
!(targetEl instanceof HTMLTextAreaElement)
|
||||
) {
|
||||
if (this.selectedItems.size === 0) {
|
||||
this.#noItemsSelected()
|
||||
return
|
||||
}
|
||||
|
||||
this.deleteSelected()
|
||||
block_default = true
|
||||
if (this.selectedItems.size === 0) {
|
||||
this.#noItemsSelected()
|
||||
return
|
||||
}
|
||||
|
||||
this.deleteSelected()
|
||||
block_default = true
|
||||
}
|
||||
|
||||
// TODO
|
||||
@@ -7839,6 +7844,8 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
|
||||
if (typeof root.onOpen == 'function') root.onOpen()
|
||||
|
||||
root.graph = this.graph
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
|
||||
@@ -856,22 +856,17 @@ export class LiteGraphGlobal {
|
||||
capture
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
pointerAndMouseEvents.includes(sEvent) ||
|
||||
pointerOnlyEvents.includes(sEvent)
|
||||
) {
|
||||
} else if (pointerOnlyEvents.includes(sEvent)) {
|
||||
if (this.pointerevents_method == 'pointer') {
|
||||
return oDOM.removeEventListener(
|
||||
oDOM.removeEventListener(
|
||||
this.pointerevents_method + sEvent,
|
||||
fCall,
|
||||
capture
|
||||
)
|
||||
}
|
||||
} else {
|
||||
oDOM.removeEventListener(sEvent, fCall, capture)
|
||||
}
|
||||
|
||||
return oDOM.removeEventListener(sEvent, fCall, capture)
|
||||
}
|
||||
|
||||
getTime(): number {
|
||||
|
||||
@@ -25,9 +25,10 @@ describe('NodeSlot', () => {
|
||||
const serialized = outputAsSerialisable(slot)
|
||||
expect(serialized).not.toHaveProperty('_data')
|
||||
})
|
||||
})
|
||||
|
||||
describe('inputAsSerialisable', () => {
|
||||
it('removes pos from widget input slots', () => {
|
||||
// Minimal slot for serialization test - boundingRect is calculated at runtime, not serialized
|
||||
const widgetInputSlot: INodeInputSlot = {
|
||||
name: 'test-id',
|
||||
pos: [10, 20],
|
||||
@@ -54,7 +55,6 @@ describe('NodeSlot', () => {
|
||||
})
|
||||
|
||||
it('preserves only widget name during serialization', () => {
|
||||
// Extra widget properties simulate real data that should be stripped during serialization
|
||||
const widgetInputSlot: INodeInputSlot = {
|
||||
name: 'test-id',
|
||||
type: 'STRING',
|
||||
|
||||
@@ -289,10 +289,13 @@ describe.skip('SubgraphSerialization - Version Compatibility', () => {
|
||||
id: 'future-id',
|
||||
name: 'Future Subgraph'
|
||||
})
|
||||
// Test with unknown future fields - simulating a hypothetical future version
|
||||
// Test with unknown future fields - simulating a hypothetical future version.
|
||||
// NOTE: The "as unknown as ExportedSubgraph" assertion below is an intentional
|
||||
// exception to normal type-safety rules. It simulates a future schema/version
|
||||
// to validate deserialization resilience against forward-compatible data.
|
||||
const extendedFormat = {
|
||||
...futureFormat,
|
||||
version: 2 as const, // Type assertion for test purposes
|
||||
version: 2 as const,
|
||||
futureFeature: 'unknown_data'
|
||||
} as unknown as ExportedSubgraph
|
||||
|
||||
|
||||
@@ -23,8 +23,11 @@ type WritableComboWidget = Omit<IComboWidget, 'value'> & { value: ExposedValue }
|
||||
const isImageFile = (file: File) => file.type.startsWith('image/')
|
||||
const isVideoFile = (file: File) => file.type.startsWith('video/')
|
||||
|
||||
const findFileComboWidget = (node: LGraphNode, inputName: string) =>
|
||||
node.widgets!.find((w) => w.name === inputName) as IComboWidget
|
||||
const findFileComboWidget = (
|
||||
node: LGraphNode,
|
||||
inputName: string
|
||||
): IComboWidget | undefined =>
|
||||
node.widgets?.find((w): w is IComboWidget => w.name === inputName)
|
||||
|
||||
export const useImageUploadWidget = () => {
|
||||
const widgetConstructor: ComfyWidgetConstructor = (
|
||||
@@ -50,6 +53,9 @@ export const useImageUploadWidget = () => {
|
||||
|
||||
const fileFilter = isVideo ? isVideoFile : isImageFile
|
||||
const fileComboWidget = findFileComboWidget(node, imageInputName)
|
||||
if (!fileComboWidget) {
|
||||
throw new Error(`Widget "${imageInputName}" not found on node`)
|
||||
}
|
||||
const initialFile = `${fileComboWidget.value}`
|
||||
const formatPath = (value: InternalFile) =>
|
||||
createAnnotatedPath(value, { rootFolder: image_folder })
|
||||
|
||||
@@ -190,7 +190,7 @@ function findBox(
|
||||
start: number,
|
||||
end: number,
|
||||
type: string
|
||||
): IsobmffBoxContentRange | null {
|
||||
): IsobmffBoxContentRange {
|
||||
let offset = start
|
||||
while (offset < end) {
|
||||
if (offset + 8 > end) break
|
||||
|
||||
@@ -39,6 +39,7 @@ export function getFromFlacFile(file: File): Promise<Record<string, string>> {
|
||||
if (event.target?.result instanceof ArrayBuffer) {
|
||||
r(getFromFlacBuffer(event.target.result))
|
||||
} else {
|
||||
console.error('FileReader returned a non-ArrayBuffer result')
|
||||
r({})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,15 +28,15 @@ export class ComfyButton implements ComfyComponent<HTMLElement> {
|
||||
contentElement = $el('span')
|
||||
popup: ComfyPopup | null = null
|
||||
element: HTMLElement
|
||||
overIcon!: string
|
||||
iconSize!: number
|
||||
content!: string | HTMLElement
|
||||
icon!: string
|
||||
tooltip!: string
|
||||
classList!: ClassList
|
||||
hidden!: boolean
|
||||
enabled!: boolean
|
||||
action!: (e: Event, btn: ComfyButton) => void
|
||||
overIcon: string | undefined
|
||||
iconSize: number | undefined
|
||||
content: string | HTMLElement | undefined
|
||||
icon: string | undefined
|
||||
tooltip: string | undefined
|
||||
classList: ClassList | undefined
|
||||
hidden: boolean | undefined
|
||||
enabled: boolean | undefined
|
||||
action: ((e: Event, btn: ComfyButton) => void) | undefined
|
||||
|
||||
constructor({
|
||||
icon,
|
||||
@@ -74,13 +74,13 @@ export class ComfyButton implements ComfyComponent<HTMLElement> {
|
||||
'icon',
|
||||
icon,
|
||||
toggleElement(this.iconElement, { onShow: this.updateIcon })
|
||||
)!
|
||||
)
|
||||
this.overIcon = prop(this, 'overIcon', overIcon, () => {
|
||||
if (this.isOver) {
|
||||
this.updateIcon()
|
||||
}
|
||||
})!
|
||||
this.iconSize = prop(this, 'iconSize', iconSize, this.updateIcon)!
|
||||
})
|
||||
this.iconSize = prop(this, 'iconSize', iconSize, this.updateIcon)
|
||||
this.content = prop(
|
||||
this,
|
||||
'content',
|
||||
@@ -94,7 +94,7 @@ export class ComfyButton implements ComfyComponent<HTMLElement> {
|
||||
}
|
||||
}
|
||||
})
|
||||
)!
|
||||
)
|
||||
|
||||
this.tooltip = prop(this, 'tooltip', tooltip, (v) => {
|
||||
if (v) {
|
||||
@@ -102,17 +102,17 @@ export class ComfyButton implements ComfyComponent<HTMLElement> {
|
||||
} else {
|
||||
this.element.removeAttribute('title')
|
||||
}
|
||||
})!
|
||||
})
|
||||
if (tooltip !== undefined) {
|
||||
this.element.setAttribute('aria-label', tooltip)
|
||||
}
|
||||
this.classList = prop(this, 'classList', classList, this.updateClasses)!
|
||||
this.hidden = prop(this, 'hidden', false, this.updateClasses)!
|
||||
this.classList = prop(this, 'classList', classList, this.updateClasses)
|
||||
this.hidden = prop(this, 'hidden', false, this.updateClasses)
|
||||
this.enabled = prop(this, 'enabled', enabled, () => {
|
||||
this.updateClasses()
|
||||
;(this.element as HTMLButtonElement).disabled = !this.enabled
|
||||
})!
|
||||
this.action = prop(this, 'action', action)!
|
||||
})
|
||||
this.action = prop(this, 'action', action)
|
||||
this.element.addEventListener('click', (e) => {
|
||||
if (this.popup) {
|
||||
// we are either a touch device or triggered by click not hover
|
||||
@@ -154,7 +154,9 @@ export class ComfyButton implements ComfyComponent<HTMLElement> {
|
||||
internalClasses.push('popup-closed')
|
||||
}
|
||||
}
|
||||
applyClasses(this.element, this.classList, ...internalClasses)
|
||||
if (this.classList !== undefined) {
|
||||
applyClasses(this.element, this.classList, ...internalClasses)
|
||||
}
|
||||
}
|
||||
|
||||
withPopup(popup: ComfyPopup, mode: 'click' | 'hover' = 'click') {
|
||||
|
||||
@@ -114,6 +114,17 @@ export class DraggableList extends EventTarget {
|
||||
return () => source.removeEventListener(event, boundListener)
|
||||
}
|
||||
|
||||
getPointerCoordinates(
|
||||
e: MouseEvent | TouchEvent
|
||||
): { clientX: number; clientY: number } | null {
|
||||
if ('clientX' in e) {
|
||||
return { clientX: e.clientX, clientY: e.clientY }
|
||||
}
|
||||
const touch = e.touches?.[0] ?? e.changedTouches?.[0]
|
||||
if (!touch) return null
|
||||
return { clientX: touch.clientX, clientY: touch.clientY }
|
||||
}
|
||||
|
||||
dragStart(e: MouseEvent | TouchEvent) {
|
||||
const target = e.target
|
||||
if (!(target instanceof HTMLElement)) return
|
||||
@@ -123,8 +134,10 @@ export class DraggableList extends EventTarget {
|
||||
|
||||
if (!this.draggableItem) return
|
||||
|
||||
const clientX = 'clientX' in e ? e.clientX : e.touches[0].clientX
|
||||
const clientY = 'clientY' in e ? e.clientY : e.touches[0].clientY
|
||||
const coords = this.getPointerCoordinates(e)
|
||||
if (!coords) return
|
||||
|
||||
const { clientX, clientY } = coords
|
||||
|
||||
this.pointerStartX = clientX
|
||||
this.pointerStartY = clientY
|
||||
@@ -184,10 +197,12 @@ export class DraggableList extends EventTarget {
|
||||
drag(e: MouseEvent | TouchEvent) {
|
||||
if (!this.draggableItem) return
|
||||
|
||||
const coords = this.getPointerCoordinates(e)
|
||||
if (!coords) return
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
const clientX = 'clientX' in e ? e.clientX : e.touches[0].clientX
|
||||
const clientY = 'clientY' in e ? e.clientY : e.touches[0].clientY
|
||||
const { clientX, clientY } = coords
|
||||
|
||||
const listRect = this.listContainer.getBoundingClientRect()
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { $el } from '../ui'
|
||||
|
||||
interface ToggleSwitchItem {
|
||||
export interface ToggleSwitchItem {
|
||||
text: string
|
||||
value?: string
|
||||
tooltip?: string
|
||||
@@ -19,6 +19,13 @@ export function toggleSwitch(
|
||||
) {
|
||||
const onChange = e?.onChange
|
||||
|
||||
const normalizedItems: ToggleSwitchItem[] = items.map((item) => {
|
||||
if (typeof item === 'string') {
|
||||
return { text: item, value: item }
|
||||
}
|
||||
return { ...item, value: item.value ?? item.text }
|
||||
})
|
||||
|
||||
let selectedIndex: number | null = null
|
||||
let elements: HTMLLabelElement[]
|
||||
|
||||
@@ -27,20 +34,14 @@ export function toggleSwitch(
|
||||
elements[selectedIndex].classList.remove('comfy-toggle-selected')
|
||||
}
|
||||
onChange?.({
|
||||
item: items[index] as ToggleSwitchItem,
|
||||
prev:
|
||||
selectedIndex == null
|
||||
? undefined
|
||||
: (items[selectedIndex] as ToggleSwitchItem)
|
||||
item: normalizedItems[index],
|
||||
prev: selectedIndex == null ? undefined : normalizedItems[selectedIndex]
|
||||
})
|
||||
selectedIndex = index
|
||||
elements[selectedIndex].classList.add('comfy-toggle-selected')
|
||||
}
|
||||
|
||||
elements = items.map((item, i) => {
|
||||
if (typeof item === 'string') item = { text: item }
|
||||
if (!item.value) item.value = item.text
|
||||
|
||||
elements = normalizedItems.map((item, i) => {
|
||||
const toggle = $el(
|
||||
'label',
|
||||
{
|
||||
|
||||
@@ -7,24 +7,6 @@ import type {
|
||||
IStringWidget
|
||||
} from '@/lib/litegraph/src/types/widgets'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
|
||||
type ComboValuesType = IComboWidget['options']['values']
|
||||
|
||||
/**
|
||||
* Normalizes combo widget values to an array.
|
||||
* Handles the case where values may be a dictionary (Record<string, string>)
|
||||
* or a legacy function type.
|
||||
*/
|
||||
function getComboValuesArray(
|
||||
values: ComboValuesType | undefined,
|
||||
widget?: IComboWidget,
|
||||
node?: LGraphNode
|
||||
): string[] {
|
||||
if (!values) return []
|
||||
if (typeof values === 'function') return values(widget, node)
|
||||
if (Array.isArray(values)) return values
|
||||
return Object.keys(values)
|
||||
}
|
||||
import { dynamicWidgets } from '@/core/graph/widgets/dynamicWidgets'
|
||||
import { useBooleanWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useBooleanWidget'
|
||||
import { useChartWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useChartWidget'
|
||||
@@ -46,6 +28,24 @@ import type { ComfyApp } from './app'
|
||||
import './domWidget'
|
||||
import './errorNodeWidgets'
|
||||
|
||||
type ComboValuesType = IComboWidget['options']['values']
|
||||
|
||||
/**
|
||||
* Normalizes combo widget values to an array.
|
||||
* Handles the case where values may be a dictionary (Record<string, string>)
|
||||
* or a legacy function type.
|
||||
*/
|
||||
function getComboValuesArray(
|
||||
values: ComboValuesType | undefined,
|
||||
widget?: IComboWidget,
|
||||
node?: LGraphNode
|
||||
): string[] {
|
||||
if (!values) return []
|
||||
if (typeof values === 'function') return values(widget, node)
|
||||
if (Array.isArray(values)) return values
|
||||
return Object.keys(values)
|
||||
}
|
||||
|
||||
export type ComfyWidgetConstructorV2 = (
|
||||
node: LGraphNode,
|
||||
inputSpec: InputSpecV2
|
||||
|
||||
5
src/types/litegraph-augmentation.d.ts
vendored
5
src/types/litegraph-augmentation.d.ts
vendored
@@ -118,6 +118,11 @@ declare module '@/lib/litegraph/src/litegraph' {
|
||||
* This method is a no-op stub for backward compatibility with extensions.
|
||||
*/
|
||||
convertWidgetToInput?(): boolean
|
||||
/**
|
||||
* Recreates this node, typically used to refresh node state after definition changes.
|
||||
* Callers should await the result and handle the null case (recreation failed or was cancelled).
|
||||
* @returns A promise resolving to the new node instance, or null if recreation failed.
|
||||
*/
|
||||
recreate?(): Promise<LGraphNode | null>
|
||||
refreshComboInNode?(defs: Record<string, ComfyNodeDef>)
|
||||
/** @deprecated groupNode */
|
||||
|
||||
@@ -92,7 +92,7 @@ export function deserialiseAndCreate(data: string, canvas: LGraphCanvas): void {
|
||||
try {
|
||||
graph.beforeChange()
|
||||
|
||||
const deserialised = JSON.parse(data)
|
||||
const deserialised = JSON.parse(data) as VintageSerialisable
|
||||
|
||||
// Find the top left point of the boundary of all pasted nodes
|
||||
const topLeft = [Infinity, Infinity]
|
||||
@@ -128,7 +128,8 @@ export function deserialiseAndCreate(data: string, canvas: LGraphCanvas): void {
|
||||
const relativeId = info[0]
|
||||
const outNode = relativeId != null ? nodes[relativeId] : undefined
|
||||
|
||||
const inNode = nodes[info[2]]
|
||||
const inNodeId = info[2]
|
||||
const inNode = inNodeId != null ? nodes[inNodeId] : undefined
|
||||
if (outNode && inNode) outNode.connect(info[1], inNode, info[3])
|
||||
else console.warn('Warning, nodes missing on pasting')
|
||||
}
|
||||
|
||||
@@ -9,22 +9,17 @@ import { useMissingNodes } from '@/workbench/extensions/manager/composables/node
|
||||
import { useWorkflowPacks } from '@/workbench/extensions/manager/composables/nodePack/useWorkflowPacks'
|
||||
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
||||
|
||||
type PartialNodeDefStore = Pick<
|
||||
ReturnType<typeof useNodeDefStore>,
|
||||
'nodeDefsByName'
|
||||
>
|
||||
type NodeDefStore = ReturnType<typeof useNodeDefStore>
|
||||
type PartialManagerStore = Pick<
|
||||
ReturnType<typeof useComfyManagerStore>,
|
||||
'isPackInstalled'
|
||||
>
|
||||
|
||||
function createMockNodeDefStore(
|
||||
names: string[]
|
||||
): ReturnType<typeof useNodeDefStore> {
|
||||
const nodeDefsByName = Object.fromEntries(
|
||||
function createMockNodeDefStore(names: string[]): NodeDefStore {
|
||||
const nodeDefsByName: Record<string, ComfyNodeDefImpl> = Object.fromEntries(
|
||||
names.map((name) => [name, { name } as ComfyNodeDefImpl])
|
||||
)
|
||||
return { nodeDefsByName } as ReturnType<typeof useNodeDefStore>
|
||||
return { nodeDefsByName } as unknown as NodeDefStore
|
||||
}
|
||||
|
||||
vi.mock('@vueuse/core', async () => {
|
||||
@@ -117,12 +112,7 @@ describe('useMissingNodes', () => {
|
||||
})
|
||||
|
||||
// Reset node def store mock
|
||||
const partialNodeDefStore: PartialNodeDefStore = {
|
||||
nodeDefsByName: {}
|
||||
}
|
||||
mockUseNodeDefStore.mockReturnValue(
|
||||
partialNodeDefStore as ReturnType<typeof useNodeDefStore>
|
||||
)
|
||||
mockUseNodeDefStore.mockReturnValue(createMockNodeDefStore([]))
|
||||
|
||||
// Reset app.rootGraph.nodes
|
||||
mockApp.rootGraph = { nodes: [] }
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { TWidgetValue } from '@/lib/litegraph/src/litegraph'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
|
||||
interface HasInputOrder {
|
||||
input_order?: Record<string, string[]>
|
||||
input_order?: { required?: string[]; optional?: string[] }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user