mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-06-15 02:56:26 +00:00
Compare commits
18 Commits
v1.4.7
...
fix-queue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02c13c403f | ||
|
|
8254e3c9cf | ||
|
|
1160231b62 | ||
|
|
a51e27bedf | ||
|
|
abed0656af | ||
|
|
5febda16c7 | ||
|
|
069dc67c30 | ||
|
|
7623810166 | ||
|
|
21fa88461f | ||
|
|
27b0493306 | ||
|
|
ad2c1a0d3e | ||
|
|
f51866d988 | ||
|
|
46627bb44b | ||
|
|
68cadbda9f | ||
|
|
0f2260065a | ||
|
|
4007cc13c2 | ||
|
|
3920210c5c | ||
|
|
4e22bffae2 |
90
browser_tests/assets/oversized_group.json
Normal file
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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 ({
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
@@ -394,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'
|
||||
])
|
||||
})
|
||||
|
||||
@@ -471,6 +471,7 @@ test.describe('Menu', () => {
|
||||
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'])
|
||||
@@ -519,7 +520,7 @@ test.describe('Menu', () => {
|
||||
await closeButton.click()
|
||||
expect(
|
||||
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
|
||||
).toEqual(['Unsaved Workflow.json'])
|
||||
).toEqual(['*Unsaved Workflow.json'])
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "comfyui-frontend",
|
||||
"version": "1.4.7",
|
||||
"version": "1.4.9",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "comfyui-frontend",
|
||||
"version": "1.4.7",
|
||||
"version": "1.4.9",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.3.6",
|
||||
"@comfyorg/litegraph": "^0.8.33",
|
||||
"@comfyorg/litegraph": "^0.8.35",
|
||||
"@primevue/themes": "^4.0.5",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
@@ -1923,9 +1923,9 @@
|
||||
"license": "GPL-3.0-only"
|
||||
},
|
||||
"node_modules/@comfyorg/litegraph": {
|
||||
"version": "0.8.33",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.33.tgz",
|
||||
"integrity": "sha512-imzOow12J2uE0q1EqNCpcm0CE7ETBSq3P/28YQDVy3O+/YjgkEOElpqXvgk5iHaNQv0/K2MDH53stqdBfynpyg==",
|
||||
"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.7",
|
||||
"version": "1.4.9",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -73,7 +73,7 @@
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.3.6",
|
||||
"@comfyorg/litegraph": "^0.8.33",
|
||||
"@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
|
||||
|
||||
@@ -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
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
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>
|
||||
@@ -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,7 +50,9 @@
|
||||
<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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -27,18 +27,10 @@ import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
|
||||
{
|
||||
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: onChangeRestartApp
|
||||
},
|
||||
{
|
||||
id: 'Comfy-Desktop.ComfyServer.ExtraLaunchArgs',
|
||||
category: ['Comfy-Desktop', 'ComfyUI Server'],
|
||||
name: 'Extra launch arguments passed to the ComfyUI main.py script',
|
||||
type: 'text',
|
||||
defaultValue: '',
|
||||
onChange: onChangeRestartApp
|
||||
}
|
||||
],
|
||||
|
||||
|
||||
@@ -13,12 +13,12 @@ import { IWidget } from '@comfyorg/litegraph'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
async function uploadFile(
|
||||
modelWidget: IWidget,
|
||||
load3d: Load3d,
|
||||
file: File,
|
||||
updateNode: boolean,
|
||||
fileInput?: HTMLInputElement
|
||||
) {
|
||||
let uploadPath
|
||||
|
||||
try {
|
||||
const body = new FormData()
|
||||
body.append('image', file)
|
||||
@@ -32,19 +32,15 @@ async function uploadFile(
|
||||
if (resp.status === 200) {
|
||||
const data = await resp.json()
|
||||
let path = data.name
|
||||
|
||||
if (data.subfolder) path = data.subfolder + '/' + path
|
||||
|
||||
if (!modelWidget?.options?.values?.includes(path)) {
|
||||
modelWidget?.options?.values?.push(path)
|
||||
}
|
||||
uploadPath = path
|
||||
|
||||
if (updateNode) {
|
||||
modelWidget.value = path
|
||||
const modelUrl = api.apiURL(
|
||||
getResourceURL(...splitFilePath(path), 'input')
|
||||
)
|
||||
await load3d.loadModel(modelUrl, file.name)
|
||||
}
|
||||
const modelUrl = api.apiURL(
|
||||
getResourceURL(...splitFilePath(path), 'input')
|
||||
)
|
||||
await load3d.loadModel(modelUrl, file.name)
|
||||
|
||||
const fileExt = file.name.split('.').pop()?.toLowerCase()
|
||||
if (fileExt === 'obj' && fileInput?.files) {
|
||||
@@ -76,6 +72,8 @@ async function uploadFile(
|
||||
error instanceof Error ? error.message : 'Upload failed'
|
||||
)
|
||||
}
|
||||
|
||||
return uploadPath
|
||||
}
|
||||
|
||||
class Load3d {
|
||||
@@ -1111,6 +1109,48 @@ app.registerExtension({
|
||||
load3d.renderer.domElement.hidden = this.flags.collapsed ?? false
|
||||
}
|
||||
|
||||
const fileInput = document.createElement('input')
|
||||
fileInput.type = 'file'
|
||||
fileInput.accept = '.gltf,.glb,.obj,.mtl,.fbx,.stl'
|
||||
fileInput.style.display = 'none'
|
||||
fileInput.onchange = async () => {
|
||||
if (fileInput.files?.length) {
|
||||
const modelWidget = node.widgets?.find(
|
||||
(w: IWidget) => w.name === 'model_file'
|
||||
)
|
||||
const uploadPath = await uploadFile(
|
||||
load3d,
|
||||
fileInput.files[0],
|
||||
fileInput
|
||||
).catch((error) => {
|
||||
console.error('File upload failed:', error)
|
||||
useToastStore().addAlert('File upload failed')
|
||||
})
|
||||
|
||||
if (uploadPath && modelWidget) {
|
||||
if (!modelWidget.options?.values?.includes(uploadPath)) {
|
||||
modelWidget.options?.values?.push(uploadPath)
|
||||
}
|
||||
|
||||
modelWidget.value = uploadPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
node.addWidget('button', 'upload 3d model', 'upload3dmodel', () => {
|
||||
fileInput.click()
|
||||
})
|
||||
|
||||
node.addWidget('button', 'clear', 'clear', () => {
|
||||
load3d.clearModel()
|
||||
const modelWidget = node.widgets?.find(
|
||||
(w: IWidget) => w.name === 'model_file'
|
||||
)
|
||||
if (modelWidget) {
|
||||
modelWidget.value = ''
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
widget: node.addDOMWidget(inputName, 'LOAD_3D', container)
|
||||
}
|
||||
@@ -1138,6 +1178,8 @@ app.registerExtension({
|
||||
|
||||
const [oldWidth, oldHeight] = node.size
|
||||
|
||||
node.setSize([Math.max(oldWidth, 300), Math.max(oldHeight, 600)])
|
||||
|
||||
await nextTick()
|
||||
|
||||
const sceneWidget = node.widgets.find((w: IWidget) => w.name === 'image')
|
||||
@@ -1212,44 +1254,6 @@ app.registerExtension({
|
||||
const data = await resp.json()
|
||||
return `threed/${data.name} [temp]`
|
||||
}
|
||||
|
||||
const fileInput = document.createElement('input')
|
||||
fileInput.type = 'file'
|
||||
fileInput.accept = '.gltf,.glb,.obj,.mtl,.fbx,.stl'
|
||||
fileInput.style.display = 'none'
|
||||
fileInput.onchange = () => {
|
||||
if (fileInput.files?.length) {
|
||||
const modelWidget = node.widgets.find(
|
||||
(w: IWidget) => w.name === 'model_file'
|
||||
)
|
||||
uploadFile(
|
||||
modelWidget,
|
||||
load3d,
|
||||
fileInput.files[0],
|
||||
true,
|
||||
fileInput
|
||||
).catch((error) => {
|
||||
console.error('File upload failed:', error)
|
||||
useToastStore().addAlert('File upload failed')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
node.addWidget('button', 'upload 3d model', 'upload3dmodel', () => {
|
||||
fileInput.click()
|
||||
})
|
||||
|
||||
node.addWidget('button', 'clear', 'clear', () => {
|
||||
load3d.clearModel()
|
||||
const modelWidget = node.widgets.find(
|
||||
(w: IWidget) => w.name === 'model_file'
|
||||
)
|
||||
if (modelWidget) {
|
||||
modelWidget.value = ''
|
||||
}
|
||||
})
|
||||
|
||||
node.setSize([Math.max(oldWidth, 300), Math.max(oldHeight, 550)])
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1293,6 +1297,103 @@ app.registerExtension({
|
||||
load3d.renderer.domElement.hidden = this.flags.collapsed ?? false
|
||||
}
|
||||
|
||||
const fileInput = document.createElement('input')
|
||||
fileInput.type = 'file'
|
||||
fileInput.accept = '.fbx,glb,gltf'
|
||||
fileInput.style.display = 'none'
|
||||
fileInput.onchange = async () => {
|
||||
if (fileInput.files?.length) {
|
||||
const modelWidget = node.widgets?.find(
|
||||
(w: IWidget) => w.name === 'model_file'
|
||||
)
|
||||
const uploadPath = await uploadFile(
|
||||
load3d,
|
||||
fileInput.files[0],
|
||||
fileInput
|
||||
).catch((error) => {
|
||||
console.error('File upload failed:', error)
|
||||
useToastStore().addAlert('File upload failed')
|
||||
})
|
||||
|
||||
if (uploadPath && modelWidget) {
|
||||
if (!modelWidget.options?.values?.includes(uploadPath)) {
|
||||
modelWidget.options?.values?.push(uploadPath)
|
||||
}
|
||||
|
||||
modelWidget.value = uploadPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
node.addWidget('button', 'upload 3d model', 'upload3dmodel', () => {
|
||||
fileInput.click()
|
||||
})
|
||||
|
||||
node.addWidget('button', 'clear', 'clear', () => {
|
||||
load3d.clearModel()
|
||||
const modelWidget = node.widgets?.find(
|
||||
(w: IWidget) => w.name === 'model_file'
|
||||
)
|
||||
if (modelWidget) {
|
||||
modelWidget.value = ''
|
||||
}
|
||||
|
||||
const animationSelect = node.widgets?.find(
|
||||
(w: IWidget) => w.name === 'animation'
|
||||
)
|
||||
|
||||
if (animationSelect) {
|
||||
animationSelect.options.values = []
|
||||
animationSelect.value = ''
|
||||
}
|
||||
|
||||
const speedSelect = node.widgets?.find(
|
||||
(w: IWidget) => w.name === 'animation_speed'
|
||||
)
|
||||
|
||||
if (speedSelect) {
|
||||
speedSelect.value = '1'
|
||||
}
|
||||
})
|
||||
|
||||
node.addWidget(
|
||||
'button',
|
||||
'Play/Pause Animation',
|
||||
'toggle_animation',
|
||||
() => {
|
||||
load3d.toggleAnimation()
|
||||
}
|
||||
)
|
||||
|
||||
const animationSelect = node.addWidget(
|
||||
'combo',
|
||||
'animation',
|
||||
'',
|
||||
() => '',
|
||||
{
|
||||
values: []
|
||||
}
|
||||
) as IWidget
|
||||
|
||||
animationSelect.callback = (value: string) => {
|
||||
const names = load3d.getAnimationNames()
|
||||
const index = names.indexOf(value)
|
||||
|
||||
if (index !== -1) {
|
||||
const wasPlaying = load3d.isAnimationPlaying
|
||||
|
||||
if (wasPlaying) {
|
||||
load3d.toggleAnimation(false)
|
||||
}
|
||||
|
||||
load3d.updateSelectedAnimation(index)
|
||||
|
||||
if (wasPlaying) {
|
||||
load3d.toggleAnimation(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
widget: node.addDOMWidget(inputName, 'LOAD_3D_ANIMATION', container)
|
||||
}
|
||||
@@ -1320,6 +1421,8 @@ app.registerExtension({
|
||||
|
||||
const [oldWidth, oldHeight] = node.size
|
||||
|
||||
node.setSize([Math.max(oldWidth, 300), Math.max(oldHeight, 700)])
|
||||
|
||||
await nextTick()
|
||||
|
||||
const sceneWidget = node.widgets.find((w: IWidget) => w.name === 'image')
|
||||
@@ -1352,29 +1455,6 @@ app.registerExtension({
|
||||
(w: IWidget) => w.name === 'up_direction'
|
||||
)
|
||||
|
||||
const animationSelect = node.addWidget('combo', 'animation', '', () => '', {
|
||||
values: []
|
||||
}) as IWidget
|
||||
|
||||
animationSelect.callback = (value: number) => {
|
||||
const names = load3d.getAnimationNames()
|
||||
const index = names.indexOf(value)
|
||||
|
||||
if (index !== -1) {
|
||||
const wasPlaying = load3d.isAnimationPlaying
|
||||
|
||||
if (wasPlaying) {
|
||||
load3d.toggleAnimation(false)
|
||||
}
|
||||
|
||||
load3d.updateSelectedAnimation(index)
|
||||
|
||||
if (wasPlaying) {
|
||||
load3d.toggleAnimation(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const speedSelect = node.widgets.find(
|
||||
(w: IWidget) => w.name === 'animation_speed'
|
||||
)
|
||||
@@ -1400,6 +1480,11 @@ app.registerExtension({
|
||||
(load3d: Load3d) => {
|
||||
const animationLoad3d = load3d as Load3dAnimation
|
||||
const names = animationLoad3d.getAnimationNames()
|
||||
|
||||
const animationSelect = node.widgets.find(
|
||||
(w: IWidget) => w.name === 'animation'
|
||||
)
|
||||
|
||||
animationSelect.options.values = names
|
||||
if (names.length) {
|
||||
animationSelect.value = names[0]
|
||||
@@ -1438,56 +1523,6 @@ app.registerExtension({
|
||||
const data = await resp.json()
|
||||
return `threed/${data.name} [temp]`
|
||||
}
|
||||
|
||||
const fileInput = document.createElement('input')
|
||||
fileInput.type = 'file'
|
||||
fileInput.accept = '.fbx,glb,gltf'
|
||||
fileInput.style.display = 'none'
|
||||
fileInput.onchange = () => {
|
||||
if (fileInput.files?.length) {
|
||||
const modelWidget = node.widgets.find(
|
||||
(w: IWidget) => w.name === 'model_file'
|
||||
)
|
||||
uploadFile(
|
||||
modelWidget,
|
||||
load3d,
|
||||
fileInput.files[0],
|
||||
true,
|
||||
fileInput
|
||||
).catch((error) => {
|
||||
console.error('File upload failed:', error)
|
||||
useToastStore().addAlert('File upload failed')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
node.addWidget('button', 'upload 3d model', 'upload3dmodel', () => {
|
||||
fileInput.click()
|
||||
})
|
||||
|
||||
node.addWidget('button', 'clear', 'clear', () => {
|
||||
load3d.clearModel()
|
||||
const modelWidget = node.widgets.find(
|
||||
(w: IWidget) => w.name === 'model_file'
|
||||
)
|
||||
if (modelWidget) {
|
||||
modelWidget.value = ''
|
||||
}
|
||||
if (animationSelect) {
|
||||
animationSelect.options.values = []
|
||||
animationSelect.value = ''
|
||||
}
|
||||
|
||||
if (speedSelect) {
|
||||
speedSelect.value = '1'
|
||||
}
|
||||
})
|
||||
|
||||
node.addWidget('button', 'Play/Pause Animation', 'toggle_animation', () => {
|
||||
load3d.toggleAnimation()
|
||||
})
|
||||
|
||||
node.setSize([Math.max(oldWidth, 300), Math.max(oldHeight, 550)])
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1556,6 +1591,8 @@ app.registerExtension({
|
||||
|
||||
const [oldWidth, oldHeight] = node.size
|
||||
|
||||
node.setSize([Math.max(oldWidth, 300), Math.max(oldHeight, 550)])
|
||||
|
||||
await nextTick()
|
||||
|
||||
const sceneWidget = node.widgets.find((w: IWidget) => w.name === 'image')
|
||||
@@ -1600,7 +1637,5 @@ app.registerExtension({
|
||||
lightIntensity,
|
||||
upDirection
|
||||
)
|
||||
|
||||
node.setSize([Math.max(oldWidth, 300), Math.max(oldHeight, 550)])
|
||||
}
|
||||
})
|
||||
|
||||
13
src/i18n.ts
13
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,18 +33,18 @@ 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',
|
||||
@@ -97,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',
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
73
src/stores/serverConfigStore.ts
Normal file
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
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'
|
||||
}
|
||||
189
tests-ui/tests/fast/store/serverConfigStore.test.ts
Normal file
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'
|
||||
])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user