mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-13 09:00:16 +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:
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'])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user