Compare commits
43 Commits
v1.4.3
...
fix-queue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02c13c403f | ||
|
|
8254e3c9cf | ||
|
|
1160231b62 | ||
|
|
a51e27bedf | ||
|
|
abed0656af | ||
|
|
5febda16c7 | ||
|
|
069dc67c30 | ||
|
|
7623810166 | ||
|
|
21fa88461f | ||
|
|
27b0493306 | ||
|
|
ad2c1a0d3e | ||
|
|
f51866d988 | ||
|
|
46627bb44b | ||
|
|
68cadbda9f | ||
|
|
0f2260065a | ||
|
|
4007cc13c2 | ||
|
|
3920210c5c | ||
|
|
4e22bffae2 | ||
|
|
462a131557 | ||
|
|
ec01a04786 | ||
|
|
4c48241e19 | ||
|
|
886c40a69a | ||
|
|
479d1b28c7 | ||
|
|
c41b57128a | ||
|
|
5d178a407d | ||
|
|
73b7606f6e | ||
|
|
94f5031f0d | ||
|
|
c857e7d98c | ||
|
|
d5b8a555d9 | ||
|
|
f34d50da3d | ||
|
|
4f3693e322 | ||
|
|
431ad7d27f | ||
|
|
0c97b09a5a | ||
|
|
bdb9f0d845 | ||
|
|
77b85acdd5 | ||
|
|
8906f5c26e | ||
|
|
81194cc7fe | ||
|
|
f4b972fab5 | ||
|
|
3aa1c03566 | ||
|
|
600b7f93e5 | ||
|
|
2a7df57404 | ||
|
|
6352cd86ee | ||
|
|
0058691579 |
90
browser_tests/assets/oversized_group.json
Normal file
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"last_node_id": 9,
|
||||
"last_link_id": 9,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 3,
|
||||
"type": "KSampler",
|
||||
"pos": [
|
||||
37,
|
||||
98
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
262
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSampler"
|
||||
},
|
||||
"widgets_values": [
|
||||
156680208700286,
|
||||
"randomize",
|
||||
20,
|
||||
8,
|
||||
"euler",
|
||||
"normal",
|
||||
1
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Group",
|
||||
"bounding": [
|
||||
23,
|
||||
23,
|
||||
900,
|
||||
825
|
||||
],
|
||||
"color": "#3f789e",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -11,7 +11,7 @@ test.describe('Browser tab title', () => {
|
||||
const workflowName = await comfyPage.page.evaluate(async () => {
|
||||
return window['app'].extensionManager.workflow.activeWorkflow.filename
|
||||
})
|
||||
expect(await comfyPage.page.title()).toBe(`${workflowName} - ComfyUI`)
|
||||
expect(await comfyPage.page.title()).toBe(`*${workflowName} - ComfyUI`)
|
||||
})
|
||||
|
||||
// Failing on CI
|
||||
|
||||
@@ -56,9 +56,7 @@ test.describe('Change Tracker', () => {
|
||||
expect(await comfyPage.getToastErrorCount()).toBe(0)
|
||||
expect(await isModified()).toBe(false)
|
||||
|
||||
// TODO(huchenlei): Investigate why saving the workflow is causing the
|
||||
// undo queue to be triggered.
|
||||
expect(await getUndoQueueSize()).toBe(1)
|
||||
expect(await getUndoQueueSize()).toBe(0)
|
||||
expect(await getRedoQueueSize()).toBe(0)
|
||||
|
||||
const node = (await comfyPage.getFirstNodeRef())!
|
||||
@@ -66,25 +64,25 @@ test.describe('Change Tracker', () => {
|
||||
await node.click('collapse')
|
||||
await expect(node).toBeCollapsed()
|
||||
expect(await isModified()).toBe(true)
|
||||
expect(await getUndoQueueSize()).toBe(2)
|
||||
expect(await getUndoQueueSize()).toBe(1)
|
||||
expect(await getRedoQueueSize()).toBe(0)
|
||||
|
||||
await comfyPage.ctrlB()
|
||||
await expect(node).toBeBypassed()
|
||||
expect(await isModified()).toBe(true)
|
||||
expect(await getUndoQueueSize()).toBe(3)
|
||||
expect(await getUndoQueueSize()).toBe(2)
|
||||
expect(await getRedoQueueSize()).toBe(0)
|
||||
|
||||
await comfyPage.ctrlZ()
|
||||
await expect(node).not.toBeBypassed()
|
||||
expect(await isModified()).toBe(true)
|
||||
expect(await getUndoQueueSize()).toBe(2)
|
||||
expect(await getUndoQueueSize()).toBe(1)
|
||||
expect(await getRedoQueueSize()).toBe(1)
|
||||
|
||||
await comfyPage.ctrlZ()
|
||||
await expect(node).not.toBeCollapsed()
|
||||
expect(await isModified()).toBe(false)
|
||||
expect(await getUndoQueueSize()).toBe(1)
|
||||
expect(await getUndoQueueSize()).toBe(0)
|
||||
expect(await getRedoQueueSize()).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -737,6 +737,19 @@ export class ComfyPage {
|
||||
)
|
||||
}
|
||||
|
||||
async confirmDialog(prompt: string, text: string = 'Yes') {
|
||||
const modal = this.page.locator(
|
||||
`.comfy-modal-content:has-text("${prompt}")`
|
||||
)
|
||||
await expect(modal).toBeVisible()
|
||||
await modal
|
||||
.locator('.comfyui-button', {
|
||||
hasText: text
|
||||
})
|
||||
.click()
|
||||
await expect(modal).toBeHidden()
|
||||
}
|
||||
|
||||
async convertAllNodesToGroupNode(groupNodeName: string) {
|
||||
this.page.on('dialog', async (dialog) => {
|
||||
await dialog.accept(groupNodeName)
|
||||
|
||||
@@ -103,6 +103,12 @@ export class WorkflowsSidebarTab extends SidebarTab {
|
||||
.allInnerTexts()
|
||||
}
|
||||
|
||||
async getActiveWorkflowName() {
|
||||
return await this.page
|
||||
.locator('.comfyui-workflows-open .p-tree-node-selected .node-label')
|
||||
.innerText()
|
||||
}
|
||||
|
||||
async getTopLevelSavedWorkflowNames() {
|
||||
return await this.page
|
||||
.locator('.comfyui-workflows-browse .node-label')
|
||||
|
||||
@@ -320,6 +320,15 @@ test.describe('Node Interaction', () => {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('group-selected-nodes.png')
|
||||
})
|
||||
|
||||
test('Can fit group to contents', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('oversized_group')
|
||||
await comfyPage.ctrlA()
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.executeCommand('Comfy.Graph.FitGroupToContents')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('group-fit-to-contents.png')
|
||||
})
|
||||
|
||||
// Somehow this test fails on GitHub Actions. It works locally.
|
||||
// https://github.com/Comfy-Org/ComfyUI_frontend/pull/736
|
||||
test.skip('Can pin/unpin nodes with keyboard shortcut', async ({
|
||||
|
||||
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
@@ -379,7 +379,9 @@ test.describe('Menu', () => {
|
||||
// Open the sidebar
|
||||
const tab = comfyPage.menu.workflowsTab
|
||||
await tab.open()
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setupWorkflowsDirectory({})
|
||||
})
|
||||
|
||||
@@ -392,7 +394,7 @@ test.describe('Menu', () => {
|
||||
await tab.newBlankWorkflowButton.click()
|
||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow.json',
|
||||
'Unsaved Workflow (2).json'
|
||||
'*Unsaved Workflow (2).json'
|
||||
])
|
||||
})
|
||||
|
||||
@@ -450,6 +452,44 @@ test.describe('Menu', () => {
|
||||
).toEqual(['*Unsaved Workflow.json', 'workflow3.json', 'workflow4.json'])
|
||||
})
|
||||
|
||||
test('Can save workflow as with same name', async ({ comfyPage }) => {
|
||||
await comfyPage.menu.topbar.saveWorkflow('workflow5.json')
|
||||
expect(
|
||||
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
|
||||
).toEqual(['workflow5.json'])
|
||||
|
||||
await comfyPage.menu.topbar.saveWorkflowAs('workflow5.json')
|
||||
await comfyPage.confirmDialog('Overwrite existing file?', 'Yes')
|
||||
expect(
|
||||
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
|
||||
).toEqual(['workflow5.json'])
|
||||
})
|
||||
|
||||
test('Can overwrite other workflows with save as', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const topbar = comfyPage.menu.topbar
|
||||
await topbar.saveWorkflow('workflow1.json')
|
||||
await topbar.saveWorkflowAs('workflow2.json')
|
||||
await comfyPage.nextFrame()
|
||||
expect(
|
||||
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
|
||||
).toEqual(['workflow1.json', 'workflow2.json'])
|
||||
expect(await comfyPage.menu.workflowsTab.getActiveWorkflowName()).toEqual(
|
||||
'workflow2.json'
|
||||
)
|
||||
|
||||
await topbar.saveWorkflowAs('workflow1.json')
|
||||
await comfyPage.confirmDialog('Overwrite existing file?', 'Yes')
|
||||
// The old workflow1.json should be deleted and the new one should be saved.
|
||||
expect(
|
||||
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
|
||||
).toEqual(['workflow2.json', 'workflow1.json'])
|
||||
expect(await comfyPage.menu.workflowsTab.getActiveWorkflowName()).toEqual(
|
||||
'workflow1.json'
|
||||
)
|
||||
})
|
||||
|
||||
test('Does not report warning when switching between opened workflows', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
@@ -475,12 +515,12 @@ test.describe('Menu', () => {
|
||||
`tempWorkflow-${test.info().title}`
|
||||
)
|
||||
const closeButton = comfyPage.page.locator(
|
||||
'.comfyui-workflows-open .p-button-icon.pi-times'
|
||||
'.comfyui-workflows-open .close-workflow-button'
|
||||
)
|
||||
await closeButton.click()
|
||||
expect(
|
||||
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
|
||||
).toEqual(['Unsaved Workflow.json'])
|
||||
).toEqual(['*Unsaved Workflow.json'])
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 96 KiB |
20
package-lock.json
generated
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "comfyui-frontend",
|
||||
"version": "1.4.3",
|
||||
"version": "1.4.9",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "comfyui-frontend",
|
||||
"version": "1.4.3",
|
||||
"version": "1.4.9",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.2.16",
|
||||
"@comfyorg/litegraph": "^0.8.29",
|
||||
"@comfyorg/comfyui-electron-types": "^0.3.6",
|
||||
"@comfyorg/litegraph": "^0.8.35",
|
||||
"@primevue/themes": "^4.0.5",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
@@ -1917,15 +1917,15 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@comfyorg/comfyui-electron-types": {
|
||||
"version": "0.2.16",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.2.16.tgz",
|
||||
"integrity": "sha512-Hm6NeyMK4sd2V5AyOnvfI+tvCsXr5NBG8wOZlWyyD17ADpbQnpm6qPMWzvm4vCp/YvTR7cUbDGiY0quhofuQGg==",
|
||||
"version": "0.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.3.6.tgz",
|
||||
"integrity": "sha512-wgMgESnCcRzvVkk8CwWiTAUJxC4LBvw5uTENxzaWkEL0qrnmiGrVLore00yX3cYz04hJaTA6PqasLqgVLDOenw==",
|
||||
"license": "GPL-3.0-only"
|
||||
},
|
||||
"node_modules/@comfyorg/litegraph": {
|
||||
"version": "0.8.29",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.29.tgz",
|
||||
"integrity": "sha512-h7c+sW/BEAPfWSDYATNk2YtB1kduQ0v85z2Rq8q11UoLHRkTa0A1zosDFSwIdCul/prdG51wbXr99EHq3Dsfnw==",
|
||||
"version": "0.8.35",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.35.tgz",
|
||||
"integrity": "sha512-taxjPoNJLajZa3z3JSxwgArRIi5lYy3nlkmemup8bo0AtC7QpKOOE+xQ5wtSXcSMZZMxbsgQHp7FoBTeIUHngA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.4.3",
|
||||
"version": "1.4.9",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -72,8 +72,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.2.16",
|
||||
"@comfyorg/litegraph": "^0.8.29",
|
||||
"@comfyorg/comfyui-electron-types": "^0.3.6",
|
||||
"@comfyorg/litegraph": "^0.8.35",
|
||||
"@primevue/themes": "^4.0.5",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
|
||||
@@ -26,7 +26,10 @@ const betaMenuEnabled = computed(
|
||||
|
||||
const workflowStore = useWorkflowStore()
|
||||
const isUnsavedText = computed(() =>
|
||||
workflowStore.activeWorkflow?.isModified ? ' *' : ''
|
||||
workflowStore.activeWorkflow?.isModified ||
|
||||
!workflowStore.activeWorkflow?.isPersisted
|
||||
? ' *'
|
||||
: ''
|
||||
)
|
||||
const workflowNameText = computed(() => {
|
||||
const workflowName = workflowStore.activeWorkflow?.filename
|
||||
|
||||
102
src/components/common/FormItem.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<!-- A generalized form item for rendering in a form. -->
|
||||
<template>
|
||||
<div class="form-label flex flex-grow items-center">
|
||||
<span class="text-[var(--p-text-muted-color)]">
|
||||
<slot name="name-prefix"></slot>
|
||||
{{ props.item.name }}
|
||||
<i
|
||||
v-if="props.item.tooltip"
|
||||
class="pi pi-info-circle bg-transparent"
|
||||
v-tooltip="props.item.tooltip"
|
||||
/>
|
||||
<slot name="name-suffix"></slot>
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-input flex justify-end">
|
||||
<component
|
||||
:is="markRaw(getFormComponent(props.item))"
|
||||
:id="props.id"
|
||||
v-model:modelValue="formValue"
|
||||
v-bind="getFormAttrs(props.item)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FormItem } from '@/types/settingTypes'
|
||||
import { markRaw, type Component } from 'vue'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import InputNumber from 'primevue/inputnumber'
|
||||
import Select from 'primevue/select'
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import CustomFormValue from '@/components/common/CustomFormValue.vue'
|
||||
import InputSlider from '@/components/common/InputSlider.vue'
|
||||
|
||||
const formValue = defineModel<any>('formValue')
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
item: FormItem
|
||||
id: string | undefined
|
||||
}>(),
|
||||
{
|
||||
id: undefined
|
||||
}
|
||||
)
|
||||
|
||||
function getFormAttrs(item: FormItem) {
|
||||
const attrs = { ...(item.attrs || {}) }
|
||||
const inputType = item.type
|
||||
if (typeof inputType === 'function') {
|
||||
attrs['renderFunction'] = () =>
|
||||
inputType(
|
||||
props.item.name,
|
||||
(v: any) => (formValue.value = v),
|
||||
formValue.value,
|
||||
item.attrs
|
||||
)
|
||||
}
|
||||
switch (item.type) {
|
||||
case 'combo':
|
||||
attrs['options'] =
|
||||
typeof item.options === 'function'
|
||||
? item.options(formValue.value)
|
||||
: item.options
|
||||
if (typeof item.options[0] !== 'string') {
|
||||
attrs['optionLabel'] = 'text'
|
||||
attrs['optionValue'] = 'value'
|
||||
}
|
||||
break
|
||||
}
|
||||
return attrs
|
||||
}
|
||||
|
||||
function getFormComponent(item: FormItem): Component {
|
||||
if (typeof item.type === 'function') {
|
||||
return CustomFormValue
|
||||
}
|
||||
switch (item.type) {
|
||||
case 'boolean':
|
||||
return ToggleSwitch
|
||||
case 'number':
|
||||
return InputNumber
|
||||
case 'slider':
|
||||
return InputSlider
|
||||
case 'combo':
|
||||
return Select
|
||||
default:
|
||||
return InputText
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-input :deep(.input-slider) .p-inputnumber input,
|
||||
.form-input :deep(.input-slider) .slider-part {
|
||||
@apply w-20;
|
||||
}
|
||||
|
||||
.form-input :deep(.p-inputtext),
|
||||
.form-input :deep(.p-select) {
|
||||
@apply w-44;
|
||||
}
|
||||
</style>
|
||||
@@ -20,6 +20,7 @@
|
||||
</template>
|
||||
|
||||
<div class="action-container">
|
||||
<ReportIssueButton v-if="showSendError" :error="props.error" />
|
||||
<FindIssueButton
|
||||
:errorMessage="props.error.exception_message"
|
||||
:repoOwner="repoOwner"
|
||||
@@ -44,9 +45,11 @@ import Divider from 'primevue/divider'
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import FindIssueButton from '@/components/dialog/content/error/FindIssueButton.vue'
|
||||
import ReportIssueButton from '@/components/dialog/content/error/ReportIssueButton.vue'
|
||||
import type { ExecutionErrorWsMessage, SystemStats } from '@/types/apiTypes'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
|
||||
const props = defineProps<{
|
||||
error: ExecutionErrorWsMessage
|
||||
@@ -59,6 +62,7 @@ const reportOpen = ref(false)
|
||||
const showReport = () => {
|
||||
reportOpen.value = true
|
||||
}
|
||||
const showSendError = isElectron()
|
||||
|
||||
const toast = useToast()
|
||||
const { copy, isSupported } = useClipboard()
|
||||
|
||||
@@ -71,6 +71,14 @@
|
||||
</template>
|
||||
</Suspense>
|
||||
</TabPanel>
|
||||
<TabPanel key="server-config" value="Server-Config">
|
||||
<Suspense>
|
||||
<ServerConfigPanel />
|
||||
<template #fallback>
|
||||
<div>Loading server config panel...</div>
|
||||
</template>
|
||||
</Suspense>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</ScrollPanel>
|
||||
@@ -93,6 +101,7 @@ import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import { flattenTree } from '@/utils/treeUtil'
|
||||
import AboutPanel from './setting/AboutPanel.vue'
|
||||
import FirstTimeUIMessage from './setting/FirstTimeUIMessage.vue'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
|
||||
const KeybindingPanel = defineAsyncComponent(
|
||||
() => import('./setting/KeybindingPanel.vue')
|
||||
@@ -100,6 +109,9 @@ const KeybindingPanel = defineAsyncComponent(
|
||||
const ExtensionPanel = defineAsyncComponent(
|
||||
() => import('./setting/ExtensionPanel.vue')
|
||||
)
|
||||
const ServerConfigPanel = defineAsyncComponent(
|
||||
() => import('./setting/ServerConfigPanel.vue')
|
||||
)
|
||||
|
||||
interface ISettingGroup {
|
||||
label: string
|
||||
@@ -124,18 +136,33 @@ const extensionPanelNode: SettingTreeNode = {
|
||||
children: []
|
||||
}
|
||||
|
||||
const serverConfigPanelNode: SettingTreeNode = {
|
||||
key: 'server-config',
|
||||
label: 'Server-Config',
|
||||
children: []
|
||||
}
|
||||
|
||||
const extensionPanelNodeList = computed<SettingTreeNode[]>(() => {
|
||||
const settingStore = useSettingStore()
|
||||
const showExtensionPanel = settingStore.get('Comfy.Settings.ExtensionPanel')
|
||||
return showExtensionPanel ? [extensionPanelNode] : []
|
||||
})
|
||||
|
||||
/**
|
||||
* Server config panel is only available in Electron. We might want to support
|
||||
* it in the web version in the future.
|
||||
*/
|
||||
const serverConfigPanelNodeList = computed<SettingTreeNode[]>(() => {
|
||||
return isElectron() ? [serverConfigPanelNode] : []
|
||||
})
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const settingRoot = computed<SettingTreeNode>(() => settingStore.settingTree)
|
||||
const categories = computed<SettingTreeNode[]>(() => [
|
||||
...(settingRoot.value.children || []),
|
||||
keybindingPanelNode,
|
||||
...extensionPanelNodeList.value,
|
||||
...serverConfigPanelNodeList.value,
|
||||
aboutPanelNode
|
||||
])
|
||||
const activeCategory = ref<SettingTreeNode | null>(null)
|
||||
|
||||
51
src/components/dialog/content/error/ReportIssueButton.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<Button
|
||||
@click="reportIssue"
|
||||
:label="$t('reportIssue')"
|
||||
:severity="submitted ? 'success' : 'secondary'"
|
||||
:icon="icon"
|
||||
:disabled="submitted"
|
||||
v-tooltip="$t('reportIssueTooltip')"
|
||||
>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, defineProps } from 'vue'
|
||||
import Button from 'primevue/button'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { ExecutionErrorWsMessage } from '@/types/apiTypes'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
|
||||
const { error } = defineProps<{
|
||||
error: ExecutionErrorWsMessage
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
const submitting = ref(false)
|
||||
const submitted = ref(false)
|
||||
const icon = computed(
|
||||
() => `pi ${submitting.value ? 'pi-spin pi-spinner' : 'pi-send'}`
|
||||
)
|
||||
|
||||
const reportIssue = async () => {
|
||||
if (submitting.value) return
|
||||
submitting.value = true
|
||||
try {
|
||||
await electronAPI().sendErrorToSentry(error.exception_message, {
|
||||
stackTrace: error.traceback?.join('\n'),
|
||||
nodeType: error.node_type
|
||||
})
|
||||
submitted.value = true
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: t('reportSent'),
|
||||
life: 3000
|
||||
})
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
43
src/components/dialog/content/setting/ServerConfigPanel.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div
|
||||
v-for="([label, items], i) in Object.entries(serverConfigsByCategory)"
|
||||
:key="label"
|
||||
>
|
||||
<Divider v-if="i > 0" />
|
||||
<h3>{{ formatCamelCase(label) }}</h3>
|
||||
<div v-for="item in items" :key="item.name" class="flex items-center mb-4">
|
||||
<FormItem :item="item" v-model:formValue="item.value" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Divider from 'primevue/divider'
|
||||
import FormItem from '@/components/common/FormItem.vue'
|
||||
import { formatCamelCase } from '@/utils/formatUtil'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useServerConfigStore } from '@/stores/serverConfigStore'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, watch } from 'vue'
|
||||
import { SERVER_CONFIG_ITEMS } from '@/constants/serverConfig'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const serverConfigStore = useServerConfigStore()
|
||||
const { serverConfigsByCategory, launchArgs, serverConfigValues } =
|
||||
storeToRefs(serverConfigStore)
|
||||
|
||||
onMounted(() => {
|
||||
serverConfigStore.loadServerConfig(
|
||||
SERVER_CONFIG_ITEMS,
|
||||
settingStore.get('Comfy.Server.ServerConfigValues')
|
||||
)
|
||||
})
|
||||
|
||||
watch(launchArgs, (newVal) => {
|
||||
settingStore.set('Comfy.Server.LaunchArgs', newVal)
|
||||
})
|
||||
|
||||
watch(serverConfigValues, (newVal) => {
|
||||
settingStore.set('Comfy.Server.ServerConfigValues', newVal)
|
||||
})
|
||||
</script>
|
||||
@@ -7,45 +7,15 @@
|
||||
:key="setting.id"
|
||||
class="setting-item flex items-center mb-4"
|
||||
>
|
||||
<div class="setting-label flex flex-grow items-center">
|
||||
<span class="text-[var(--p-text-muted-color)]">
|
||||
<Tag v-if="setting.experimental" :value="$t('experimental')" />
|
||||
<Tag
|
||||
v-if="setting.deprecated"
|
||||
:value="$t('deprecated')"
|
||||
severity="danger" />
|
||||
{{ setting.name }}
|
||||
<i
|
||||
v-if="setting.tooltip"
|
||||
class="pi pi-info-circle bg-transparent"
|
||||
v-tooltip="setting.tooltip"
|
||||
/></span>
|
||||
</div>
|
||||
<div class="setting-input flex justify-end">
|
||||
<component
|
||||
:is="markRaw(getSettingComponent(setting))"
|
||||
:id="setting.id"
|
||||
:modelValue="settingStore.get(setting.id)"
|
||||
@update:modelValue="updateSetting(setting, $event)"
|
||||
v-bind="getSettingAttrs(setting)"
|
||||
/>
|
||||
</div>
|
||||
<SettingItem :setting="setting" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { SettingParams } from '@/types/settingTypes'
|
||||
import { markRaw, type Component } from 'vue'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import InputNumber from 'primevue/inputnumber'
|
||||
import Select from 'primevue/select'
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import Divider from 'primevue/divider'
|
||||
import Tag from 'primevue/tag'
|
||||
import CustomSettingValue from '@/components/dialog/content/setting/CustomSettingValue.vue'
|
||||
import InputSlider from '@/components/common/InputSlider.vue'
|
||||
import SettingItem from '@/components/dialog/content/setting/SettingItem.vue'
|
||||
import { SettingParams } from '@/types/settingTypes'
|
||||
import { formatCamelCase } from '@/utils/formatUtil'
|
||||
|
||||
defineProps<{
|
||||
@@ -55,67 +25,4 @@ defineProps<{
|
||||
}
|
||||
divider?: boolean
|
||||
}>()
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
function getSettingAttrs(setting: SettingParams) {
|
||||
const attrs = { ...(setting.attrs || {}) }
|
||||
const settingType = setting.type
|
||||
if (typeof settingType === 'function') {
|
||||
attrs['renderFunction'] = () =>
|
||||
settingType(
|
||||
setting.name,
|
||||
(v) => updateSetting(setting, v),
|
||||
settingStore.get(setting.id),
|
||||
setting.attrs
|
||||
)
|
||||
}
|
||||
switch (setting.type) {
|
||||
case 'combo':
|
||||
attrs['options'] =
|
||||
typeof setting.options === 'function'
|
||||
? setting.options(settingStore.get(setting.id))
|
||||
: setting.options
|
||||
if (typeof setting.options[0] !== 'string') {
|
||||
attrs['optionLabel'] = 'text'
|
||||
attrs['optionValue'] = 'value'
|
||||
}
|
||||
break
|
||||
}
|
||||
return attrs
|
||||
}
|
||||
|
||||
const updateSetting = (setting: SettingParams, value: any) => {
|
||||
settingStore.set(setting.id, value)
|
||||
}
|
||||
|
||||
function getSettingComponent(setting: SettingParams): Component {
|
||||
if (typeof setting.type === 'function') {
|
||||
return CustomSettingValue
|
||||
}
|
||||
switch (setting.type) {
|
||||
case 'boolean':
|
||||
return ToggleSwitch
|
||||
case 'number':
|
||||
return InputNumber
|
||||
case 'slider':
|
||||
return InputSlider
|
||||
case 'combo':
|
||||
return Select
|
||||
default:
|
||||
return InputText
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.setting-input :deep(.input-slider) .p-inputnumber input,
|
||||
.setting-input :deep(.input-slider) .slider-part {
|
||||
@apply w-20;
|
||||
}
|
||||
|
||||
.setting-input :deep(.p-inputtext),
|
||||
.setting-input :deep(.p-select) {
|
||||
@apply w-44;
|
||||
}
|
||||
</style>
|
||||
|
||||
35
src/components/dialog/content/setting/SettingItem.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<FormItem
|
||||
:item="setting"
|
||||
:id="setting.id"
|
||||
:formValue="settingValue"
|
||||
@update:formValue="updateSettingValue"
|
||||
>
|
||||
<template #name-prefix>
|
||||
<Tag v-if="setting.experimental" :value="$t('experimental')" />
|
||||
<Tag
|
||||
v-if="setting.deprecated"
|
||||
:value="$t('deprecated')"
|
||||
severity="danger"
|
||||
/>
|
||||
</template>
|
||||
</FormItem>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Tag from 'primevue/tag'
|
||||
import FormItem from '@/components/common/FormItem.vue'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { SettingParams } from '@/types/settingTypes'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
setting: SettingParams
|
||||
}>()
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const settingValue = computed(() => settingStore.get(props.setting.id))
|
||||
const updateSettingValue = (value: any) => {
|
||||
settingStore.set(props.setting.id, value)
|
||||
}
|
||||
</script>
|
||||
@@ -182,10 +182,7 @@ watchEffect(() => {
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (comfyApp.graph?.config) {
|
||||
comfyApp.graph.config.alwaysSnapToGrid =
|
||||
settingStore.get('pysssss.SnapToGrid')
|
||||
}
|
||||
LiteGraph.alwaysSnapToGrid = settingStore.get('pysssss.SnapToGrid')
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
|
||||
@@ -163,6 +163,7 @@ const outputFilterPopup = ref(null)
|
||||
|
||||
const ITEMS_PER_PAGE = 8
|
||||
const SCROLL_THRESHOLD = 100 // pixels from bottom to trigger load
|
||||
const MAX_LOAD_ITERATIONS = 10
|
||||
|
||||
const allTasks = computed(() =>
|
||||
isInFolderView.value
|
||||
@@ -181,42 +182,47 @@ const allGalleryItems = computed(() =>
|
||||
)
|
||||
|
||||
const filterTasks = (tasks: TaskItemImpl[]) =>
|
||||
tasks
|
||||
.filter((t) => {
|
||||
if (
|
||||
hideCanceled.value &&
|
||||
t.status?.messages?.at(-1)?.[0] === 'execution_interrupted'
|
||||
) {
|
||||
return false
|
||||
}
|
||||
tasks.filter((t) => {
|
||||
if (
|
||||
hideCanceled.value &&
|
||||
t.status?.messages?.at(-1)?.[0] === 'execution_interrupted'
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
hideCached.value &&
|
||||
t.flatOutputs?.length &&
|
||||
t.flatOutputs.every((o) => o.cached)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (
|
||||
hideCached.value &&
|
||||
t.flatOutputs?.length &&
|
||||
t.flatOutputs.every((o) => o.cached)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
.slice(0, ITEMS_PER_PAGE)
|
||||
return true
|
||||
})
|
||||
|
||||
const loadMoreItems = () => {
|
||||
const loadMoreItems = (iteration: number) => {
|
||||
const currentLength = visibleTasks.value.length
|
||||
const newTasks = filterTasks(allTasks.value).slice(
|
||||
currentLength,
|
||||
currentLength + ITEMS_PER_PAGE
|
||||
)
|
||||
visibleTasks.value.push(...newTasks)
|
||||
// If we've added some items, check if we need to add more
|
||||
// Prevent loading everything at once in case of render update issues
|
||||
if (newTasks.length && iteration < MAX_LOAD_ITERATIONS) {
|
||||
nextTick(() => {
|
||||
checkAndLoadMore(iteration + 1)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const checkAndLoadMore = () => {
|
||||
const checkAndLoadMore = (iteration: number) => {
|
||||
if (!scrollContainer.value) return
|
||||
|
||||
const { scrollHeight, scrollTop, clientHeight } = scrollContainer.value
|
||||
if (scrollHeight - scrollTop - clientHeight < SCROLL_THRESHOLD) {
|
||||
loadMoreItems()
|
||||
loadMoreItems(iteration)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,7 +230,7 @@ useInfiniteScroll(
|
||||
scrollContainer,
|
||||
() => {
|
||||
if (visibleTasks.value.length < allTasks.value.length) {
|
||||
loadMoreItems()
|
||||
loadMoreItems(0)
|
||||
}
|
||||
},
|
||||
{ distance: SCROLL_THRESHOLD }
|
||||
@@ -234,12 +240,16 @@ useInfiniteScroll(
|
||||
// This is necessary as the sidebar tab can change size when user drags the splitter.
|
||||
useResizeObserver(scrollContainer, () => {
|
||||
nextTick(() => {
|
||||
checkAndLoadMore()
|
||||
checkAndLoadMore(0)
|
||||
})
|
||||
})
|
||||
|
||||
const updateVisibleTasks = () => {
|
||||
visibleTasks.value = filterTasks(allTasks.value)
|
||||
visibleTasks.value = filterTasks(allTasks.value).slice(0, ITEMS_PER_PAGE)
|
||||
|
||||
nextTick(() => {
|
||||
checkAndLoadMore(0)
|
||||
})
|
||||
}
|
||||
|
||||
const toggleExpanded = () => {
|
||||
|
||||
@@ -50,10 +50,13 @@
|
||||
<template #node="{ node }">
|
||||
<TreeExplorerTreeNode :node="node">
|
||||
<template #before-label="{ node }">
|
||||
<span v-if="node.data.isModified">*</span>
|
||||
<span v-if="node.data.isModified || !node.data.isPersisted"
|
||||
>*</span
|
||||
>
|
||||
</template>
|
||||
<template #actions="{ node }">
|
||||
<Button
|
||||
class="close-workflow-button"
|
||||
icon="pi pi-times"
|
||||
text
|
||||
:severity="
|
||||
|
||||
@@ -18,7 +18,10 @@
|
||||
<div class="relative">
|
||||
<span
|
||||
class="status-indicator"
|
||||
v-if="!workspaceStore.shiftDown && option.workflow.isModified"
|
||||
v-if="
|
||||
!workspaceStore.shiftDown &&
|
||||
(option.workflow.isModified || !option.workflow.isPersisted)
|
||||
"
|
||||
>•</span
|
||||
>
|
||||
<Button
|
||||
|
||||
@@ -59,7 +59,7 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
||||
key: 's',
|
||||
ctrl: true
|
||||
},
|
||||
commandId: 'Comfy.ExportWorkflow'
|
||||
commandId: 'Comfy.SaveWorkflow'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
@@ -74,13 +74,6 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
||||
},
|
||||
commandId: 'Comfy.ClearWorkflow'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'd',
|
||||
ctrl: true
|
||||
},
|
||||
commandId: 'Comfy.LoadDefaultWorkflow'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'g',
|
||||
@@ -624,5 +624,23 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
versionAdded: '1.3.13'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Server.ServerConfigValues',
|
||||
name: 'Server config values for frontend display',
|
||||
tooltip: 'Server config values used for frontend display only',
|
||||
type: 'hidden',
|
||||
// Mapping from server config id to value.
|
||||
defaultValue: {} as Record<string, any>,
|
||||
versionAdded: '1.4.8'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Server.LaunchArgs',
|
||||
name: 'Server launch arguments',
|
||||
tooltip:
|
||||
'These are the actual arguments that are passed to the server when it is launched.',
|
||||
type: 'hidden',
|
||||
defaultValue: {} as Record<string, string>,
|
||||
versionAdded: '1.4.8'
|
||||
}
|
||||
]
|
||||
432
src/constants/serverConfig.ts
Normal file
@@ -0,0 +1,432 @@
|
||||
import { FormItem } from '@/types/settingTypes'
|
||||
import {
|
||||
LatentPreviewMethod,
|
||||
LogLevel,
|
||||
HashFunction,
|
||||
AutoLaunch,
|
||||
CudaMalloc,
|
||||
FloatingPointPrecision,
|
||||
CrossAttentionMethod,
|
||||
VramManagement
|
||||
} from '@/types/serverArgs'
|
||||
|
||||
export interface ServerConfig<T> extends FormItem {
|
||||
id: string
|
||||
defaultValue: T
|
||||
category?: string[]
|
||||
// Override the default value getter with a custom function.
|
||||
getValue?: (value: T) => Record<string, any>
|
||||
}
|
||||
|
||||
export const WEB_ONLY_CONFIG_ITEMS: ServerConfig<any>[] = [
|
||||
// We only need these settings in the web version. Desktop app manages them already.
|
||||
{
|
||||
id: 'listen',
|
||||
name: 'Host: The IP address to listen on',
|
||||
category: ['Network'],
|
||||
type: 'text',
|
||||
defaultValue: '127.0.0.1'
|
||||
},
|
||||
{
|
||||
id: 'port',
|
||||
name: 'Port: The port to listen on',
|
||||
category: ['Network'],
|
||||
type: 'number',
|
||||
defaultValue: 8188
|
||||
}
|
||||
]
|
||||
|
||||
export const SERVER_CONFIG_ITEMS: ServerConfig<any>[] = [
|
||||
// Network settings
|
||||
{
|
||||
id: 'tls-keyfile',
|
||||
name: 'TLS Key File: Path to TLS key file for HTTPS',
|
||||
category: ['Network'],
|
||||
type: 'text',
|
||||
defaultValue: undefined
|
||||
},
|
||||
{
|
||||
id: 'tls-certfile',
|
||||
name: 'TLS Certificate File: Path to TLS certificate file for HTTPS',
|
||||
category: ['Network'],
|
||||
type: 'text',
|
||||
defaultValue: undefined
|
||||
},
|
||||
{
|
||||
id: 'enable-cors-header',
|
||||
name: 'Enable CORS header: Use "*" for all origins or specify domain',
|
||||
category: ['Network'],
|
||||
type: 'text',
|
||||
defaultValue: undefined
|
||||
},
|
||||
{
|
||||
id: 'max-upload-size',
|
||||
name: 'Maximum upload size (MB)',
|
||||
category: ['Network'],
|
||||
type: 'number',
|
||||
defaultValue: 100
|
||||
},
|
||||
|
||||
// Launch behavior
|
||||
{
|
||||
id: 'auto-launch',
|
||||
name: 'Automatically opens in the browser on startup',
|
||||
category: ['Launch'],
|
||||
type: 'combo',
|
||||
options: Object.values(AutoLaunch),
|
||||
defaultValue: AutoLaunch.Auto,
|
||||
getValue: (value: AutoLaunch) => {
|
||||
switch (value) {
|
||||
case AutoLaunch.Auto:
|
||||
return {}
|
||||
case AutoLaunch.Enable:
|
||||
return {
|
||||
['auto-launch']: true
|
||||
}
|
||||
case AutoLaunch.Disable:
|
||||
return {
|
||||
['disable-auto-launch']: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// CUDA settings
|
||||
{
|
||||
id: 'cuda-device',
|
||||
name: 'CUDA device index to use',
|
||||
category: ['CUDA'],
|
||||
type: 'number',
|
||||
defaultValue: undefined
|
||||
},
|
||||
{
|
||||
id: 'cuda-malloc',
|
||||
name: 'Use CUDA malloc for memory allocation',
|
||||
category: ['CUDA'],
|
||||
type: 'combo',
|
||||
options: Object.values(CudaMalloc),
|
||||
defaultValue: CudaMalloc.Auto,
|
||||
getValue: (value: CudaMalloc) => {
|
||||
switch (value) {
|
||||
case CudaMalloc.Auto:
|
||||
return {}
|
||||
case CudaMalloc.Enable:
|
||||
return {
|
||||
['cuda-malloc']: true
|
||||
}
|
||||
case CudaMalloc.Disable:
|
||||
return {
|
||||
['disable-cuda-malloc']: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Precision settings
|
||||
{
|
||||
id: 'global-precision',
|
||||
name: 'Global floating point precision',
|
||||
category: ['Inference'],
|
||||
type: 'combo',
|
||||
options: [
|
||||
FloatingPointPrecision.AUTO,
|
||||
FloatingPointPrecision.FP32,
|
||||
FloatingPointPrecision.FP16
|
||||
],
|
||||
defaultValue: FloatingPointPrecision.AUTO,
|
||||
tooltip: 'Global floating point precision',
|
||||
getValue: (value: FloatingPointPrecision) => {
|
||||
switch (value) {
|
||||
case FloatingPointPrecision.AUTO:
|
||||
return {}
|
||||
case FloatingPointPrecision.FP32:
|
||||
return {
|
||||
['force-fp32']: true
|
||||
}
|
||||
case FloatingPointPrecision.FP16:
|
||||
return {
|
||||
['force-fp16']: true
|
||||
}
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// UNET precision
|
||||
{
|
||||
id: 'unet-precision',
|
||||
name: 'UNET precision',
|
||||
category: ['Inference'],
|
||||
type: 'combo',
|
||||
options: [
|
||||
FloatingPointPrecision.AUTO,
|
||||
FloatingPointPrecision.FP16,
|
||||
FloatingPointPrecision.BF16,
|
||||
FloatingPointPrecision.FP8E4M3FN,
|
||||
FloatingPointPrecision.FP8E5M2
|
||||
],
|
||||
defaultValue: FloatingPointPrecision.AUTO,
|
||||
tooltip: 'UNET precision',
|
||||
getValue: (value: FloatingPointPrecision) => {
|
||||
switch (value) {
|
||||
case FloatingPointPrecision.AUTO:
|
||||
return {}
|
||||
default:
|
||||
return {
|
||||
[`${value.toLowerCase()}-unet`]: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// VAE settings
|
||||
{
|
||||
id: 'vae-precision',
|
||||
name: 'VAE precision',
|
||||
category: ['Inference'],
|
||||
type: 'combo',
|
||||
options: [
|
||||
FloatingPointPrecision.AUTO,
|
||||
FloatingPointPrecision.FP16,
|
||||
FloatingPointPrecision.FP32,
|
||||
FloatingPointPrecision.BF16
|
||||
],
|
||||
defaultValue: FloatingPointPrecision.AUTO,
|
||||
tooltip: 'VAE precision',
|
||||
getValue: (value: FloatingPointPrecision) => {
|
||||
switch (value) {
|
||||
case FloatingPointPrecision.AUTO:
|
||||
return {}
|
||||
default:
|
||||
return {
|
||||
[`${value.toLowerCase()}-vae`]: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'cpu-vae',
|
||||
name: 'Run VAE on CPU',
|
||||
category: ['Inference'],
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
|
||||
// Text Encoder settings
|
||||
{
|
||||
id: 'text-encoder-precision',
|
||||
name: 'Text Encoder precision',
|
||||
category: ['Inference'],
|
||||
type: 'combo',
|
||||
options: [
|
||||
FloatingPointPrecision.AUTO,
|
||||
FloatingPointPrecision.FP8E4M3FN,
|
||||
FloatingPointPrecision.FP8E5M2,
|
||||
FloatingPointPrecision.FP16,
|
||||
FloatingPointPrecision.FP32
|
||||
],
|
||||
defaultValue: FloatingPointPrecision.AUTO,
|
||||
tooltip: 'Text Encoder precision',
|
||||
getValue: (value: FloatingPointPrecision) => {
|
||||
switch (value) {
|
||||
case FloatingPointPrecision.AUTO:
|
||||
return {}
|
||||
default:
|
||||
return {
|
||||
[`${value.toLowerCase()}-text-enc`]: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Memory and performance settings
|
||||
{
|
||||
id: 'force-channels-last',
|
||||
name: 'Force channels-last memory format',
|
||||
category: ['Memory'],
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: 'directml',
|
||||
name: 'DirectML device index',
|
||||
category: ['Memory'],
|
||||
type: 'number',
|
||||
defaultValue: undefined
|
||||
},
|
||||
{
|
||||
id: 'disable-ipex-optimize',
|
||||
name: 'Disable IPEX optimization',
|
||||
category: ['Memory'],
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
|
||||
// Preview settings
|
||||
{
|
||||
id: 'preview-method',
|
||||
name: 'Method used for latent previews',
|
||||
category: ['Preview'],
|
||||
type: 'combo',
|
||||
options: Object.values(LatentPreviewMethod),
|
||||
defaultValue: LatentPreviewMethod.NoPreviews
|
||||
},
|
||||
{
|
||||
id: 'preview-size',
|
||||
name: 'Size of preview images',
|
||||
category: ['Preview'],
|
||||
type: 'slider',
|
||||
defaultValue: 512,
|
||||
attrs: {
|
||||
min: 128,
|
||||
max: 2048,
|
||||
step: 128
|
||||
}
|
||||
},
|
||||
|
||||
// Cache settings
|
||||
{
|
||||
id: 'cache-classic',
|
||||
name: 'Use classic cache system',
|
||||
category: ['Cache'],
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: 'cache-lru',
|
||||
name: 'Use LRU caching with a maximum of N node results cached. (0 to disable).',
|
||||
category: ['Cache'],
|
||||
type: 'number',
|
||||
defaultValue: 0,
|
||||
tooltip: 'May use more RAM/VRAM.'
|
||||
},
|
||||
|
||||
// Attention settings
|
||||
{
|
||||
id: 'cross-attention-method',
|
||||
name: 'Cross attention method',
|
||||
category: ['Attention'],
|
||||
type: 'combo',
|
||||
options: Object.values(CrossAttentionMethod),
|
||||
defaultValue: CrossAttentionMethod.Auto,
|
||||
getValue: (value: CrossAttentionMethod) => {
|
||||
switch (value) {
|
||||
case CrossAttentionMethod.Auto:
|
||||
return {}
|
||||
default:
|
||||
return {
|
||||
[`use-${value.toLowerCase()}-cross-attention`]: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'disable-xformers',
|
||||
name: 'Disable xFormers optimization',
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: 'force-upcast-attention',
|
||||
name: 'Force attention upcast',
|
||||
category: ['Attention'],
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: 'dont-upcast-attention',
|
||||
name: 'Prevent attention upcast',
|
||||
category: ['Attention'],
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
|
||||
// VRAM management
|
||||
{
|
||||
id: 'vram-management',
|
||||
name: 'VRAM management mode',
|
||||
category: ['Memory'],
|
||||
type: 'combo',
|
||||
options: Object.values(VramManagement),
|
||||
defaultValue: VramManagement.Auto,
|
||||
getValue: (value: VramManagement) => {
|
||||
switch (value) {
|
||||
case VramManagement.Auto:
|
||||
return {}
|
||||
default:
|
||||
return {
|
||||
[value]: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'reserve-vram',
|
||||
name: 'Reserved VRAM (GB)',
|
||||
category: ['Memory'],
|
||||
type: 'number',
|
||||
defaultValue: undefined,
|
||||
tooltip:
|
||||
'Set the amount of vram in GB you want to reserve for use by your OS/other software. By default some amount is reverved depending on your OS.'
|
||||
},
|
||||
|
||||
// Misc settings
|
||||
{
|
||||
id: 'default-hashing-function',
|
||||
name: 'Default hashing function for model files',
|
||||
type: 'combo',
|
||||
options: Object.values(HashFunction),
|
||||
defaultValue: HashFunction.SHA256
|
||||
},
|
||||
{
|
||||
id: 'disable-smart-memory',
|
||||
name: 'Force ComfyUI to agressively offload to regular ram instead of keeping models in vram when it can.',
|
||||
category: ['Memory'],
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: 'deterministic',
|
||||
name: 'Make pytorch use slower deterministic algorithms when it can.',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
tooltip: 'Note that this might not make images deterministic in all cases.'
|
||||
},
|
||||
{
|
||||
id: 'fast',
|
||||
name: 'Enable some untested and potentially quality deteriorating optimizations.',
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: 'dont-print-server',
|
||||
name: "Don't print server output to console.",
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: 'disable-metadata',
|
||||
name: 'Disable saving prompt metadata in files.',
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: 'disable-all-custom-nodes',
|
||||
name: 'Disable loading all custom nodes.',
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: 'log-level',
|
||||
name: 'Logging verbosity level',
|
||||
type: 'combo',
|
||||
options: Object.values(LogLevel),
|
||||
defaultValue: LogLevel.INFO,
|
||||
getValue: (value: LogLevel) => {
|
||||
return {
|
||||
verbose: value
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -5,6 +5,14 @@ import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
|
||||
|
||||
const electronAPI = getElectronAPI()
|
||||
const desktopAppVersion = await electronAPI.getElectronVersion()
|
||||
|
||||
const onChangeRestartApp = (newValue: string, oldValue: string) => {
|
||||
// Add a delay to allow changes to take effect before restarting.
|
||||
if (oldValue !== undefined && newValue !== oldValue) {
|
||||
electronAPI.restartApp('Restart ComfyUI to apply changes.', 1500)
|
||||
}
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: 'Comfy.ElectronAdapter',
|
||||
settings: [
|
||||
@@ -14,29 +22,15 @@ import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
|
||||
name: 'Automatically check for updates',
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
onChange(newValue, oldValue) {
|
||||
if (oldValue !== undefined && newValue !== oldValue) {
|
||||
electronAPI.restartApp(
|
||||
'Restart ComfyUI to apply changes.',
|
||||
1500 // add delay to allow changes to take effect before restarting.
|
||||
)
|
||||
}
|
||||
}
|
||||
onChange: onChangeRestartApp
|
||||
},
|
||||
{
|
||||
id: 'Comfy-Desktop.SendStatistics',
|
||||
category: ['Comfy-Desktop', 'General', 'Send Statistics'],
|
||||
name: 'Send anonymous usage statistics',
|
||||
name: 'Send anonymous crash reports',
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
onChange(newValue, oldValue) {
|
||||
if (oldValue !== undefined && newValue !== oldValue) {
|
||||
electronAPI.restartApp(
|
||||
'Restart ComfyUI to apply changes.',
|
||||
1500 // add delay to allow changes to take effect before restarting.
|
||||
)
|
||||
}
|
||||
}
|
||||
onChange: onChangeRestartApp
|
||||
}
|
||||
],
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { FitAddon } from '@xterm/addon-fit'
|
||||
import { Terminal } from '@xterm/xterm'
|
||||
import { debounce } from 'lodash'
|
||||
import { onMounted, onUnmounted, Ref } from 'vue'
|
||||
import { markRaw, onMounted, onUnmounted, Ref } from 'vue'
|
||||
import '@xterm/xterm/css/xterm.css'
|
||||
|
||||
export function useTerminal(element: Ref<HTMLElement>) {
|
||||
const fitAddon = new FitAddon()
|
||||
const terminal = new Terminal({
|
||||
convertEol: true
|
||||
})
|
||||
const terminal = markRaw(
|
||||
new Terminal({
|
||||
convertEol: true
|
||||
})
|
||||
)
|
||||
terminal.loadAddon(fitAddon)
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
39
src/i18n.ts
@@ -15,7 +15,7 @@ const messages = {
|
||||
failedToSelectDirectory: 'Failed to select directory',
|
||||
pathValidationFailed: 'Failed to validate path',
|
||||
installLocationDescription:
|
||||
"Select the directory for ComfyUI's user data. A python environment will be installed to the selected location. Please make sure the selected disk has enough space (~5GB) left.",
|
||||
"Select the directory for ComfyUI's user data. A python environment will be installed to the selected location. Please make sure the selected disk has enough space (~15GB) left.",
|
||||
installLocationTooltip:
|
||||
"ComfyUI's user data directory. Stores:\n- Python Environment\n- Models\n- Custom nodes\n",
|
||||
appDataLocationTooltip:
|
||||
@@ -33,24 +33,38 @@ const messages = {
|
||||
'Configure how ComfyUI behaves on your desktop. You can change these settings later.',
|
||||
settings: {
|
||||
autoUpdate: 'Automatic Updates',
|
||||
allowMetrics: 'Usage Analytics',
|
||||
allowMetrics: 'Crash Reports',
|
||||
autoUpdateDescription:
|
||||
"Automatically download and install updates when they become available. You'll always be notified before updates are installed.",
|
||||
allowMetricsDescription:
|
||||
'Help improve ComfyUI by sending anonymous usage data. No personal information or workflow content will be collected.',
|
||||
'Help improve ComfyUI by sending anonymous crash reports. No personal information or workflow content will be collected. This can be disabled at any time in the settings menu.',
|
||||
learnMoreAboutData: 'Learn more about data collection',
|
||||
dataCollectionDialog: {
|
||||
title: 'About Data Collection',
|
||||
whatWeCollect: 'What we collect:',
|
||||
whatWeDoNotCollect: "What we don't collect:",
|
||||
errorReports: 'Error reports',
|
||||
systemInfo: 'Operating system and app version',
|
||||
errorReports: 'Error message and stack trace',
|
||||
systemInfo: 'Hardware, OS type, and app version',
|
||||
personalInformation: 'Personal information',
|
||||
workflowContent: 'Workflow content',
|
||||
fileSystemInformation: 'File system information'
|
||||
fileSystemInformation: 'File system information',
|
||||
workflowContents: 'Workflow contents',
|
||||
customNodeConfigurations: 'Custom node configurations'
|
||||
}
|
||||
}
|
||||
},
|
||||
serverStart: {
|
||||
reinstall: 'Reinstall',
|
||||
reportIssue: 'Report Issue',
|
||||
openLogs: 'Open Logs',
|
||||
process: {
|
||||
'initial-state': 'Loading...',
|
||||
'python-setup': 'Setting up Python Environment...',
|
||||
'starting-server': 'Starting ComfyUI server...',
|
||||
ready: 'Finishing...',
|
||||
error: 'Unable to start ComfyUI'
|
||||
}
|
||||
},
|
||||
firstTimeUIMessage:
|
||||
'This is the first time you use the new UI. Choose "Menu > Use New Menu > Disabled" to restore the old UI.',
|
||||
download: 'Download',
|
||||
@@ -83,6 +97,9 @@ const messages = {
|
||||
error: 'Error',
|
||||
loading: 'Loading',
|
||||
findIssues: 'Find Issues',
|
||||
reportIssue: 'Send Report',
|
||||
reportIssueTooltip: 'Submit the error report to Comfy Org',
|
||||
reportSent: 'Report Submitted',
|
||||
copyToClipboard: 'Copy to Clipboard',
|
||||
openNewIssue: 'Open New Issue',
|
||||
showReport: 'Show Report',
|
||||
@@ -148,8 +165,8 @@ const messages = {
|
||||
'The workflow will be queued instantly after a generation finishes',
|
||||
change: 'On Change',
|
||||
changeTooltip: 'The workflow will be queued once a change is made',
|
||||
queueWorkflow: 'Queue workflow',
|
||||
queueWorkflowFront: 'Queue workflow (Insert at Front)',
|
||||
queueWorkflow: 'Queue workflow (Shift to queue at front)',
|
||||
queueWorkflowFront: 'Queue workflow at front',
|
||||
queue: 'Queue',
|
||||
interrupt: 'Cancel current run',
|
||||
refresh: 'Refresh node definitions',
|
||||
@@ -275,8 +292,8 @@ const messages = {
|
||||
instantTooltip: '工作流将会在生成完成后立即执行',
|
||||
change: '变动',
|
||||
changeTooltip: '工作流将会在改变后执行',
|
||||
queueWorkflow: '执行工作流',
|
||||
queueWorkflowFront: '执行工作流 (队列首)',
|
||||
queueWorkflow: '执行 (Shift 执行到队列首)',
|
||||
queueWorkflowFront: '执行到队列首',
|
||||
queue: '队列',
|
||||
interrupt: '取消当前任务',
|
||||
refresh: '刷新节点',
|
||||
@@ -396,7 +413,7 @@ const messages = {
|
||||
change: 'При изменении',
|
||||
changeTooltip:
|
||||
'Рабочий процесс будет поставлен в очередь после внесения изменений',
|
||||
queueWorkflow: 'Очередь рабочего процесса',
|
||||
queueWorkflow: 'Очередь рабочего процесса (Shift для вставки спереди)',
|
||||
queueWorkflowFront: 'Очередь рабочего процесса (Вставка спереди)',
|
||||
queue: 'Очередь',
|
||||
interrupt: 'Отменить текущее выполнение',
|
||||
|
||||
@@ -10,15 +10,7 @@ import _ from 'lodash'
|
||||
import * as jsondiffpatch from 'jsondiffpatch'
|
||||
import log from 'loglevel'
|
||||
|
||||
function clone(obj: any) {
|
||||
try {
|
||||
if (typeof structuredClone !== 'undefined') {
|
||||
return structuredClone(obj)
|
||||
}
|
||||
} catch (error) {
|
||||
// structuredClone is stricter than using JSON.parse/stringify so fallback to that
|
||||
}
|
||||
|
||||
function clone<T>(obj: T): T {
|
||||
return JSON.parse(JSON.stringify(obj))
|
||||
}
|
||||
|
||||
@@ -69,7 +61,7 @@ export class ChangeTracker {
|
||||
if (this.restoringState) return
|
||||
|
||||
logger.debug('Reset State')
|
||||
this.activeState = state ?? this.activeState
|
||||
if (state) this.activeState = clone(state)
|
||||
this.initialState = clone(this.activeState)
|
||||
}
|
||||
|
||||
@@ -91,6 +83,10 @@ export class ChangeTracker {
|
||||
}
|
||||
|
||||
updateModified() {
|
||||
api.dispatchEvent(
|
||||
new CustomEvent('graphChanged', { detail: this.activeState })
|
||||
)
|
||||
|
||||
// Get the workflow from the store as ChangeTracker is raw object, i.e.
|
||||
// `this.workflow` is not reactive.
|
||||
const workflow = useWorkflowStore().getWorkflowByPath(this.workflow.path)
|
||||
@@ -112,9 +108,9 @@ export class ChangeTracker {
|
||||
checkState() {
|
||||
if (!this.app.graph || this.changeCount) return
|
||||
// @ts-expect-error zod types issue. Will be fixed after we enable ts-strict
|
||||
const currentState = this.app.graph.serialize() as ComfyWorkflowJSON
|
||||
const currentState = clone(this.app.graph.serialize()) as ComfyWorkflowJSON
|
||||
if (!this.activeState) {
|
||||
this.activeState = clone(currentState)
|
||||
this.activeState = currentState
|
||||
return
|
||||
}
|
||||
if (!ChangeTracker.graphEqual(this.activeState, currentState)) {
|
||||
@@ -124,11 +120,8 @@ export class ChangeTracker {
|
||||
}
|
||||
logger.debug('Diff detected. Undo queue length:', this.undoQueue.length)
|
||||
|
||||
this.activeState = clone(currentState)
|
||||
this.activeState = currentState
|
||||
this.redoQueue.length = 0
|
||||
api.dispatchEvent(
|
||||
new CustomEvent('graphChanged', { detail: this.activeState })
|
||||
)
|
||||
this.updateModified()
|
||||
}
|
||||
}
|
||||
@@ -136,7 +129,7 @@ export class ChangeTracker {
|
||||
async updateState(source: ComfyWorkflowJSON[], target: ComfyWorkflowJSON[]) {
|
||||
const prevState = source.pop()
|
||||
if (prevState) {
|
||||
target.push(this.activeState!)
|
||||
target.push(this.activeState)
|
||||
this.restoringState = true
|
||||
try {
|
||||
await this.app.loadGraphData(prevState, false, false, this.workflow, {
|
||||
|
||||
@@ -63,17 +63,36 @@ export const workflowService = {
|
||||
|
||||
const newPath = workflow.directory + '/' + appendJsonExt(newFilename)
|
||||
const newKey = newPath.substring(ComfyWorkflow.basePath.length)
|
||||
const workflowStore = useWorkflowStore()
|
||||
const existingWorkflow = workflowStore.getWorkflowByPath(newPath)
|
||||
|
||||
if (existingWorkflow) {
|
||||
const res = (await ComfyAsyncDialog.prompt({
|
||||
title: 'Overwrite existing file?',
|
||||
message: `"${newPath}" already exists. Do you want to overwrite it?`,
|
||||
actions: ['Yes', 'No']
|
||||
})) as 'Yes' | 'No'
|
||||
|
||||
if (res === 'No') return
|
||||
|
||||
if (existingWorkflow.path === workflow.path) {
|
||||
await this.saveWorkflow(workflow)
|
||||
return
|
||||
}
|
||||
const deleted = await this.deleteWorkflow(existingWorkflow)
|
||||
if (!deleted) return
|
||||
}
|
||||
|
||||
if (workflow.isTemporary) {
|
||||
await this.renameWorkflow(workflow, newPath)
|
||||
await useWorkflowStore().saveWorkflow(workflow)
|
||||
await workflowStore.saveWorkflow(workflow)
|
||||
} else {
|
||||
const tempWorkflow = useWorkflowStore().createTemporary(
|
||||
const tempWorkflow = workflowStore.createTemporary(
|
||||
newKey,
|
||||
workflow.activeState as ComfyWorkflowJSON
|
||||
)
|
||||
await this.openWorkflow(tempWorkflow)
|
||||
await useWorkflowStore().saveWorkflow(tempWorkflow)
|
||||
await workflowStore.saveWorkflow(tempWorkflow)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -131,9 +150,9 @@ export const workflowService = {
|
||||
async closeWorkflow(
|
||||
workflow: ComfyWorkflow,
|
||||
options: { warnIfUnsaved: boolean } = { warnIfUnsaved: true }
|
||||
): Promise<void> {
|
||||
): Promise<boolean> {
|
||||
if (!workflow.isLoaded) {
|
||||
return
|
||||
return true
|
||||
}
|
||||
|
||||
if (workflow.isModified && options.warnIfUnsaved) {
|
||||
@@ -146,7 +165,7 @@ export const workflowService = {
|
||||
if (res === 'Yes') {
|
||||
await this.saveWorkflow(workflow)
|
||||
} else if (res === 'Cancel') {
|
||||
return
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,18 +180,26 @@ export const workflowService = {
|
||||
}
|
||||
|
||||
await workflowStore.closeWorkflow(workflow)
|
||||
return true
|
||||
},
|
||||
|
||||
async renameWorkflow(workflow: ComfyWorkflow, newPath: string) {
|
||||
await useWorkflowStore().renameWorkflow(workflow, newPath)
|
||||
},
|
||||
|
||||
async deleteWorkflow(workflow: ComfyWorkflow) {
|
||||
/**
|
||||
* Delete a workflow
|
||||
* @param workflow The workflow to delete
|
||||
* @returns true if the workflow was deleted, false if the user cancelled
|
||||
*/
|
||||
async deleteWorkflow(workflow: ComfyWorkflow): Promise<boolean> {
|
||||
const workflowStore = useWorkflowStore()
|
||||
if (workflowStore.isOpen(workflow)) {
|
||||
await this.closeWorkflow(workflow)
|
||||
const closed = await this.closeWorkflow(workflow)
|
||||
if (!closed) return false
|
||||
}
|
||||
await workflowStore.deleteWorkflow(workflow)
|
||||
return true
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -493,6 +493,24 @@ export const useCommandStore = defineStore('command', () => {
|
||||
function: () => {
|
||||
useWorkspaceStore().toggleFocusMode()
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Graph.FitGroupToContents',
|
||||
icon: 'pi pi-expand',
|
||||
label: 'Fit Group To Contents',
|
||||
versionAdded: '1.4.9',
|
||||
function: () => {
|
||||
for (const group of app.canvas.selectedItems) {
|
||||
if (group instanceof LGraphGroup) {
|
||||
group.recomputeInsideNodes()
|
||||
const padding = useSettingStore().get(
|
||||
'Comfy.GroupSelectedNodes.Padding'
|
||||
)
|
||||
group.resizeTo(group.children, padding)
|
||||
app.graph.change()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { isElectron, electronAPI } from '@/utils/envUtil'
|
||||
import type {
|
||||
DownloadState,
|
||||
DownloadStatus
|
||||
} from '@comfyorg/comfyui-electron-types'
|
||||
|
||||
export interface ElectronDownload {
|
||||
url: string
|
||||
status: 'paused' | 'in_progress' | 'cancelled'
|
||||
progress: number
|
||||
savePath: string
|
||||
filename: string
|
||||
export interface ElectronDownload
|
||||
extends Pick<DownloadState, 'url' | 'filename'> {
|
||||
progress?: number
|
||||
savePath?: string
|
||||
status?: DownloadStatus
|
||||
}
|
||||
|
||||
/** Electron donwloads store handler */
|
||||
@@ -20,15 +23,14 @@ export const useElectronDownloadStore = defineStore('downloads', () => {
|
||||
|
||||
const initialize = async () => {
|
||||
if (isElectron()) {
|
||||
const allDownloads: ElectronDownload[] =
|
||||
(await DownloadManager.getAllDownloads()) as unknown as ElectronDownload[]
|
||||
const allDownloads = await DownloadManager.getAllDownloads()
|
||||
|
||||
for (const download of allDownloads) {
|
||||
downloads.value.push(download)
|
||||
}
|
||||
|
||||
// ToDO: replace with ElectronDownload type
|
||||
DownloadManager.onDownloadProgress((data: any) => {
|
||||
DownloadManager.onDownloadProgress((data) => {
|
||||
if (!findByUrl(data.url)) {
|
||||
downloads.value.push(data)
|
||||
}
|
||||
@@ -51,8 +53,11 @@ export const useElectronDownloadStore = defineStore('downloads', () => {
|
||||
url,
|
||||
savePath,
|
||||
filename
|
||||
}: Pick<ElectronDownload, 'url' | 'savePath' | 'filename'>) =>
|
||||
DownloadManager.startDownload(url, savePath, filename)
|
||||
}: {
|
||||
url: string
|
||||
savePath: string
|
||||
filename: string
|
||||
}) => DownloadManager.startDownload(url, savePath, filename)
|
||||
const pause = (url: string) => DownloadManager.pauseDownload(url)
|
||||
const resume = (url: string) => DownloadManager.resumeDownload(url)
|
||||
const cancel = (url: string) => DownloadManager.cancelDownload(url)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { defineStore } from 'pinia'
|
||||
import { computed, Ref, ref, toRaw } from 'vue'
|
||||
import { Keybinding, KeyCombo } from '@/types/keyBindingTypes'
|
||||
import { useSettingStore } from './settingStore'
|
||||
import { CORE_KEYBINDINGS } from './coreKeybindings'
|
||||
import { CORE_KEYBINDINGS } from '@/constants/coreKeybindings'
|
||||
import type { ComfyExtension } from '@/types/comfy'
|
||||
|
||||
export class KeybindingImpl implements Keybinding {
|
||||
@@ -121,8 +121,7 @@ export const useKeybindingStore = defineStore('keybinding', () => {
|
||||
|
||||
const keybindingByKeyCombo = computed<Record<string, KeybindingImpl>>(() => {
|
||||
const result: Record<string, KeybindingImpl> = {
|
||||
...defaultKeybindings.value,
|
||||
...userKeybindings.value
|
||||
...defaultKeybindings.value
|
||||
}
|
||||
|
||||
for (const keybinding of Object.values(userUnsetKeybindings.value)) {
|
||||
@@ -131,7 +130,11 @@ export const useKeybindingStore = defineStore('keybinding', () => {
|
||||
delete result[serializedCombo]
|
||||
}
|
||||
}
|
||||
return result
|
||||
|
||||
return {
|
||||
...result,
|
||||
...userKeybindings.value
|
||||
}
|
||||
})
|
||||
|
||||
const keybindings = computed<KeybindingImpl[]>(() =>
|
||||
@@ -218,7 +221,10 @@ export const useKeybindingStore = defineStore('keybinding', () => {
|
||||
function unsetKeybinding(keybinding: KeybindingImpl) {
|
||||
const serializedCombo = keybinding.combo.serialize()
|
||||
if (!(serializedCombo in keybindingByKeyCombo.value)) {
|
||||
throw new Error(`Keybinding on ${keybinding.combo} does not exist`)
|
||||
console.warn(
|
||||
`Trying to unset non-exist keybinding: ${JSON.stringify(keybinding)}`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (userKeybindings.value[serializedCombo]?.equals(keybinding)) {
|
||||
@@ -231,7 +237,7 @@ export const useKeybindingStore = defineStore('keybinding', () => {
|
||||
return
|
||||
}
|
||||
|
||||
throw new Error(`NOT_REACHED`)
|
||||
throw new Error(`Unknown keybinding: ${JSON.stringify(keybinding)}`)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
73
src/stores/serverConfigStore.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { ServerConfig } from '@/constants/serverConfig'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
export type ServerConfigWithValue<T> = ServerConfig<T> & {
|
||||
value: T
|
||||
}
|
||||
|
||||
export const useServerConfigStore = defineStore('serverConfig', () => {
|
||||
const serverConfigById = ref<Record<string, ServerConfigWithValue<any>>>({})
|
||||
const serverConfigs = computed(() => {
|
||||
return Object.values(serverConfigById.value)
|
||||
})
|
||||
const serverConfigsByCategory = computed<
|
||||
Record<string, ServerConfigWithValue<any>[]>
|
||||
>(() => {
|
||||
return serverConfigs.value.reduce(
|
||||
(acc, config) => {
|
||||
const category = config.category?.[0] ?? 'General'
|
||||
acc[category] = acc[category] || []
|
||||
acc[category].push(config)
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, ServerConfigWithValue<any>[]>
|
||||
)
|
||||
})
|
||||
const serverConfigValues = computed<Record<string, any>>(() => {
|
||||
return Object.fromEntries(
|
||||
serverConfigs.value.map((config) => {
|
||||
return [
|
||||
config.id,
|
||||
config.value === config.defaultValue || !config.value
|
||||
? undefined
|
||||
: config.value
|
||||
]
|
||||
})
|
||||
)
|
||||
})
|
||||
const launchArgs = computed<Record<string, string>>(() => {
|
||||
return Object.assign(
|
||||
{},
|
||||
...serverConfigs.value.map((config) => {
|
||||
if (config.value === config.defaultValue || !config.value) {
|
||||
return {}
|
||||
}
|
||||
return config.getValue
|
||||
? config.getValue(config.value)
|
||||
: { [config.id]: config.value }
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
function loadServerConfig(
|
||||
configs: ServerConfig<any>[],
|
||||
values: Record<string, any>
|
||||
) {
|
||||
for (const config of configs) {
|
||||
serverConfigById.value[config.id] = {
|
||||
...config,
|
||||
value: values[config.id] ?? config.defaultValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
serverConfigById,
|
||||
serverConfigs,
|
||||
serverConfigsByCategory,
|
||||
serverConfigValues,
|
||||
launchArgs,
|
||||
loadServerConfig
|
||||
}
|
||||
})
|
||||
@@ -16,7 +16,7 @@ import type { SettingParams } from '@/types/settingTypes'
|
||||
import type { TreeNode } from 'primevue/treenode'
|
||||
import type { ComfyExtension } from '@/types/comfy'
|
||||
import { buildTree } from '@/utils/treeUtil'
|
||||
import { CORE_SETTINGS } from '@/stores/coreSettings'
|
||||
import { CORE_SETTINGS } from '@/constants/coreSettings'
|
||||
|
||||
export interface SettingTreeNode extends TreeNode {
|
||||
data?: SettingParams
|
||||
|
||||
@@ -526,7 +526,9 @@ const zSettings = z.record(z.any()).and(
|
||||
'Comfy.Settings.ExtensionPanel': z.boolean(),
|
||||
'Comfy.LinkRenderMode': z.number(),
|
||||
'Comfy.Node.AutoSnapLinkToSlot': z.boolean(),
|
||||
'Comfy.Node.SnapHighlightsNode': z.boolean()
|
||||
'Comfy.Node.SnapHighlightsNode': z.boolean(),
|
||||
'Comfy.Server.ServerConfigValues': z.record(z.string(), z.any()),
|
||||
'Comfy.Server.LaunchArgs': z.record(z.string(), z.string())
|
||||
})
|
||||
.optional()
|
||||
)
|
||||
|
||||
65
src/types/serverArgs.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
export enum LatentPreviewMethod {
|
||||
NoPreviews = 'none',
|
||||
Auto = 'auto',
|
||||
Latent2RGB = 'latent2rgb',
|
||||
TAESD = 'taesd'
|
||||
}
|
||||
|
||||
export enum LogLevel {
|
||||
DEBUG = 'DEBUG',
|
||||
INFO = 'INFO',
|
||||
WARNING = 'WARNING',
|
||||
ERROR = 'ERROR',
|
||||
CRITICAL = 'CRITICAL'
|
||||
}
|
||||
|
||||
export enum HashFunction {
|
||||
MD5 = 'md5',
|
||||
SHA1 = 'sha1',
|
||||
SHA256 = 'sha256',
|
||||
SHA512 = 'sha512'
|
||||
}
|
||||
|
||||
export enum AutoLaunch {
|
||||
// Let server decide whether to auto launch based on the current environment
|
||||
Auto = 'auto',
|
||||
// Disable auto launch
|
||||
Disable = 'disable',
|
||||
// Enable auto launch
|
||||
Enable = 'enable'
|
||||
}
|
||||
|
||||
export enum CudaMalloc {
|
||||
// Let server decide whether to use CUDA malloc based on the current environment
|
||||
Auto = 'auto',
|
||||
// Disable CUDA malloc
|
||||
Disable = 'disable',
|
||||
// Enable CUDA malloc
|
||||
Enable = 'enable'
|
||||
}
|
||||
|
||||
export enum FloatingPointPrecision {
|
||||
AUTO = 'auto',
|
||||
FP32 = 'fp32',
|
||||
FP16 = 'fp16',
|
||||
BF16 = 'bf16',
|
||||
FP8E4M3FN = 'fp8_e4m3fn',
|
||||
FP8E5M2 = 'fp8_e5m2'
|
||||
}
|
||||
|
||||
export enum CrossAttentionMethod {
|
||||
Auto = 'auto',
|
||||
Split = 'split',
|
||||
Quad = 'quad',
|
||||
Pytorch = 'pytorch'
|
||||
}
|
||||
|
||||
export enum VramManagement {
|
||||
Auto = 'auto',
|
||||
GPUOnly = 'gpu-only',
|
||||
HighVram = 'highvram',
|
||||
NormalVram = 'normalvram',
|
||||
LowVram = 'lowvram',
|
||||
NoVram = 'novram',
|
||||
CPU = 'cpu'
|
||||
}
|
||||
@@ -29,15 +29,10 @@ export interface Setting {
|
||||
render: () => HTMLElement
|
||||
}
|
||||
|
||||
export interface SettingParams {
|
||||
export interface SettingParams extends FormItem {
|
||||
id: keyof Settings
|
||||
name: string
|
||||
type: SettingInputType | SettingCustomRenderer
|
||||
defaultValue: any
|
||||
onChange?: (newValue: any, oldValue?: any) => void
|
||||
attrs?: any
|
||||
tooltip?: string
|
||||
options?: Array<string | SettingOption> | ((value: any) => SettingOption[])
|
||||
// By default category is id.split('.'). However, changing id to assign
|
||||
// new category has poor backward compatibility. Use this field to overwrite
|
||||
// default category from id.
|
||||
@@ -52,3 +47,14 @@ export interface SettingParams {
|
||||
// Version of the setting when it was last modified
|
||||
versionModified?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The base form item for rendering in a form.
|
||||
*/
|
||||
export interface FormItem {
|
||||
name: string
|
||||
type: SettingInputType | SettingCustomRenderer
|
||||
tooltip?: string
|
||||
attrs?: Record<string, any>
|
||||
options?: Array<string | SettingOption> | ((value: any) => SettingOption[])
|
||||
}
|
||||
|
||||
@@ -2,43 +2,85 @@
|
||||
<div
|
||||
class="font-sans flex flex-col justify-center items-center h-screen m-0 text-neutral-300 bg-neutral-900 dark-theme pointer-events-auto"
|
||||
>
|
||||
<h2 class="text-2xl font-bold">{{ ProgressMessages[status] }}</h2>
|
||||
<LogTerminal :fetch-logs="fetchLogs" :fetch-interval="500" />
|
||||
<h2 class="text-2xl font-bold">{{ t(`serverStart.process.${status}`) }}</h2>
|
||||
<div
|
||||
v-if="status == ProgressStatus.ERROR"
|
||||
class="flex items-center my-4 gap-2"
|
||||
>
|
||||
<Button
|
||||
icon="pi pi-flag"
|
||||
severity="secondary"
|
||||
:label="t('serverStart.reportIssue')"
|
||||
@click="reportIssue"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-file"
|
||||
severity="secondary"
|
||||
:label="t('serverStart.openLogs')"
|
||||
@click="openLogs"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-refresh"
|
||||
:label="t('serverStart.reinstall')"
|
||||
@click="reinstall"
|
||||
/>
|
||||
</div>
|
||||
<BaseTerminal @created="terminalCreated" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import LogTerminal from '@/components/common/LogTerminal.vue'
|
||||
import {
|
||||
ProgressStatus,
|
||||
ProgressMessages
|
||||
} from '@comfyorg/comfyui-electron-types'
|
||||
import Button from 'primevue/button'
|
||||
import { ref, onMounted, Ref } from 'vue'
|
||||
import BaseTerminal from '@/components/bottomPanel/tabs/terminal/BaseTerminal.vue'
|
||||
import { ProgressStatus } from '@comfyorg/comfyui-electron-types'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
import type { useTerminal } from '@/hooks/bottomPanelTabs/useTerminal'
|
||||
import { Terminal } from '@xterm/xterm'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const electron = electronAPI()
|
||||
const { t } = useI18n()
|
||||
|
||||
const status = ref<ProgressStatus>(ProgressStatus.INITIAL_STATE)
|
||||
const logs = ref<string[]>([])
|
||||
let xterm: Terminal | undefined
|
||||
|
||||
const updateProgress = ({ status: newStatus }: { status: ProgressStatus }) => {
|
||||
status.value = newStatus
|
||||
logs.value = [] // Clear logs when status changes
|
||||
xterm?.clear()
|
||||
}
|
||||
|
||||
const addLogMessage = (message: string) => {
|
||||
logs.value = [...logs.value, message]
|
||||
const terminalCreated = (
|
||||
{ terminal, useAutoSize }: ReturnType<typeof useTerminal>,
|
||||
root: Ref<HTMLElement>
|
||||
) => {
|
||||
xterm = terminal
|
||||
|
||||
useAutoSize(root, true, true)
|
||||
electron.onLogMessage((message: string) => {
|
||||
terminal.write(message)
|
||||
})
|
||||
|
||||
terminal.options.cursorBlink = false
|
||||
terminal.options.disableStdin = true
|
||||
terminal.options.cursorInactiveStyle = 'block'
|
||||
}
|
||||
|
||||
const fetchLogs = async () => {
|
||||
return logs.value.join('\n')
|
||||
const reinstall = () => electron.reinstall()
|
||||
const reportIssue = () => {
|
||||
window.open('https://forum.comfy.org/c/v1-feedback/', '_blank')
|
||||
}
|
||||
const openLogs = () => electron.openLogsFolder()
|
||||
|
||||
onMounted(() => {
|
||||
electron.sendReady()
|
||||
electron.onProgressUpdate(updateProgress)
|
||||
electron.onLogMessage((message: string) => {
|
||||
addLogMessage(message)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.xterm-helper-textarea) {
|
||||
/* Hide this as it moves all over when uv is running */
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
189
tests-ui/tests/fast/store/serverConfigStore.test.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
import { useServerConfigStore } from '@/stores/serverConfigStore'
|
||||
import { ServerConfig } from '@/constants/serverConfig'
|
||||
import type { FormItem } from '@/types/settingTypes'
|
||||
|
||||
const dummyFormItem: FormItem = {
|
||||
name: '',
|
||||
type: 'text'
|
||||
}
|
||||
|
||||
describe('useServerConfigStore', () => {
|
||||
let store: ReturnType<typeof useServerConfigStore>
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
store = useServerConfigStore()
|
||||
})
|
||||
|
||||
it('should initialize with empty configs', () => {
|
||||
expect(store.serverConfigs).toHaveLength(0)
|
||||
expect(Object.keys(store.serverConfigById)).toHaveLength(0)
|
||||
expect(Object.keys(store.serverConfigsByCategory)).toHaveLength(0)
|
||||
expect(Object.keys(store.serverConfigValues)).toHaveLength(0)
|
||||
expect(Object.keys(store.launchArgs)).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should load server configs with default values', () => {
|
||||
const configs: ServerConfig<any>[] = [
|
||||
{
|
||||
...dummyFormItem,
|
||||
id: 'test.config1',
|
||||
defaultValue: 'default1',
|
||||
category: ['Test']
|
||||
},
|
||||
{
|
||||
...dummyFormItem,
|
||||
id: 'test.config2',
|
||||
defaultValue: 'default2'
|
||||
}
|
||||
]
|
||||
|
||||
store.loadServerConfig(configs, {})
|
||||
|
||||
expect(store.serverConfigs).toHaveLength(2)
|
||||
expect(store.serverConfigById['test.config1'].value).toBe('default1')
|
||||
expect(store.serverConfigById['test.config2'].value).toBe('default2')
|
||||
})
|
||||
|
||||
it('should load server configs with provided values', () => {
|
||||
const configs: ServerConfig<any>[] = [
|
||||
{
|
||||
...dummyFormItem,
|
||||
id: 'test.config1',
|
||||
defaultValue: 'default1',
|
||||
category: ['Test']
|
||||
}
|
||||
]
|
||||
|
||||
store.loadServerConfig(configs, {
|
||||
'test.config1': 'custom1'
|
||||
})
|
||||
|
||||
expect(store.serverConfigs).toHaveLength(1)
|
||||
expect(store.serverConfigById['test.config1'].value).toBe('custom1')
|
||||
})
|
||||
|
||||
it('should organize configs by category', () => {
|
||||
const configs: ServerConfig<any>[] = [
|
||||
{
|
||||
...dummyFormItem,
|
||||
id: 'test.config1',
|
||||
defaultValue: 'default1',
|
||||
category: ['Test']
|
||||
},
|
||||
{
|
||||
...dummyFormItem,
|
||||
id: 'test.config2',
|
||||
defaultValue: 'default2',
|
||||
category: ['Other']
|
||||
},
|
||||
{
|
||||
...dummyFormItem,
|
||||
id: 'test.config3',
|
||||
defaultValue: 'default3'
|
||||
}
|
||||
]
|
||||
|
||||
store.loadServerConfig(configs, {})
|
||||
|
||||
expect(Object.keys(store.serverConfigsByCategory)).toHaveLength(3)
|
||||
expect(store.serverConfigsByCategory['Test']).toHaveLength(1)
|
||||
expect(store.serverConfigsByCategory['Other']).toHaveLength(1)
|
||||
expect(store.serverConfigsByCategory['General']).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should generate server config values excluding defaults', () => {
|
||||
const configs: ServerConfig<any>[] = [
|
||||
{
|
||||
...dummyFormItem,
|
||||
id: 'test.config1',
|
||||
defaultValue: 'default1'
|
||||
},
|
||||
{
|
||||
...dummyFormItem,
|
||||
id: 'test.config2',
|
||||
defaultValue: 'default2'
|
||||
}
|
||||
]
|
||||
|
||||
store.loadServerConfig(configs, {
|
||||
'test.config1': 'custom1',
|
||||
'test.config2': 'default2'
|
||||
})
|
||||
|
||||
expect(Object.keys(store.serverConfigValues)).toHaveLength(2)
|
||||
expect(store.serverConfigValues['test.config1']).toBe('custom1')
|
||||
expect(store.serverConfigValues['test.config2']).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should generate launch arguments with custom getValue function', () => {
|
||||
const configs: ServerConfig<any>[] = [
|
||||
{
|
||||
...dummyFormItem,
|
||||
id: 'test.config1',
|
||||
defaultValue: 'default1',
|
||||
getValue: (value: string) => ({ customArg: value })
|
||||
},
|
||||
{
|
||||
...dummyFormItem,
|
||||
id: 'test.config2',
|
||||
defaultValue: 'default2'
|
||||
}
|
||||
]
|
||||
|
||||
store.loadServerConfig(configs, {
|
||||
'test.config1': 'custom1',
|
||||
'test.config2': 'custom2'
|
||||
})
|
||||
|
||||
expect(Object.keys(store.launchArgs)).toHaveLength(2)
|
||||
expect(store.launchArgs['customArg']).toBe('custom1')
|
||||
expect(store.launchArgs['test.config2']).toBe('custom2')
|
||||
})
|
||||
|
||||
it('should not include default values in launch arguments', () => {
|
||||
const configs: ServerConfig<any>[] = [
|
||||
{
|
||||
...dummyFormItem,
|
||||
id: 'test.config1',
|
||||
defaultValue: 'default1'
|
||||
},
|
||||
{
|
||||
...dummyFormItem,
|
||||
id: 'test.config2',
|
||||
defaultValue: 'default2'
|
||||
}
|
||||
]
|
||||
|
||||
store.loadServerConfig(configs, {
|
||||
'test.config1': 'custom1',
|
||||
'test.config2': 'default2'
|
||||
})
|
||||
|
||||
expect(Object.keys(store.launchArgs)).toHaveLength(1)
|
||||
expect(store.launchArgs['test.config1']).toBe('custom1')
|
||||
expect(store.launchArgs['test.config2']).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should not include nullish values in launch arguments', () => {
|
||||
const configs: ServerConfig<any>[] = [
|
||||
{ ...dummyFormItem, id: 'test.config1', defaultValue: 'default1' },
|
||||
{ ...dummyFormItem, id: 'test.config2', defaultValue: 'default2' },
|
||||
{ ...dummyFormItem, id: 'test.config3', defaultValue: 'default3' }
|
||||
]
|
||||
|
||||
store.loadServerConfig(configs, {
|
||||
'test.config1': undefined,
|
||||
'test.config2': null,
|
||||
'test.config3': ''
|
||||
})
|
||||
|
||||
expect(Object.keys(store.launchArgs)).toHaveLength(0)
|
||||
expect(Object.keys(store.serverConfigValues)).toEqual([
|
||||
'test.config1',
|
||||
'test.config2',
|
||||
'test.config3'
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -126,14 +126,14 @@ describe('useKeybindingStore', () => {
|
||||
expect(store.getKeybinding(keybinding2.combo)).toEqual(keybinding2)
|
||||
})
|
||||
|
||||
it('should throw an error when unsetting non-existent keybindings', () => {
|
||||
it('should not throw an error when unsetting non-existent keybindings', () => {
|
||||
const store = useKeybindingStore()
|
||||
const keybinding = new KeybindingImpl({
|
||||
commandId: 'test.command',
|
||||
combo: { key: 'H', alt: true, shift: true }
|
||||
})
|
||||
|
||||
expect(() => store.unsetKeybinding(keybinding)).toThrow()
|
||||
expect(() => store.unsetKeybinding(keybinding)).not.toThrow()
|
||||
})
|
||||
|
||||
it('should remove unset keybinding when adding back a default keybinding', () => {
|
||||
@@ -160,4 +160,76 @@ describe('useKeybindingStore', () => {
|
||||
defaultKeybinding
|
||||
)
|
||||
})
|
||||
|
||||
it('Should accept same keybinding from default and user', () => {
|
||||
const store = useKeybindingStore()
|
||||
const keybinding = new KeybindingImpl({
|
||||
commandId: 'test.command',
|
||||
combo: { key: 'J', ctrl: true }
|
||||
})
|
||||
// Add default keybinding.
|
||||
// This can happen when we change default keybindings.
|
||||
store.addDefaultKeybinding(keybinding)
|
||||
// Add user keybinding.
|
||||
store.addUserKeybinding(keybinding)
|
||||
|
||||
expect(store.keybindings).toHaveLength(1)
|
||||
expect(store.getKeybinding(keybinding.combo)).toEqual(keybinding)
|
||||
})
|
||||
|
||||
it('Should keep previously customized keybindings after default keybindings change', () => {
|
||||
// Initially command 'foo' was bound to 'K, Ctrl'. User unset it and bound the
|
||||
// command to 'A, Ctrl'.
|
||||
// Now we change the default keybindings of 'foo' to 'A, Ctrl'.
|
||||
// The user customized keybinding should be kept.
|
||||
const store = useKeybindingStore()
|
||||
|
||||
const userUnsetKeybindings = [
|
||||
new KeybindingImpl({
|
||||
commandId: 'foo',
|
||||
combo: { key: 'K', ctrl: true }
|
||||
})
|
||||
]
|
||||
|
||||
const userNewKeybindings = [
|
||||
new KeybindingImpl({
|
||||
commandId: 'foo',
|
||||
combo: { key: 'A', ctrl: true }
|
||||
})
|
||||
]
|
||||
|
||||
const newCoreKeybindings = [
|
||||
new KeybindingImpl({
|
||||
commandId: 'foo',
|
||||
combo: { key: 'A', ctrl: true }
|
||||
})
|
||||
]
|
||||
|
||||
for (const keybinding of newCoreKeybindings) {
|
||||
store.addDefaultKeybinding(keybinding)
|
||||
}
|
||||
|
||||
expect(store.keybindings).toHaveLength(1)
|
||||
expect(store.getKeybinding(userNewKeybindings[0].combo)).toEqual(
|
||||
userNewKeybindings[0]
|
||||
)
|
||||
|
||||
for (const keybinding of userUnsetKeybindings) {
|
||||
store.unsetKeybinding(keybinding)
|
||||
}
|
||||
|
||||
expect(store.keybindings).toHaveLength(1)
|
||||
expect(store.getKeybinding(userNewKeybindings[0].combo)).toEqual(
|
||||
userNewKeybindings[0]
|
||||
)
|
||||
|
||||
for (const keybinding of userNewKeybindings) {
|
||||
store.addUserKeybinding(keybinding)
|
||||
}
|
||||
|
||||
expect(store.keybindings).toHaveLength(1)
|
||||
expect(store.getKeybinding(userNewKeybindings[0].combo)).toEqual(
|
||||
userNewKeybindings[0]
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||