mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-15 09:57:33 +00:00
Compare commits
10 Commits
task-runne
...
core/1.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aeed923735 | ||
|
|
ca33015ced | ||
|
|
3195e8a697 | ||
|
|
66f3ccd7a8 | ||
|
|
31c46fe2b1 | ||
|
|
a16061d670 | ||
|
|
77d45d8eff | ||
|
|
5972a07cfe | ||
|
|
9d8633aafa | ||
|
|
06292bccfc |
1
.github/workflows/release.yaml
vendored
1
.github/workflows/release.yaml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
- core/*
|
||||
paths:
|
||||
- "package.json"
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { Keybinding } from '../src/types/keyBindingTypes'
|
||||
import { comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||
|
||||
test.describe('Load workflow warning', () => {
|
||||
@@ -103,4 +104,52 @@ test.describe('Settings', () => {
|
||||
expect(await comfyPage.getSetting('Comfy.Graph.ZoomSpeed')).toBe(maxSpeed)
|
||||
})
|
||||
})
|
||||
|
||||
test('Should persist keybinding setting', async ({ comfyPage }) => {
|
||||
// Open the settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
await comfyPage.page.waitForSelector('.settings-container')
|
||||
|
||||
// Open the keybinding tab
|
||||
await comfyPage.page.getByLabel('Keybinding').click()
|
||||
await comfyPage.page.waitForSelector(
|
||||
'[placeholder="Search Keybindings..."]'
|
||||
)
|
||||
|
||||
// Focus the 'New Blank Workflow' row
|
||||
const newBlankWorkflowRow = comfyPage.page.locator('tr', {
|
||||
has: comfyPage.page.getByRole('cell', { name: 'New Blank Workflow' })
|
||||
})
|
||||
await newBlankWorkflowRow.click()
|
||||
|
||||
// Click edit button
|
||||
const editKeybindingButton = newBlankWorkflowRow.locator('.pi-pencil')
|
||||
await editKeybindingButton.click()
|
||||
|
||||
// Set new keybinding
|
||||
const input = comfyPage.page.getByPlaceholder('Press keys for new binding')
|
||||
await input.press('Alt+n')
|
||||
|
||||
const requestPromise = comfyPage.page.waitForRequest(
|
||||
'**/api/settings/Comfy.Keybinding.NewBindings'
|
||||
)
|
||||
|
||||
// Save keybinding
|
||||
const saveButton = comfyPage.page
|
||||
.getByLabel('Comfy.NewBlankWorkflow')
|
||||
.getByLabel('Save')
|
||||
await saveButton.click()
|
||||
|
||||
const request = await requestPromise
|
||||
const expectedSetting: Keybinding = {
|
||||
commandId: 'Comfy.NewBlankWorkflow',
|
||||
combo: {
|
||||
key: 'n',
|
||||
ctrl: false,
|
||||
alt: true,
|
||||
shift: false
|
||||
}
|
||||
}
|
||||
expect(request.postData()).toContain(JSON.stringify(expectedSetting))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -821,6 +821,11 @@ export class ComfyPage {
|
||||
async getNodeRefById(id: NodeId) {
|
||||
return new NodeReference(id, this)
|
||||
}
|
||||
async getNodes() {
|
||||
return await this.page.evaluate(() => {
|
||||
return window['app'].graph.nodes
|
||||
})
|
||||
}
|
||||
async getNodeRefsByType(type: string): Promise<NodeReference[]> {
|
||||
return Promise.all(
|
||||
(
|
||||
|
||||
@@ -152,6 +152,13 @@ export class WorkflowsSidebarTab extends SidebarTab {
|
||||
await this.page.keyboard.press('Enter')
|
||||
await this.page.waitForTimeout(300)
|
||||
}
|
||||
|
||||
async insertWorkflow(locator: Locator) {
|
||||
await locator.click({ button: 'right' })
|
||||
await this.page
|
||||
.locator('.p-contextmenu-item-content', { hasText: 'Insert' })
|
||||
.click()
|
||||
}
|
||||
}
|
||||
|
||||
export class QueueSidebarTab extends SidebarTab {
|
||||
|
||||
@@ -469,6 +469,43 @@ test.describe('Canvas Interaction', () => {
|
||||
expect(await getCursorStyle()).toBe('default')
|
||||
})
|
||||
|
||||
// https://github.com/Comfy-Org/litegraph.js/pull/424
|
||||
test('Properly resets dragging state after pan mode sequence', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const getCursorStyle = async () => {
|
||||
return await comfyPage.page.evaluate(() => {
|
||||
return (
|
||||
document.getElementById('graph-canvas')!.style.cursor || 'default'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// Initial state check
|
||||
await comfyPage.page.mouse.move(10, 10)
|
||||
expect(await getCursorStyle()).toBe('default')
|
||||
|
||||
// Click and hold
|
||||
await comfyPage.page.mouse.down()
|
||||
expect(await getCursorStyle()).toBe('grabbing')
|
||||
|
||||
// Press space while holding click
|
||||
await comfyPage.page.keyboard.down('Space')
|
||||
expect(await getCursorStyle()).toBe('grabbing')
|
||||
|
||||
// Release click while space is still down
|
||||
await comfyPage.page.mouse.up()
|
||||
expect(await getCursorStyle()).toBe('grab')
|
||||
|
||||
// Release space
|
||||
await comfyPage.page.keyboard.up('Space')
|
||||
expect(await getCursorStyle()).toBe('default')
|
||||
|
||||
// Move mouse - cursor should remain default
|
||||
await comfyPage.page.mouse.move(20, 20)
|
||||
expect(await getCursorStyle()).toBe('default')
|
||||
})
|
||||
|
||||
test('Can pan when dragging a link', async ({ comfyPage }) => {
|
||||
const posSlot1 = comfyPage.clipTextEncodeNode1InputSlot
|
||||
await comfyPage.page.mouse.move(posSlot1.x, posSlot1.y)
|
||||
|
||||
@@ -429,6 +429,26 @@ test.describe('Menu', () => {
|
||||
])
|
||||
})
|
||||
|
||||
test('Can open workflow after insert', async ({ comfyPage }) => {
|
||||
await comfyPage.setupWorkflowsDirectory({
|
||||
'workflow1.json': 'single_ksampler.json'
|
||||
})
|
||||
await comfyPage.setup()
|
||||
|
||||
const tab = comfyPage.menu.workflowsTab
|
||||
await tab.open()
|
||||
await comfyPage.executeCommand('Comfy.LoadDefaultWorkflow')
|
||||
const originalNodeCount = (await comfyPage.getNodes()).length
|
||||
|
||||
await tab.insertWorkflow(tab.getPersistedItem('workflow1.json'))
|
||||
await comfyPage.nextFrame()
|
||||
expect((await comfyPage.getNodes()).length).toEqual(originalNodeCount + 1)
|
||||
|
||||
await tab.getPersistedItem('workflow1.json').click()
|
||||
await comfyPage.nextFrame()
|
||||
expect((await comfyPage.getNodes()).length).toEqual(1)
|
||||
})
|
||||
|
||||
test('Can rename nested workflow from opened workflow item', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
|
||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.6.14",
|
||||
"version": "1.6.18",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.6.14",
|
||||
"version": "1.6.18",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.3",
|
||||
"@comfyorg/litegraph": "^0.8.58",
|
||||
"@comfyorg/litegraph": "^0.8.60",
|
||||
"@primevue/themes": "^4.0.5",
|
||||
"@tiptap/core": "^2.10.4",
|
||||
"@tiptap/extension-link": "^2.10.4",
|
||||
@@ -1940,9 +1940,9 @@
|
||||
"license": "GPL-3.0-only"
|
||||
},
|
||||
"node_modules/@comfyorg/litegraph": {
|
||||
"version": "0.8.58",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.58.tgz",
|
||||
"integrity": "sha512-V/4yC8i5QOpDV20ZiEMiZP6KnmYD5d15El3V4tmH/MkhjOxjc6owAFMyAVgpxphYdcBF2qj1QTNTrZLgC6x2VQ==",
|
||||
"version": "0.8.60",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.60.tgz",
|
||||
"integrity": "sha512-LkZalBcka1xVxkL7JnkF/1EzyvspLyrSthzyN9ZumWJw7kAaZkO9omraXv2t/UiFsqwMr5M/AV5UY915Vq8cxQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.6.14",
|
||||
"version": "1.6.18",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -82,7 +82,7 @@
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.3",
|
||||
"@comfyorg/litegraph": "^0.8.58",
|
||||
"@comfyorg/litegraph": "^0.8.60",
|
||||
"@primevue/themes": "^4.0.5",
|
||||
"@tiptap/core": "^2.10.4",
|
||||
"@tiptap/extension-link": "^2.10.4",
|
||||
|
||||
@@ -112,7 +112,7 @@ const loadWorkflow = async (id: string) => {
|
||||
let json
|
||||
if (selectedTab.value.moduleName === 'default') {
|
||||
// Default templates provided by frontend are served on this separate endpoint
|
||||
json = await fetch(api.fileURL(`templates/${id}.json`)).then((r) =>
|
||||
json = await fetch(api.fileURL(`/templates/${id}.json`)).then((r) =>
|
||||
r.json()
|
||||
)
|
||||
} else {
|
||||
|
||||
@@ -25,11 +25,11 @@ app.registerExtension({
|
||||
if (!this.properties) {
|
||||
this.properties = { text: '' }
|
||||
}
|
||||
ComfyWidgets.MARKDOWN(
|
||||
ComfyWidgets.STRING(
|
||||
// Should we extends LGraphNode? Yesss
|
||||
this,
|
||||
'',
|
||||
['', { default: this.properties.text }],
|
||||
['', { default: this.properties.text, multiline: true }],
|
||||
app
|
||||
)
|
||||
|
||||
@@ -50,5 +50,33 @@ app.registerExtension({
|
||||
)
|
||||
|
||||
NoteNode.category = 'utils'
|
||||
|
||||
/** Markdown variant of NoteNode */
|
||||
class MarkdownNoteNode extends LGraphNode {
|
||||
static title = 'Markdown Note'
|
||||
|
||||
color = LGraphCanvas.node_colors.yellow.color
|
||||
bgcolor = LGraphCanvas.node_colors.yellow.bgcolor
|
||||
groupcolor = LGraphCanvas.node_colors.yellow.groupcolor
|
||||
|
||||
constructor(title?: string) {
|
||||
super(title)
|
||||
if (!this.properties) {
|
||||
this.properties = { text: '' }
|
||||
}
|
||||
ComfyWidgets.MARKDOWN(
|
||||
this,
|
||||
'',
|
||||
['', { default: this.properties.text }],
|
||||
app
|
||||
)
|
||||
|
||||
this.serialize_widgets = true
|
||||
this.isVirtualNode = true
|
||||
}
|
||||
}
|
||||
|
||||
LiteGraph.registerNodeType('MarkdownNote', MarkdownNoteNode)
|
||||
MarkdownNoteNode.category = 'utils'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -49,7 +49,7 @@ import {
|
||||
} from './pnginfo'
|
||||
import { $el, ComfyUI } from './ui'
|
||||
import { ComfyAppMenu } from './ui/menu/index'
|
||||
import { getStorageValue } from './utils'
|
||||
import { clone, getStorageValue } from './utils'
|
||||
import { type ComfyWidgetConstructor, ComfyWidgets } from './widgets'
|
||||
|
||||
export const ANIM_PREVIEW_WIDGET = '$$comfy_animation_preview'
|
||||
@@ -1271,11 +1271,7 @@ export class ComfyApp {
|
||||
reset_invalid_values = true
|
||||
}
|
||||
|
||||
if (typeof structuredClone === 'undefined') {
|
||||
graphData = JSON.parse(JSON.stringify(graphData))
|
||||
} else {
|
||||
graphData = structuredClone(graphData)
|
||||
}
|
||||
graphData = clone(graphData)
|
||||
|
||||
if (useSettingStore().get('Comfy.Validation.Workflows')) {
|
||||
// TODO: Show validation error in a dialog.
|
||||
|
||||
@@ -84,11 +84,11 @@ export const useKeybindingService = () => {
|
||||
// Allow setting multiple values at once in settingStore
|
||||
await settingStore.set(
|
||||
'Comfy.Keybinding.NewBindings',
|
||||
Object.values(keybindingStore.userKeybindings.value)
|
||||
Object.values(keybindingStore.getUserKeybindings())
|
||||
)
|
||||
await settingStore.set(
|
||||
'Comfy.Keybinding.UnsetBindings',
|
||||
Object.values(keybindingStore.userUnsetKeybindings.value)
|
||||
Object.values(keybindingStore.getUserUnsetKeybindings())
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -105,6 +105,20 @@ export const useKeybindingStore = defineStore('keybinding', () => {
|
||||
*/
|
||||
const userUnsetKeybindings = ref<Record<string, KeybindingImpl>>({})
|
||||
|
||||
/**
|
||||
* Get user-defined keybindings.
|
||||
*/
|
||||
function getUserKeybindings() {
|
||||
return userKeybindings.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user-defined keybindings that unset default keybindings.
|
||||
*/
|
||||
function getUserUnsetKeybindings() {
|
||||
return userUnsetKeybindings.value
|
||||
}
|
||||
|
||||
const keybindingByKeyCombo = computed<Record<string, KeybindingImpl>>(() => {
|
||||
const result: Record<string, KeybindingImpl> = {
|
||||
...defaultKeybindings.value
|
||||
@@ -262,8 +276,8 @@ export const useKeybindingStore = defineStore('keybinding', () => {
|
||||
|
||||
return {
|
||||
keybindings,
|
||||
userKeybindings,
|
||||
userUnsetKeybindings,
|
||||
getUserKeybindings,
|
||||
getUserUnsetKeybindings,
|
||||
getKeybinding,
|
||||
getKeybindingsByCommandId,
|
||||
getKeybindingByCommandId,
|
||||
|
||||
@@ -289,6 +289,19 @@ export const SYSTEM_NODE_DEFS: Record<string, ComfyNodeDef> = {
|
||||
output_node: false,
|
||||
python_module: 'nodes',
|
||||
description: 'Node that add notes to your project'
|
||||
},
|
||||
MarkdownNote: {
|
||||
name: 'MarkdownNote',
|
||||
display_name: 'Markdown Note',
|
||||
category: 'utils',
|
||||
input: { required: {}, optional: {} },
|
||||
output: [],
|
||||
output_name: [],
|
||||
output_is_list: [],
|
||||
output_node: false,
|
||||
python_module: 'nodes',
|
||||
description:
|
||||
'Node that add notes to your project. Reformats text as markdown.'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import _ from 'lodash'
|
||||
import { defineStore } from 'pinia'
|
||||
import type { TreeNode } from 'primevue/treenode'
|
||||
import { computed, ref } from 'vue'
|
||||
@@ -74,7 +75,12 @@ export const useSettingStore = defineStore('setting', () => {
|
||||
* @param value - The value to set.
|
||||
*/
|
||||
async function set<K extends keyof Settings>(key: K, value: Settings[K]) {
|
||||
const newValue = tryMigrateDeprecatedValue(settingsById.value[key], value)
|
||||
// Clone the incoming value to prevent external mutations
|
||||
const clonedValue = _.cloneDeep(value)
|
||||
const newValue = tryMigrateDeprecatedValue(
|
||||
settingsById.value[key],
|
||||
clonedValue
|
||||
)
|
||||
const oldValue = get(key)
|
||||
if (newValue === oldValue) return
|
||||
|
||||
@@ -89,7 +95,8 @@ export const useSettingStore = defineStore('setting', () => {
|
||||
* @returns The value of the setting.
|
||||
*/
|
||||
function get<K extends keyof Settings>(key: K): Settings[K] {
|
||||
return settingValues.value[key] ?? getDefaultValue(key)
|
||||
// Clone the value when returning to prevent external mutations
|
||||
return _.cloneDeep(settingValues.value[key] ?? getDefaultValue(key))
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -160,6 +160,95 @@ describe('useSettingStore', () => {
|
||||
'differentvalue'
|
||||
)
|
||||
})
|
||||
|
||||
describe('object mutation prevention', () => {
|
||||
beforeEach(() => {
|
||||
const setting: SettingParams = {
|
||||
id: 'test.setting',
|
||||
name: 'Test setting',
|
||||
type: 'hidden',
|
||||
defaultValue: {}
|
||||
}
|
||||
store.addSetting(setting)
|
||||
})
|
||||
|
||||
it('should prevent mutations of objects after set', async () => {
|
||||
const originalObject = { foo: 'bar', nested: { value: 123 } }
|
||||
|
||||
await store.set('test.setting', originalObject)
|
||||
|
||||
// Attempt to mutate the original object
|
||||
originalObject.foo = 'changed'
|
||||
originalObject.nested.value = 456
|
||||
|
||||
// Get the stored value
|
||||
const storedValue = store.get('test.setting')
|
||||
|
||||
// Verify the stored value wasn't affected by the mutation
|
||||
expect(storedValue).toEqual({ foo: 'bar', nested: { value: 123 } })
|
||||
})
|
||||
|
||||
it('should prevent mutations of retrieved objects', () => {
|
||||
const initialValue = { foo: 'bar', nested: { value: 123 } }
|
||||
|
||||
// Set initial value
|
||||
store.set('test.setting', initialValue)
|
||||
|
||||
// Get the value and try to mutate it
|
||||
const retrievedValue = store.get('test.setting')
|
||||
retrievedValue.foo = 'changed'
|
||||
if (retrievedValue.nested) {
|
||||
retrievedValue.nested.value = 456
|
||||
}
|
||||
|
||||
// Get the value again
|
||||
const newRetrievedValue = store.get('test.setting')
|
||||
|
||||
// Verify the stored value wasn't affected by the mutation
|
||||
expect(newRetrievedValue).toEqual({
|
||||
foo: 'bar',
|
||||
nested: { value: 123 }
|
||||
})
|
||||
})
|
||||
|
||||
it('should prevent mutations of arrays after set', async () => {
|
||||
const originalArray = [1, 2, { value: 3 }]
|
||||
|
||||
await store.set('test.setting', originalArray)
|
||||
|
||||
// Attempt to mutate the original array
|
||||
originalArray.push(4)
|
||||
if (typeof originalArray[2] === 'object') {
|
||||
originalArray[2].value = 999
|
||||
}
|
||||
|
||||
// Get the stored value
|
||||
const storedValue = store.get('test.setting')
|
||||
|
||||
// Verify the stored value wasn't affected by the mutation
|
||||
expect(storedValue).toEqual([1, 2, { value: 3 }])
|
||||
})
|
||||
|
||||
it('should prevent mutations of retrieved arrays', () => {
|
||||
const initialArray = [1, 2, { value: 3 }]
|
||||
|
||||
// Set initial value
|
||||
store.set('test.setting', initialArray)
|
||||
|
||||
// Get the value and try to mutate it
|
||||
const retrievedArray = store.get('test.setting')
|
||||
retrievedArray.push(4)
|
||||
if (typeof retrievedArray[2] === 'object') {
|
||||
retrievedArray[2].value = 999
|
||||
}
|
||||
|
||||
// Get the value again
|
||||
const newRetrievedValue = store.get('test.setting')
|
||||
|
||||
// Verify the stored value wasn't affected by the mutation
|
||||
expect(newRetrievedValue).toEqual([1, 2, { value: 3 }])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user