refactor: move settings loading state to settingStore

- Move isReady, isLoading, error state from bootstrapStore to settingStore
- Use useAsyncState in settingStore for load() action
- Rename loadSettingValues to load for consistency
- Update GraphCanvas to get settings state from settingStore
- Update tests to use createTestingPinia with stubActions: false

Amp-Thread-ID: https://ampcode.com/threads/T-019bfc95-aaa8-737d-bcd2-a5bdbc8b158f
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Alexander Brown
2026-01-26 15:16:49 -08:00
parent 51bce9239e
commit c171227d28
6 changed files with 81 additions and 67 deletions

View File

@@ -185,8 +185,9 @@ const colorPaletteStore = useColorPaletteStore()
const colorPaletteService = useColorPaletteService() const colorPaletteService = useColorPaletteService()
const canvasInteractions = useCanvasInteractions() const canvasInteractions = useCanvasInteractions()
const bootstrapStore = useBootstrapStore() const bootstrapStore = useBootstrapStore()
const { isI18nReady, i18nError, isSettingsReady, settingsError } = const { isI18nReady, i18nError } = storeToRefs(bootstrapStore)
storeToRefs(bootstrapStore) const { isReady: isSettingsReady, error: settingsError } =
storeToRefs(settingStore)
const betaMenuEnabled = computed( const betaMenuEnabled = computed(
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled' () => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'

View File

@@ -167,25 +167,30 @@ describe('useCoreCommands', () => {
return { return {
get: vi.fn().mockReturnValue(getReturnValue), get: vi.fn().mockReturnValue(getReturnValue),
addSetting: vi.fn(), addSetting: vi.fn(),
loadSettingValues: vi.fn(), load: vi.fn(),
set: vi.fn(), set: vi.fn(),
exists: vi.fn(), exists: vi.fn(),
getDefaultValue: vi.fn(), getDefaultValue: vi.fn(),
isReady: true,
isLoading: false,
error: undefined,
settingValues: {}, settingValues: {},
settingsById: {}, settingsById: {},
$id: 'setting', $id: 'setting',
$state: { $state: {
settingValues: {}, settingValues: {},
settingsById: {} settingsById: {},
isReady: true,
isLoading: false,
error: undefined
}, },
$patch: vi.fn(), $patch: vi.fn(),
$reset: vi.fn(), $reset: vi.fn(),
$subscribe: vi.fn(), $subscribe: vi.fn(),
$onAction: vi.fn(), $onAction: vi.fn(),
$dispose: vi.fn(), $dispose: vi.fn(),
_customProperties: new Set(), _customProperties: new Set()
_p: {} } satisfies ReturnType<typeof useSettingStore>
} as ReturnType<typeof useSettingStore>
} }
beforeEach(() => { beforeEach(() => {

View File

@@ -1,4 +1,5 @@
import { createPinia, setActivePinia } from 'pinia' import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import { import {
@@ -32,7 +33,7 @@ describe('useSettingStore', () => {
let store: ReturnType<typeof useSettingStore> let store: ReturnType<typeof useSettingStore>
beforeEach(() => { beforeEach(() => {
setActivePinia(createPinia()) setActivePinia(createTestingPinia({ stubActions: false }))
store = useSettingStore() store = useSettingStore()
vi.clearAllMocks() vi.clearAllMocks()
}) })
@@ -42,18 +43,18 @@ describe('useSettingStore', () => {
expect(store.settingsById).toEqual({}) expect(store.settingsById).toEqual({})
}) })
describe('loadSettingValues', () => { describe('load', () => {
it('should load settings from API', async () => { it('should load settings from API', async () => {
const mockSettings = { 'test.setting': 'value' } const mockSettings = { 'test.setting': 'value' }
vi.mocked(api.getSettings).mockResolvedValue(mockSettings as any) vi.mocked(api.getSettings).mockResolvedValue(mockSettings as any)
await store.loadSettingValues() await store.load()
expect(store.settingValues).toEqual(mockSettings) expect(store.settingValues).toEqual(mockSettings)
expect(api.getSettings).toHaveBeenCalled() expect(api.getSettings).toHaveBeenCalled()
}) })
it('should throw error if settings are loaded after registration', async () => { it('should set error if settings are loaded after registration', async () => {
const setting: SettingParams = { const setting: SettingParams = {
id: 'test.setting', id: 'test.setting',
name: 'test.setting', name: 'test.setting',
@@ -62,9 +63,14 @@ describe('useSettingStore', () => {
} }
store.addSetting(setting) store.addSetting(setting)
await expect(store.loadSettingValues()).rejects.toThrow( await store.load()
'Setting values must be loaded before any setting is registered.'
) expect(store.error).toBeInstanceOf(Error)
if (store.error instanceof Error) {
expect(store.error.message).toBe(
'Setting values must be loaded before any setting is registered.'
)
}
}) })
}) })

View File

@@ -1,4 +1,5 @@
import _ from 'es-toolkit/compat' import _ from 'es-toolkit/compat'
import { useAsyncState } from '@vueuse/core'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { compare, valid } from 'semver' import { compare, valid } from 'semver'
import { ref } from 'vue' import { ref } from 'vue'
@@ -47,6 +48,31 @@ export const useSettingStore = defineStore('setting', () => {
const settingValues = ref<Record<string, any>>({}) const settingValues = ref<Record<string, any>>({})
const settingsById = ref<Record<string, SettingParams>>({}) const settingsById = ref<Record<string, SettingParams>>({})
const {
isReady,
isLoading,
error,
execute: loadSettingValues
} = useAsyncState(
async () => {
if (Object.keys(settingsById.value).length) {
throw new Error(
'Setting values must be loaded before any setting is registered.'
)
}
settingValues.value = await api.getSettings()
await migrateZoomThresholdToFontSize()
},
undefined,
{ immediate: false }
)
async function load() {
if (!isReady.value && !isLoading.value) {
return loadSettingValues()
}
}
/** /**
* Check if a setting's value exists, i.e. if the user has set it manually. * Check if a setting's value exists, i.e. if the user has set it manually.
* @param key - The key of the setting to check. * @param key - The key of the setting to check.
@@ -188,22 +214,6 @@ export const useSettingStore = defineStore('setting', () => {
onChange(setting, get(setting.id), undefined) onChange(setting, get(setting.id), undefined)
} }
/*
* Load setting values from server.
* This needs to be called before any setting is registered.
*/
async function loadSettingValues() {
if (Object.keys(settingsById.value).length) {
throw new Error(
'Setting values must be loaded before any setting is registered.'
)
}
settingValues.value = await api.getSettings()
// Migrate old zoom threshold setting to new font size setting
await migrateZoomThresholdToFontSize()
}
/** /**
* Migrate the old zoom threshold setting to the new font size setting. * Migrate the old zoom threshold setting to the new font size setting.
* Preserves the exact zoom threshold behavior by converting it to equivalent font size. * Preserves the exact zoom threshold behavior by converting it to equivalent font size.
@@ -246,8 +256,11 @@ export const useSettingStore = defineStore('setting', () => {
return { return {
settingValues, settingValues,
settingsById, settingsById,
isReady,
isLoading,
error,
load,
addSetting, addSetting,
loadSettingValues,
set, set,
get, get,
exists, exists,

View File

@@ -1,5 +1,9 @@
import { createPinia, setActivePinia } from 'pinia' import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useBootstrapStore } from './bootstrapStore' import { useBootstrapStore } from './bootstrapStore'
@@ -16,9 +20,16 @@ vi.mock('@/i18n', () => ({
mergeCustomNodesI18n: vi.fn() mergeCustomNodesI18n: vi.fn()
})) }))
const mockIsSettingsReady = ref(false)
vi.mock('@/platform/settings/settingStore', () => ({ vi.mock('@/platform/settings/settingStore', () => ({
useSettingStore: vi.fn(() => ({ useSettingStore: vi.fn(() => ({
loadSettingValues: vi.fn().mockResolvedValue(undefined) load: vi.fn(() => {
mockIsSettingsReady.value = true
}),
isReady: mockIsSettingsReady,
isLoading: ref(false),
error: ref(undefined)
})) }))
})) }))
@@ -34,14 +45,16 @@ describe('bootstrapStore', () => {
let store: ReturnType<typeof useBootstrapStore> let store: ReturnType<typeof useBootstrapStore>
beforeEach(() => { beforeEach(() => {
setActivePinia(createPinia()) mockIsSettingsReady.value = false
setActivePinia(createTestingPinia({ stubActions: false }))
store = useBootstrapStore() store = useBootstrapStore()
vi.clearAllMocks() vi.clearAllMocks()
}) })
it('initializes with all flags false', () => { it('initializes with all flags false', () => {
const settingStore = useSettingStore()
expect(store.isNodeDefsReady).toBe(false) expect(store.isNodeDefsReady).toBe(false)
expect(store.isSettingsReady).toBe(false) expect(settingStore.isReady).toBe(false)
expect(store.isI18nReady).toBe(false) expect(store.isI18nReady).toBe(false)
}) })
@@ -58,10 +71,11 @@ describe('bootstrapStore', () => {
}) })
it('starts store bootstrap (settings, i18n)', async () => { it('starts store bootstrap (settings, i18n)', async () => {
const settingStore = useSettingStore()
void store.startStoreBootstrap() void store.startStoreBootstrap()
await vi.waitFor(() => { await vi.waitFor(() => {
expect(store.isSettingsReady).toBe(true) expect(settingStore.isReady).toBe(true)
expect(store.isI18nReady).toBe(true) expect(store.isI18nReady).toBe(true)
}) })
}) })

View File

@@ -1,11 +1,14 @@
import { useAsyncState } from '@vueuse/core' import { useAsyncState } from '@vueuse/core'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { useSettingStore } from '@/platform/settings/settingStore'
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema' import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
import { api } from '@/scripts/api' import { api } from '@/scripts/api'
import { useUserStore } from '@/stores/userStore' import { useUserStore } from '@/stores/userStore'
export const useBootstrapStore = defineStore('bootstrap', () => { export const useBootstrapStore = defineStore('bootstrap', () => {
const settingStore = useSettingStore()
const { const {
state: nodeDefs, state: nodeDefs,
isReady: isNodeDefsReady, isReady: isNodeDefsReady,
@@ -20,30 +23,6 @@ export const useBootstrapStore = defineStore('bootstrap', () => {
{ immediate: false } { immediate: false }
) )
const {
isReady: isSettingsReady,
isLoading: isSettingsLoading,
error: settingsError,
execute: executeLoadSettings
} = useAsyncState(
async () => {
const { useSettingStore } =
await import('@/platform/settings/settingStore')
await useSettingStore().loadSettingValues()
},
undefined,
{ immediate: false }
)
function loadSettings() {
// TODO: This check makes the store "sticky" across logouts. Add a reset
// method to clear isSettingsReady, then replace window.location.reload()
// with router.push() in SidebarLogoutIcon.vue
if (!isSettingsReady.value && !isSettingsLoading.value) {
void executeLoadSettings()
}
}
const { const {
isReady: isI18nReady, isReady: isI18nReady,
error: i18nError, error: i18nError,
@@ -91,7 +70,7 @@ export const useBootstrapStore = defineStore('bootstrap', () => {
void loadI18n() void loadI18n()
if (!userStore.needsLogin) { if (!userStore.needsLogin) {
loadSettings() await settingStore.load()
syncWorkflows() syncWorkflows()
} }
} }
@@ -100,13 +79,9 @@ export const useBootstrapStore = defineStore('bootstrap', () => {
nodeDefs, nodeDefs,
isNodeDefsReady, isNodeDefsReady,
nodeDefsError, nodeDefsError,
isSettingsReady,
settingsError,
isI18nReady, isI18nReady,
i18nError, i18nError,
startEarlyBootstrap, startEarlyBootstrap,
startStoreBootstrap, startStoreBootstrap
loadSettings,
syncWorkflows
} }
}) })