diff --git a/browser_tests/tests/dialog.spec.ts b/browser_tests/tests/dialog.spec.ts index fddbfbc708..c954177d63 100644 --- a/browser_tests/tests/dialog.spec.ts +++ b/browser_tests/tests/dialog.spec.ts @@ -298,7 +298,10 @@ test.describe('Settings', () => { await input.press('Alt+n') const requestPromise = comfyPage.page.waitForRequest( - '**/api/settings/Comfy.Keybinding.NewBindings' + (req) => + req.url().includes('/api/settings') && + !req.url().includes('/api/settings/') && + req.method() === 'POST' ) // Save keybinding diff --git a/src/composables/useCoreCommands.test.ts b/src/composables/useCoreCommands.test.ts index a02fb0414f..925b0aac4e 100644 --- a/src/composables/useCoreCommands.test.ts +++ b/src/composables/useCoreCommands.test.ts @@ -197,6 +197,7 @@ describe('useCoreCommands', () => { addSetting: vi.fn(), load: vi.fn(), set: vi.fn(), + setMany: vi.fn(), exists: vi.fn(), getDefaultValue: vi.fn(), isReady: true, diff --git a/src/platform/keybindings/keybindingService.ts b/src/platform/keybindings/keybindingService.ts index f8c822273e..6b6047dec2 100644 --- a/src/platform/keybindings/keybindingService.ts +++ b/src/platform/keybindings/keybindingService.ts @@ -137,14 +137,14 @@ export function useKeybindingService() { } async function persistUserKeybindings() { - await settingStore.set( - 'Comfy.Keybinding.NewBindings', - Object.values(keybindingStore.getUserKeybindings()) - ) - await settingStore.set( - 'Comfy.Keybinding.UnsetBindings', - Object.values(keybindingStore.getUserUnsetKeybindings()) - ) + await settingStore.setMany({ + 'Comfy.Keybinding.NewBindings': Object.values( + keybindingStore.getUserKeybindings() + ), + 'Comfy.Keybinding.UnsetBindings': Object.values( + keybindingStore.getUserUnsetKeybindings() + ) + }) } return { diff --git a/src/platform/settings/constants/coreSettings.ts b/src/platform/settings/constants/coreSettings.ts index 38e635cb9b..e75e6445c6 100644 --- a/src/platform/settings/constants/coreSettings.ts +++ b/src/platform/settings/constants/coreSettings.ts @@ -170,13 +170,15 @@ export const CORE_SETTINGS: SettingParams[] = [ const settingStore = useSettingStore() if (newValue === 'standard') { - // Update related settings to match standard mode - select + panning - await settingStore.set('Comfy.Canvas.LeftMouseClickBehavior', 'select') - await settingStore.set('Comfy.Canvas.MouseWheelScroll', 'panning') + await settingStore.setMany({ + 'Comfy.Canvas.LeftMouseClickBehavior': 'select', + 'Comfy.Canvas.MouseWheelScroll': 'panning' + }) } else if (newValue === 'legacy') { - // Update related settings to match legacy mode - panning + zoom - await settingStore.set('Comfy.Canvas.LeftMouseClickBehavior', 'panning') - await settingStore.set('Comfy.Canvas.MouseWheelScroll', 'zoom') + await settingStore.setMany({ + 'Comfy.Canvas.LeftMouseClickBehavior': 'panning', + 'Comfy.Canvas.MouseWheelScroll': 'zoom' + }) } } }, diff --git a/src/platform/settings/settingStore.test.ts b/src/platform/settings/settingStore.test.ts index 681d13e886..6117441950 100644 --- a/src/platform/settings/settingStore.test.ts +++ b/src/platform/settings/settingStore.test.ts @@ -15,7 +15,8 @@ import { app } from '@/scripts/app' vi.mock('@/scripts/api', () => ({ api: { getSettings: vi.fn(), - storeSetting: vi.fn() + storeSetting: vi.fn(), + storeSettings: vi.fn() } })) @@ -503,6 +504,85 @@ describe('useSettingStore', () => { }) }) }) + + describe('setMany', () => { + it('should set multiple values and make a single API call', async () => { + const onChange1 = vi.fn() + const onChange2 = vi.fn() + store.addSetting({ + id: 'Comfy.Release.Version', + name: 'Release Version', + type: 'hidden', + defaultValue: '', + onChange: onChange1 + }) + store.addSetting({ + id: 'Comfy.Release.Status', + name: 'Release Status', + type: 'hidden', + defaultValue: 'skipped', + onChange: onChange2 + }) + vi.clearAllMocks() + + await store.setMany({ + 'Comfy.Release.Version': '1.0.0', + 'Comfy.Release.Status': 'changelog seen' + }) + + expect(store.get('Comfy.Release.Version')).toBe('1.0.0') + expect(store.get('Comfy.Release.Status')).toBe('changelog seen') + expect(onChange1).toHaveBeenCalledWith('1.0.0', '') + expect(onChange2).toHaveBeenCalledWith('changelog seen', 'skipped') + expect(api.storeSettings).toHaveBeenCalledTimes(1) + expect(api.storeSettings).toHaveBeenCalledWith({ + 'Comfy.Release.Version': '1.0.0', + 'Comfy.Release.Status': 'changelog seen' + }) + expect(api.storeSetting).not.toHaveBeenCalled() + }) + + it('should skip unchanged values', async () => { + store.addSetting({ + id: 'Comfy.Release.Version', + name: 'Release Version', + type: 'hidden', + defaultValue: '' + }) + store.addSetting({ + id: 'Comfy.Release.Status', + name: 'Release Status', + type: 'hidden', + defaultValue: 'skipped' + }) + await store.set('Comfy.Release.Version', 'existing') + vi.clearAllMocks() + + await store.setMany({ + 'Comfy.Release.Version': 'existing', + 'Comfy.Release.Status': 'changelog seen' + }) + + expect(api.storeSettings).toHaveBeenCalledWith({ + 'Comfy.Release.Status': 'changelog seen' + }) + }) + + it('should not call API when all values are unchanged', async () => { + store.addSetting({ + id: 'Comfy.Release.Version', + name: 'Release Version', + type: 'hidden', + defaultValue: '' + }) + await store.set('Comfy.Release.Version', 'existing') + vi.clearAllMocks() + + await store.setMany({ 'Comfy.Release.Version': 'existing' }) + + expect(api.storeSettings).not.toHaveBeenCalled() + }) + }) }) describe('getSettingInfo', () => { diff --git a/src/platform/settings/settingStore.ts b/src/platform/settings/settingStore.ts index 1627f70c10..3bfa58b4e6 100644 --- a/src/platform/settings/settingStore.ts +++ b/src/platform/settings/settingStore.ts @@ -92,23 +92,58 @@ export const useSettingStore = defineStore('setting', () => { } /** - * Set a setting value. - * @param key - The key of the setting to set. - * @param value - The value to set. + * Apply a setting value locally: clone, migrate, fire onChange, and + * update the in-memory store. Returns the migrated value, or + * `undefined` when the value is unchanged and was skipped. */ - async function set(key: K, value: Settings[K]) { - // Clone the incoming value to prevent external mutations + function applySettingLocally( + key: K, + value: Settings[K] + ): Settings[K] | undefined { const clonedValue = _.cloneDeep(value) const newValue = tryMigrateDeprecatedValue( settingsById.value[key], clonedValue ) const oldValue = get(key) - if (newValue === oldValue) return + if (newValue === oldValue) return undefined onChange(settingsById.value[key], newValue, oldValue) settingValues.value[key] = newValue - await api.storeSetting(key, newValue) + return newValue as Settings[K] + } + + /** + * Set a setting value. + * @param key - The key of the setting to set. + * @param value - The value to set. + */ + async function set(key: K, value: Settings[K]) { + const applied = applySettingLocally(key, value) + if (applied === undefined) return + await api.storeSetting(key, applied) + } + + /** + * Set multiple setting values in a single API call. + * @param settings - A partial settings object with key-value pairs to set. + */ + async function setMany(settings: Partial) { + const updatedSettings: Partial = {} + + for (const key of Object.keys(settings) as (keyof Settings)[]) { + const applied = applySettingLocally( + key, + settings[key] as Settings[typeof key] + ) + if (applied !== undefined) { + updatedSettings[key] = applied + } + } + + if (Object.keys(updatedSettings).length > 0) { + await api.storeSettings(updatedSettings) + } } /** @@ -271,6 +306,7 @@ export const useSettingStore = defineStore('setting', () => { load, addSetting, set, + setMany, get, exists, getDefaultValue diff --git a/src/platform/updates/common/releaseStore.test.ts b/src/platform/updates/common/releaseStore.test.ts index 754ded17d9..6f045003ff 100644 --- a/src/platform/updates/common/releaseStore.test.ts +++ b/src/platform/updates/common/releaseStore.test.ts @@ -46,8 +46,9 @@ vi.mock('@/platform/settings/settingStore', () => { return null }) const set = vi.fn() + const setMany = vi.fn() return { - useSettingStore: () => ({ get, set }) + useSettingStore: () => ({ get, set, setMany }) } }) @@ -534,18 +535,11 @@ describe('useReleaseStore', () => { const settingStore = useSettingStore() await store.handleSkipRelease('1.2.0') - expect(settingStore.set).toHaveBeenCalledWith( - 'Comfy.Release.Version', - '1.2.0' - ) - expect(settingStore.set).toHaveBeenCalledWith( - 'Comfy.Release.Status', - 'skipped' - ) - expect(settingStore.set).toHaveBeenCalledWith( - 'Comfy.Release.Timestamp', - expect.any(Number) - ) + expect(settingStore.setMany).toHaveBeenCalledWith({ + 'Comfy.Release.Version': '1.2.0', + 'Comfy.Release.Status': 'skipped', + 'Comfy.Release.Timestamp': expect.any(Number) + }) }) it('should handle show changelog', async () => { @@ -554,18 +548,11 @@ describe('useReleaseStore', () => { const settingStore = useSettingStore() await store.handleShowChangelog('1.2.0') - expect(settingStore.set).toHaveBeenCalledWith( - 'Comfy.Release.Version', - '1.2.0' - ) - expect(settingStore.set).toHaveBeenCalledWith( - 'Comfy.Release.Status', - 'changelog seen' - ) - expect(settingStore.set).toHaveBeenCalledWith( - 'Comfy.Release.Timestamp', - expect.any(Number) - ) + expect(settingStore.setMany).toHaveBeenCalledWith({ + 'Comfy.Release.Version': '1.2.0', + 'Comfy.Release.Status': 'changelog seen', + 'Comfy.Release.Timestamp': expect.any(Number) + }) }) it('should handle whats new seen', async () => { @@ -574,18 +561,11 @@ describe('useReleaseStore', () => { const settingStore = useSettingStore() await store.handleWhatsNewSeen('1.2.0') - expect(settingStore.set).toHaveBeenCalledWith( - 'Comfy.Release.Version', - '1.2.0' - ) - expect(settingStore.set).toHaveBeenCalledWith( - 'Comfy.Release.Status', - "what's new seen" - ) - expect(settingStore.set).toHaveBeenCalledWith( - 'Comfy.Release.Timestamp', - expect.any(Number) - ) + expect(settingStore.setMany).toHaveBeenCalledWith({ + 'Comfy.Release.Version': '1.2.0', + 'Comfy.Release.Status': "what's new seen", + 'Comfy.Release.Timestamp': expect.any(Number) + }) }) }) diff --git a/src/platform/updates/common/releaseStore.ts b/src/platform/updates/common/releaseStore.ts index d6581b26f3..fa268ed8c3 100644 --- a/src/platform/updates/common/releaseStore.ts +++ b/src/platform/updates/common/releaseStore.ts @@ -208,9 +208,11 @@ export const useReleaseStore = defineStore('release', () => { return } - await settingStore.set('Comfy.Release.Version', version) - await settingStore.set('Comfy.Release.Status', 'skipped') - await settingStore.set('Comfy.Release.Timestamp', Date.now()) + await settingStore.setMany({ + 'Comfy.Release.Version': version, + 'Comfy.Release.Status': 'skipped', + 'Comfy.Release.Timestamp': Date.now() + }) } async function handleShowChangelog(version: string): Promise { @@ -218,9 +220,11 @@ export const useReleaseStore = defineStore('release', () => { return } - await settingStore.set('Comfy.Release.Version', version) - await settingStore.set('Comfy.Release.Status', 'changelog seen') - await settingStore.set('Comfy.Release.Timestamp', Date.now()) + await settingStore.setMany({ + 'Comfy.Release.Version': version, + 'Comfy.Release.Status': 'changelog seen', + 'Comfy.Release.Timestamp': Date.now() + }) } async function handleWhatsNewSeen(version: string): Promise { @@ -228,9 +232,11 @@ export const useReleaseStore = defineStore('release', () => { return } - await settingStore.set('Comfy.Release.Version', version) - await settingStore.set('Comfy.Release.Status', "what's new seen") - await settingStore.set('Comfy.Release.Timestamp', Date.now()) + await settingStore.setMany({ + 'Comfy.Release.Version': version, + 'Comfy.Release.Status': "what's new seen", + 'Comfy.Release.Timestamp': Date.now() + }) } // Fetch releases from API diff --git a/src/scripts/api.ts b/src/scripts/api.ts index 98c7008a7a..3bacedd8e3 100644 --- a/src/scripts/api.ts +++ b/src/scripts/api.ts @@ -1047,7 +1047,7 @@ export class ComfyApi extends EventTarget { /** * Stores a dictionary of settings for the current user */ - async storeSettings(settings: Settings) { + async storeSettings(settings: Partial) { return this.fetchApi(`/settings`, { method: 'POST', body: JSON.stringify(settings)