mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 10:42:44 +00:00
feat: add reset-to-default for widget parameters in right side panel (#8861)
## Summary Add per-widget and reset-all-parameters functionality to the right side panel, allowing users to quickly revert widget values to their defaults. ## Changes - **What**: Per-widget "Reset to default" option in the WidgetActions overflow menu, plus a "Reset all parameters" button in each SectionWidgets header. Defaults are derived from the InputSpec (explicit default, then type-specific fallbacks: 0 for INT/FLOAT, false for BOOLEAN, empty string for STRING, first option for COMBO). - **Dependencies**: Builds on #8594 (WidgetValueStore) for reactive UI updates after reset. ## Review Focus - `getWidgetDefaultValue` fallback logic in `src/utils/widgetUtil.ts` — are the type-specific defaults appropriate? - Deep equality check (`isEqual`) for disabling the reset button when the value already matches the default. - Event flow: WidgetActions emits `resetToDefault` → WidgetItem forwards → SectionWidgets handles via `writeWidgetValue` (sets value, triggers callback, marks canvas dirty). ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8861-feat-add-reset-to-default-for-widget-parameters-in-right-side-panel-3076d73d365081d1aa08d5b965a16cf4) by [Unito](https://www.unito.io) Co-authored-by: Terry Jia <terryjia88@gmail.com>
This commit is contained in:
@@ -12,6 +12,9 @@ import type {
|
|||||||
} from '@/lib/litegraph/src/litegraph'
|
} from '@/lib/litegraph/src/litegraph'
|
||||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||||
|
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||||
|
import { getWidgetDefaultValue } from '@/utils/widgetUtil'
|
||||||
|
import type { WidgetValue } from '@/utils/widgetUtil'
|
||||||
|
|
||||||
import PropertiesAccordionItem from '../layout/PropertiesAccordionItem.vue'
|
import PropertiesAccordionItem from '../layout/PropertiesAccordionItem.vue'
|
||||||
import { HideLayoutFieldKey } from '@/types/widgetTypes'
|
import { HideLayoutFieldKey } from '@/types/widgetTypes'
|
||||||
@@ -57,6 +60,7 @@ watchEffect(() => (widgets.value = widgetsProp))
|
|||||||
provide(HideLayoutFieldKey, true)
|
provide(HideLayoutFieldKey, true)
|
||||||
|
|
||||||
const canvasStore = useCanvasStore()
|
const canvasStore = useCanvasStore()
|
||||||
|
const nodeDefStore = useNodeDefStore()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const getNodeParentGroup = inject(GetNodeParentGroupKey, null)
|
const getNodeParentGroup = inject(GetNodeParentGroupKey, null)
|
||||||
@@ -118,15 +122,31 @@ function handleLocateNode() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleWidgetValueUpdate(
|
function writeWidgetValue(widget: IBaseWidget, value: WidgetValue) {
|
||||||
widget: IBaseWidget,
|
widget.value = value
|
||||||
newValue: string | number | boolean | object
|
widget.callback?.(value)
|
||||||
) {
|
|
||||||
widget.value = newValue
|
|
||||||
widget.callback?.(newValue)
|
|
||||||
canvasStore.canvas?.setDirty(true, true)
|
canvasStore.canvas?.setDirty(true, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleResetAllWidgets() {
|
||||||
|
for (const { widget, node: widgetNode } of widgetsProp) {
|
||||||
|
const spec = nodeDefStore.getInputSpecForWidget(widgetNode, widget.name)
|
||||||
|
const defaultValue = getWidgetDefaultValue(spec)
|
||||||
|
if (defaultValue !== undefined) {
|
||||||
|
writeWidgetValue(widget, defaultValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWidgetValueUpdate(widget: IBaseWidget, newValue: WidgetValue) {
|
||||||
|
if (newValue === undefined) return
|
||||||
|
writeWidgetValue(widget, newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWidgetReset(widget: IBaseWidget, newValue: WidgetValue) {
|
||||||
|
writeWidgetValue(widget, newValue)
|
||||||
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
widgetsContainer,
|
widgetsContainer,
|
||||||
rootElement
|
rootElement
|
||||||
@@ -157,6 +177,17 @@ defineExpose({
|
|||||||
{{ parentGroup.title }}
|
{{ parentGroup.title }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
<Button
|
||||||
|
v-if="!isEmpty"
|
||||||
|
variant="textonly"
|
||||||
|
size="icon-sm"
|
||||||
|
class="subbutton shrink-0 size-8 cursor-pointer text-muted-foreground hover:text-base-foreground"
|
||||||
|
:title="t('rightSidePanel.resetAllParameters')"
|
||||||
|
:aria-label="t('rightSidePanel.resetAllParameters')"
|
||||||
|
@click.stop="handleResetAllWidgets"
|
||||||
|
>
|
||||||
|
<i class="icon-[lucide--rotate-ccw] size-4" />
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
v-if="canShowLocateButton"
|
v-if="canShowLocateButton"
|
||||||
variant="textonly"
|
variant="textonly"
|
||||||
@@ -189,6 +220,7 @@ defineExpose({
|
|||||||
:parents="parents"
|
:parents="parents"
|
||||||
:is-shown-on-parents="isWidgetShownOnParents(node, widget)"
|
:is-shown-on-parents="isWidgetShownOnParents(node, widget)"
|
||||||
@update:widget-value="handleWidgetValueUpdate(widget, $event)"
|
@update:widget-value="handleWidgetValueUpdate(widget, $event)"
|
||||||
|
@reset-to-default="handleWidgetReset(widget, $event)"
|
||||||
/>
|
/>
|
||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
209
src/components/rightSidePanel/parameters/WidgetActions.test.ts
Normal file
209
src/components/rightSidePanel/parameters/WidgetActions.test.ts
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
import { createTestingPinia } from '@pinia/testing'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import { setActivePinia } from 'pinia'
|
||||||
|
import type { Slots } from 'vue'
|
||||||
|
import { h } from 'vue'
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { createI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||||
|
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||||
|
|
||||||
|
import WidgetActions from './WidgetActions.vue'
|
||||||
|
|
||||||
|
const { mockGetInputSpecForWidget } = vi.hoisted(() => ({
|
||||||
|
mockGetInputSpecForWidget: vi.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/stores/nodeDefStore', () => ({
|
||||||
|
useNodeDefStore: () => ({
|
||||||
|
getInputSpecForWidget: mockGetInputSpecForWidget
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||||
|
useCanvasStore: () => ({
|
||||||
|
canvas: { setDirty: vi.fn() }
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/stores/workspace/favoritedWidgetsStore', () => ({
|
||||||
|
useFavoritedWidgetsStore: () => ({
|
||||||
|
isFavorited: vi.fn().mockReturnValue(false),
|
||||||
|
toggleFavorite: vi.fn()
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/services/dialogService', () => ({
|
||||||
|
useDialogService: () => ({
|
||||||
|
prompt: vi.fn()
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/components/button/MoreButton.vue', () => ({
|
||||||
|
default: (_: unknown, { slots }: { slots: Slots }) =>
|
||||||
|
h('div', slots.default?.({ close: () => {} }))
|
||||||
|
}))
|
||||||
|
|
||||||
|
const i18n = createI18n({
|
||||||
|
legacy: false,
|
||||||
|
locale: 'en',
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
g: {
|
||||||
|
rename: 'Rename',
|
||||||
|
enterNewName: 'Enter new name'
|
||||||
|
},
|
||||||
|
rightSidePanel: {
|
||||||
|
hideInput: 'Hide input',
|
||||||
|
showInput: 'Show input',
|
||||||
|
addFavorite: 'Favorite',
|
||||||
|
removeFavorite: 'Unfavorite',
|
||||||
|
resetToDefault: 'Reset to default'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('WidgetActions', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||||
|
vi.resetAllMocks()
|
||||||
|
mockGetInputSpecForWidget.mockReturnValue({
|
||||||
|
type: 'INT',
|
||||||
|
default: 42
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function createMockWidget(
|
||||||
|
value: number = 100,
|
||||||
|
callback?: () => void
|
||||||
|
): IBaseWidget {
|
||||||
|
return {
|
||||||
|
name: 'test_widget',
|
||||||
|
type: 'number',
|
||||||
|
value,
|
||||||
|
label: 'Test Widget',
|
||||||
|
options: {},
|
||||||
|
y: 0,
|
||||||
|
callback
|
||||||
|
} as IBaseWidget
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMockNode(): LGraphNode {
|
||||||
|
return {
|
||||||
|
id: 1,
|
||||||
|
type: 'TestNode'
|
||||||
|
} as LGraphNode
|
||||||
|
}
|
||||||
|
|
||||||
|
function mountWidgetActions(widget: IBaseWidget, node: LGraphNode) {
|
||||||
|
return mount(WidgetActions, {
|
||||||
|
props: {
|
||||||
|
widget,
|
||||||
|
node,
|
||||||
|
label: 'Test Widget'
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
plugins: [i18n]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
it('shows reset button when widget has default value', () => {
|
||||||
|
const widget = createMockWidget()
|
||||||
|
const node = createMockNode()
|
||||||
|
|
||||||
|
const wrapper = mountWidgetActions(widget, node)
|
||||||
|
|
||||||
|
const resetButton = wrapper
|
||||||
|
.findAll('button')
|
||||||
|
.find((b) => b.text().includes('Reset'))
|
||||||
|
expect(resetButton).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('emits resetToDefault with default value when reset button clicked', async () => {
|
||||||
|
const widget = createMockWidget(100)
|
||||||
|
const node = createMockNode()
|
||||||
|
|
||||||
|
const wrapper = mountWidgetActions(widget, node)
|
||||||
|
|
||||||
|
const resetButton = wrapper
|
||||||
|
.findAll('button')
|
||||||
|
.find((b) => b.text().includes('Reset'))
|
||||||
|
|
||||||
|
await resetButton?.trigger('click')
|
||||||
|
|
||||||
|
expect(wrapper.emitted('resetToDefault')).toHaveLength(1)
|
||||||
|
expect(wrapper.emitted('resetToDefault')![0]).toEqual([42])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('disables reset button when value equals default', () => {
|
||||||
|
const widget = createMockWidget(42)
|
||||||
|
const node = createMockNode()
|
||||||
|
|
||||||
|
const wrapper = mountWidgetActions(widget, node)
|
||||||
|
|
||||||
|
const resetButton = wrapper
|
||||||
|
.findAll('button')
|
||||||
|
.find((b) => b.text().includes('Reset'))
|
||||||
|
|
||||||
|
expect(resetButton?.attributes('disabled')).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not show reset button when no default value exists', () => {
|
||||||
|
mockGetInputSpecForWidget.mockReturnValue({
|
||||||
|
type: 'CUSTOM'
|
||||||
|
})
|
||||||
|
|
||||||
|
const widget = createMockWidget(100)
|
||||||
|
const node = createMockNode()
|
||||||
|
|
||||||
|
const wrapper = mountWidgetActions(widget, node)
|
||||||
|
|
||||||
|
const resetButton = wrapper
|
||||||
|
.findAll('button')
|
||||||
|
.find((b) => b.text().includes('Reset'))
|
||||||
|
|
||||||
|
expect(resetButton).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses fallback default for INT type without explicit default', async () => {
|
||||||
|
mockGetInputSpecForWidget.mockReturnValue({
|
||||||
|
type: 'INT'
|
||||||
|
})
|
||||||
|
|
||||||
|
const widget = createMockWidget(100)
|
||||||
|
const node = createMockNode()
|
||||||
|
|
||||||
|
const wrapper = mountWidgetActions(widget, node)
|
||||||
|
|
||||||
|
const resetButton = wrapper
|
||||||
|
.findAll('button')
|
||||||
|
.find((b) => b.text().includes('Reset'))
|
||||||
|
|
||||||
|
await resetButton?.trigger('click')
|
||||||
|
|
||||||
|
expect(wrapper.emitted('resetToDefault')![0]).toEqual([0])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses first option as default for combo without explicit default', async () => {
|
||||||
|
mockGetInputSpecForWidget.mockReturnValue({
|
||||||
|
type: 'COMBO',
|
||||||
|
options: ['option1', 'option2', 'option3']
|
||||||
|
})
|
||||||
|
|
||||||
|
const widget = createMockWidget(100)
|
||||||
|
const node = createMockNode()
|
||||||
|
|
||||||
|
const wrapper = mountWidgetActions(widget, node)
|
||||||
|
|
||||||
|
const resetButton = wrapper
|
||||||
|
.findAll('button')
|
||||||
|
.find((b) => b.text().includes('Reset'))
|
||||||
|
|
||||||
|
await resetButton?.trigger('click')
|
||||||
|
|
||||||
|
expect(wrapper.emitted('resetToDefault')![0]).toEqual(['option1'])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { cn } from '@comfyorg/tailwind-utils'
|
import { cn } from '@comfyorg/tailwind-utils'
|
||||||
|
import { isEqual } from 'es-toolkit'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
@@ -14,7 +15,10 @@ import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
|||||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||||
import { useDialogService } from '@/services/dialogService'
|
import { useDialogService } from '@/services/dialogService'
|
||||||
|
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||||
import { useFavoritedWidgetsStore } from '@/stores/workspace/favoritedWidgetsStore'
|
import { useFavoritedWidgetsStore } from '@/stores/workspace/favoritedWidgetsStore'
|
||||||
|
import { getWidgetDefaultValue } from '@/utils/widgetUtil'
|
||||||
|
import type { WidgetValue } from '@/utils/widgetUtil'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
widget,
|
widget,
|
||||||
@@ -28,10 +32,15 @@ const {
|
|||||||
isShownOnParents?: boolean
|
isShownOnParents?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
resetToDefault: [value: WidgetValue]
|
||||||
|
}>()
|
||||||
|
|
||||||
const label = defineModel<string>('label', { required: true })
|
const label = defineModel<string>('label', { required: true })
|
||||||
|
|
||||||
const canvasStore = useCanvasStore()
|
const canvasStore = useCanvasStore()
|
||||||
const favoritedWidgetsStore = useFavoritedWidgetsStore()
|
const favoritedWidgetsStore = useFavoritedWidgetsStore()
|
||||||
|
const nodeDefStore = useNodeDefStore()
|
||||||
const dialogService = useDialogService()
|
const dialogService = useDialogService()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
@@ -43,6 +52,19 @@ const isFavorited = computed(() =>
|
|||||||
favoritedWidgetsStore.isFavorited(favoriteNode.value, widget.name)
|
favoritedWidgetsStore.isFavorited(favoriteNode.value, widget.name)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const inputSpec = computed(() =>
|
||||||
|
nodeDefStore.getInputSpecForWidget(node, widget.name)
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultValue = computed(() => getWidgetDefaultValue(inputSpec.value))
|
||||||
|
|
||||||
|
const hasDefault = computed(() => defaultValue.value !== undefined)
|
||||||
|
|
||||||
|
const isCurrentValueDefault = computed(() => {
|
||||||
|
if (!hasDefault.value) return true
|
||||||
|
return isEqual(widget.value, defaultValue.value)
|
||||||
|
})
|
||||||
|
|
||||||
async function handleRename() {
|
async function handleRename() {
|
||||||
const newLabel = await dialogService.prompt({
|
const newLabel = await dialogService.prompt({
|
||||||
title: t('g.rename'),
|
title: t('g.rename'),
|
||||||
@@ -97,6 +119,11 @@ function handleToggleFavorite() {
|
|||||||
favoritedWidgetsStore.toggleFavorite(favoriteNode.value, widget.name)
|
favoritedWidgetsStore.toggleFavorite(favoriteNode.value, widget.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleResetToDefault() {
|
||||||
|
if (!hasDefault.value) return
|
||||||
|
emit('resetToDefault', defaultValue.value)
|
||||||
|
}
|
||||||
|
|
||||||
const buttonClasses = cn([
|
const buttonClasses = cn([
|
||||||
'border-none bg-transparent',
|
'border-none bg-transparent',
|
||||||
'w-full flex items-center gap-2 rounded px-3 py-2 text-sm',
|
'w-full flex items-center gap-2 rounded px-3 py-2 text-sm',
|
||||||
@@ -162,6 +189,21 @@ const buttonClasses = cn([
|
|||||||
<span>{{ t('rightSidePanel.addFavorite') }}</span>
|
<span>{{ t('rightSidePanel.addFavorite') }}</span>
|
||||||
</template>
|
</template>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="hasDefault"
|
||||||
|
:class="cn(buttonClasses, isCurrentValueDefault && 'opacity-50')"
|
||||||
|
:disabled="isCurrentValueDefault"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
handleResetToDefault()
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<i class="icon-[lucide--rotate-ccw] size-4" />
|
||||||
|
<span>{{ t('rightSidePanel.resetToDefault') }}</span>
|
||||||
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</MoreButton>
|
</MoreButton>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { getNodeByExecutionId } from '@/utils/graphTraversalUtil'
|
|||||||
import { resolveNodeDisplayName } from '@/utils/nodeTitleUtil'
|
import { resolveNodeDisplayName } from '@/utils/nodeTitleUtil'
|
||||||
import { cn } from '@/utils/tailwindUtil'
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
import { renameWidget } from '@/utils/widgetUtil'
|
import { renameWidget } from '@/utils/widgetUtil'
|
||||||
|
import type { WidgetValue } from '@/utils/widgetUtil'
|
||||||
|
|
||||||
import WidgetActions from './WidgetActions.vue'
|
import WidgetActions from './WidgetActions.vue'
|
||||||
|
|
||||||
@@ -42,7 +43,8 @@ const {
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:widgetValue': [value: string | number | boolean | object]
|
'update:widgetValue': [value: WidgetValue]
|
||||||
|
resetToDefault: [value: WidgetValue]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -84,7 +86,7 @@ const favoriteNode = computed(() =>
|
|||||||
|
|
||||||
const widgetValue = computed({
|
const widgetValue = computed({
|
||||||
get: () => widget.value,
|
get: () => widget.value,
|
||||||
set: (newValue: string | number | boolean | object) => {
|
set: (newValue: WidgetValue) => {
|
||||||
emit('update:widgetValue', newValue)
|
emit('update:widgetValue', newValue)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -154,6 +156,7 @@ const displayLabel = customRef((track, trigger) => {
|
|||||||
:node="node"
|
:node="node"
|
||||||
:parents="parents"
|
:parents="parents"
|
||||||
:is-shown-on-parents="isShownOnParents"
|
:is-shown-on-parents="isShownOnParents"
|
||||||
|
@reset-to-default="emit('resetToDefault', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2953,7 +2953,9 @@
|
|||||||
"nodesNoneDesc": "NO NODES",
|
"nodesNoneDesc": "NO NODES",
|
||||||
"fallbackGroupTitle": "Group",
|
"fallbackGroupTitle": "Group",
|
||||||
"fallbackNodeTitle": "Node",
|
"fallbackNodeTitle": "Node",
|
||||||
"hideAdvancedInputsButton": "Hide advanced inputs"
|
"hideAdvancedInputsButton": "Hide advanced inputs",
|
||||||
|
"resetToDefault": "Reset to default",
|
||||||
|
"resetAllParameters": "Reset all parameters"
|
||||||
},
|
},
|
||||||
"help": {
|
"help": {
|
||||||
"recentReleases": "Recent releases",
|
"recentReleases": "Recent releases",
|
||||||
|
|||||||
46
src/utils/widgetUtil.test.ts
Normal file
46
src/utils/widgetUtil.test.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||||
|
|
||||||
|
import { getWidgetDefaultValue } from '@/utils/widgetUtil'
|
||||||
|
|
||||||
|
describe('getWidgetDefaultValue', () => {
|
||||||
|
it('returns undefined for undefined spec', () => {
|
||||||
|
expect(getWidgetDefaultValue(undefined)).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns explicit default when provided', () => {
|
||||||
|
const spec = { type: 'INT', default: 42 } as InputSpec
|
||||||
|
expect(getWidgetDefaultValue(spec)).toBe(42)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns 0 for INT type without default', () => {
|
||||||
|
const spec = { type: 'INT' } as InputSpec
|
||||||
|
expect(getWidgetDefaultValue(spec)).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns 0 for FLOAT type without default', () => {
|
||||||
|
const spec = { type: 'FLOAT' } as InputSpec
|
||||||
|
expect(getWidgetDefaultValue(spec)).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns false for BOOLEAN type without default', () => {
|
||||||
|
const spec = { type: 'BOOLEAN' } as InputSpec
|
||||||
|
expect(getWidgetDefaultValue(spec)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns empty string for STRING type without default', () => {
|
||||||
|
const spec = { type: 'STRING' } as InputSpec
|
||||||
|
expect(getWidgetDefaultValue(spec)).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns first option for array options without default', () => {
|
||||||
|
const spec = { type: 'COMBO', options: ['a', 'b', 'c'] } as InputSpec
|
||||||
|
expect(getWidgetDefaultValue(spec)).toBe('a')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns undefined for unknown type without options', () => {
|
||||||
|
const spec = { type: 'CUSTOM' } as InputSpec
|
||||||
|
expect(getWidgetDefaultValue(spec)).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -2,6 +2,32 @@ import { isProxyWidget } from '@/core/graph/subgraph/proxyWidget'
|
|||||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||||
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||||
|
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||||
|
|
||||||
|
export type WidgetValue = boolean | number | string | object | undefined
|
||||||
|
|
||||||
|
export function getWidgetDefaultValue(
|
||||||
|
spec: InputSpec | undefined
|
||||||
|
): WidgetValue {
|
||||||
|
if (!spec) return undefined
|
||||||
|
|
||||||
|
if (spec.default !== undefined) return spec.default as WidgetValue
|
||||||
|
|
||||||
|
switch (spec.type) {
|
||||||
|
case 'INT':
|
||||||
|
case 'FLOAT':
|
||||||
|
return 0
|
||||||
|
case 'BOOLEAN':
|
||||||
|
return false
|
||||||
|
case 'STRING':
|
||||||
|
return ''
|
||||||
|
default:
|
||||||
|
if (Array.isArray(spec.options) && spec.options.length > 0) {
|
||||||
|
return spec.options[0] as WidgetValue
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renames a widget and its corresponding input.
|
* Renames a widget and its corresponding input.
|
||||||
|
|||||||
Reference in New Issue
Block a user