From e872c253b156ff8d3ee7fa0916970fb5a7e42552 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 22 Jul 2025 11:38:40 +1000 Subject: [PATCH] refactor(ui): cleaner slice definitions --- invokeai/frontend/web/src/app/store/store.ts | 191 ++++++++---------- invokeai/frontend/web/src/app/store/types.ts | 34 ++++ .../features/changeBoardModal/store/slice.ts | 14 +- .../store/canvasSettingsSlice.ts | 18 +- .../controlLayers/store/canvasSlice.ts | 26 ++- .../store/canvasStagingAreaSlice.ts | 18 +- .../controlLayers/store/lorasSlice.ts | 27 +-- .../controlLayers/store/paramsSlice.ts | 16 +- .../controlLayers/store/refImagesSlice.ts | 21 +- .../store/dynamicPromptsSlice.ts | 27 +-- .../features/gallery/store/gallerySlice.ts | 27 +-- .../store/modelManagerV2Slice.ts | 27 +-- .../src/features/nodes/store/nodesSlice.ts | 58 +++--- .../nodes/store/workflowLibrarySlice.ts | 26 +-- .../nodes/store/workflowSettingsSlice.ts | 26 +-- .../features/parameters/store/upscaleSlice.ts | 26 +-- .../src/features/queue/store/queueSlice.ts | 16 +- .../stylePresets/store/stylePresetSlice.ts | 29 +-- .../src/features/system/store/configSlice.ts | 21 +- .../src/features/system/store/systemSlice.ts | 26 +-- .../web/src/features/ui/store/uiSlice.ts | 29 +-- .../web/src/features/ui/store/uiTypes.ts | 4 +- 22 files changed, 393 insertions(+), 314 deletions(-) create mode 100644 invokeai/frontend/web/src/app/store/types.ts diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 16c3134a66..bdfbe7da9e 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -20,34 +20,31 @@ import { addSetDefaultSettingsListener } from 'app/store/middleware/listenerMidd import { addSocketConnectedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketConnected'; import { deepClone } from 'common/util/deepClone'; import { keys, mergeWith, omit, pick } from 'es-toolkit/compat'; -import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice'; -import { canvasSettingsPersistConfig, canvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice'; -import { canvasPersistConfig, canvasSlice, canvasUndoableConfig } from 'features/controlLayers/store/canvasSlice'; -import { - canvasSessionSlice, - canvasStagingAreaPersistConfig, -} from 'features/controlLayers/store/canvasStagingAreaSlice'; -import { lorasPersistConfig, lorasSlice } from 'features/controlLayers/store/lorasSlice'; -import { paramsPersistConfig, paramsSlice } from 'features/controlLayers/store/paramsSlice'; -import { refImagesPersistConfig, refImagesSlice } from 'features/controlLayers/store/refImagesSlice'; -import { dynamicPromptsPersistConfig, dynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; -import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/gallerySlice'; -import { modelManagerV2PersistConfig, modelManagerV2Slice } from 'features/modelManagerV2/store/modelManagerV2Slice'; -import { nodesPersistConfig, nodesSlice, nodesUndoableConfig } from 'features/nodes/store/nodesSlice'; -import { workflowLibraryPersistConfig, workflowLibrarySlice } from 'features/nodes/store/workflowLibrarySlice'; -import { workflowSettingsPersistConfig, workflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice'; -import { upscalePersistConfig, upscaleSlice } from 'features/parameters/store/upscaleSlice'; -import { queueSlice } from 'features/queue/store/queueSlice'; -import { stylePresetPersistConfig, stylePresetSlice } from 'features/stylePresets/store/stylePresetSlice'; -import { configSlice } from 'features/system/store/configSlice'; -import { systemPersistConfig, systemSlice } from 'features/system/store/systemSlice'; -import { uiPersistConfig, uiSlice } from 'features/ui/store/uiSlice'; +import { changeBoardModalSliceConfig } from 'features/changeBoardModal/store/slice'; +import { canvasSettingsSliceConfig } from 'features/controlLayers/store/canvasSettingsSlice'; +import { canvasSliceConfig } from 'features/controlLayers/store/canvasSlice'; +import { canvasSessionSliceConfig } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { lorasSliceConfig } from 'features/controlLayers/store/lorasSlice'; +import { paramsSliceConfig } from 'features/controlLayers/store/paramsSlice'; +import { refImagesSliceConfig } from 'features/controlLayers/store/refImagesSlice'; +import { dynamicPromptsSliceConfig } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; +import { gallerySliceConfig } from 'features/gallery/store/gallerySlice'; +import { modelManagerSliceConfig } from 'features/modelManagerV2/store/modelManagerV2Slice'; +import { nodesSliceConfig } from 'features/nodes/store/nodesSlice'; +import { workflowLibrarySliceConfig } from 'features/nodes/store/workflowLibrarySlice'; +import { workflowSettingsSliceConfig } from 'features/nodes/store/workflowSettingsSlice'; +import { upscaleSliceConfig } from 'features/parameters/store/upscaleSlice'; +import { queueSliceConfig } from 'features/queue/store/queueSlice'; +import { stylePresetSliceConfig } from 'features/stylePresets/store/stylePresetSlice'; +import { configSliceConfig } from 'features/system/store/configSlice'; +import { systemSliceConfig } from 'features/system/store/systemSlice'; +import { uiSliceConfig } from 'features/ui/store/uiSlice'; import { diff } from 'jsondiffpatch'; import { atom } from 'nanostores'; import dynamicMiddlewares from 'redux-dynamic-middlewares'; import type { SerializeFunction, UnserializeFunction } from 'redux-remember'; import { REMEMBER_PERSISTED, rememberEnhancer, rememberReducer } from 'redux-remember'; -import undoable, { newHistory } from 'redux-undo'; +import { newHistory } from 'redux-undo'; import { serializeError } from 'serialize-error'; import { api } from 'services/api'; import { authToastMiddleware } from 'services/api/authToastMiddleware'; @@ -64,90 +61,73 @@ export const listenerMiddleware = createListenerMiddleware(); const log = logger('system'); -const allReducers = { - [api.reducerPath]: api.reducer, - [gallerySlice.name]: gallerySlice.reducer, - [nodesSlice.name]: undoable(nodesSlice.reducer, nodesUndoableConfig), - [systemSlice.name]: systemSlice.reducer, - [configSlice.name]: configSlice.reducer, - [uiSlice.name]: uiSlice.reducer, - [dynamicPromptsSlice.name]: dynamicPromptsSlice.reducer, - [changeBoardModalSlice.name]: changeBoardModalSlice.reducer, - [modelManagerV2Slice.name]: modelManagerV2Slice.reducer, - [queueSlice.name]: queueSlice.reducer, - [canvasSlice.name]: undoable(canvasSlice.reducer, canvasUndoableConfig), - [workflowSettingsSlice.name]: workflowSettingsSlice.reducer, - [upscaleSlice.name]: upscaleSlice.reducer, - [stylePresetSlice.name]: stylePresetSlice.reducer, - [paramsSlice.name]: paramsSlice.reducer, - [canvasSettingsSlice.name]: canvasSettingsSlice.reducer, - [canvasSessionSlice.name]: canvasSessionSlice.reducer, - [lorasSlice.name]: lorasSlice.reducer, - [workflowLibrarySlice.name]: workflowLibrarySlice.reducer, - [refImagesSlice.name]: refImagesSlice.reducer, +const SLICE_CONFIGS = { + [canvasSessionSliceConfig.slice.name]: canvasSessionSliceConfig, + [canvasSettingsSliceConfig.slice.name]: canvasSettingsSliceConfig, + [canvasSliceConfig.slice.name]: canvasSliceConfig, + [changeBoardModalSliceConfig.slice.name]: changeBoardModalSliceConfig, + [configSliceConfig.slice.name]: configSliceConfig, + [dynamicPromptsSliceConfig.slice.name]: dynamicPromptsSliceConfig, + [gallerySliceConfig.slice.name]: gallerySliceConfig, + [lorasSliceConfig.slice.name]: lorasSliceConfig, + [modelManagerSliceConfig.slice.name]: modelManagerSliceConfig, + [nodesSliceConfig.slice.name]: nodesSliceConfig, + [paramsSliceConfig.slice.name]: paramsSliceConfig, + [queueSliceConfig.slice.name]: queueSliceConfig, + [refImagesSliceConfig.slice.name]: refImagesSliceConfig, + [stylePresetSliceConfig.slice.name]: stylePresetSliceConfig, + [systemSliceConfig.slice.name]: systemSliceConfig, + [uiSliceConfig.slice.name]: uiSliceConfig, + [upscaleSliceConfig.slice.name]: upscaleSliceConfig, + [workflowLibrarySliceConfig.slice.name]: workflowLibrarySliceConfig, + [workflowSettingsSliceConfig.slice.name]: workflowSettingsSliceConfig, }; -const rootReducer = combineReducers(allReducers); +const ALL_REDUCERS = { + [api.reducerPath]: api.reducer, + [canvasSessionSliceConfig.slice.name]: canvasSessionSliceConfig.slice.reducer, + [canvasSettingsSliceConfig.slice.name]: canvasSettingsSliceConfig.slice.reducer, + [canvasSliceConfig.slice.name]: canvasSliceConfig.slice.reducer, + [changeBoardModalSliceConfig.slice.name]: changeBoardModalSliceConfig.slice.reducer, + [configSliceConfig.slice.name]: configSliceConfig.slice.reducer, + [dynamicPromptsSliceConfig.slice.name]: dynamicPromptsSliceConfig.slice.reducer, + [gallerySliceConfig.slice.name]: gallerySliceConfig.slice.reducer, + [lorasSliceConfig.slice.name]: lorasSliceConfig.slice.reducer, + [modelManagerSliceConfig.slice.name]: modelManagerSliceConfig.slice.reducer, + [nodesSliceConfig.slice.name]: nodesSliceConfig.slice.reducer, + [paramsSliceConfig.slice.name]: paramsSliceConfig.slice.reducer, + [queueSliceConfig.slice.name]: queueSliceConfig.slice.reducer, + [refImagesSliceConfig.slice.name]: refImagesSliceConfig.slice.reducer, + [stylePresetSliceConfig.slice.name]: stylePresetSliceConfig.slice.reducer, + [systemSliceConfig.slice.name]: systemSliceConfig.slice.reducer, + [uiSliceConfig.slice.name]: uiSliceConfig.slice.reducer, + [upscaleSliceConfig.slice.name]: upscaleSliceConfig.slice.reducer, + [workflowLibrarySliceConfig.slice.name]: workflowLibrarySliceConfig.slice.reducer, + [workflowSettingsSliceConfig.slice.name]: workflowSettingsSliceConfig.slice.reducer, +}; + +const rootReducer = combineReducers(ALL_REDUCERS); const rememberedRootReducer = rememberReducer(rootReducer); -/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -export type PersistConfig = { - /** - * The name of the slice. - */ - name: keyof typeof allReducers; - /** - * The initial state of the slice. - */ - initialState: T; - /** - * Migrate the state to the current version during rehydration. - * @param state The rehydrated state. - * @returns A correctly-shaped state. - */ - migrate: (state: unknown) => T; - /** - * Keys to omit from the persisted state. - */ - persistDenylist: (keyof T)[]; -}; - -export const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = { - [galleryPersistConfig.name]: galleryPersistConfig, - [nodesPersistConfig.name]: nodesPersistConfig, - [systemPersistConfig.name]: systemPersistConfig, - [uiPersistConfig.name]: uiPersistConfig, - [dynamicPromptsPersistConfig.name]: dynamicPromptsPersistConfig, - [modelManagerV2PersistConfig.name]: modelManagerV2PersistConfig, - [canvasPersistConfig.name]: canvasPersistConfig, - [workflowSettingsPersistConfig.name]: workflowSettingsPersistConfig, - [upscalePersistConfig.name]: upscalePersistConfig, - [stylePresetPersistConfig.name]: stylePresetPersistConfig, - [paramsPersistConfig.name]: paramsPersistConfig, - [canvasSettingsPersistConfig.name]: canvasSettingsPersistConfig, - [canvasStagingAreaPersistConfig.name]: canvasStagingAreaPersistConfig, - [lorasPersistConfig.name]: lorasPersistConfig, - [workflowLibraryPersistConfig.name]: workflowLibraryPersistConfig, - [refImagesSlice.name]: refImagesPersistConfig, -}; - export const $isPendingPersist = atom(false); const unserialize: UnserializeFunction = (data, key) => { - const persistConfig = persistConfigs[key as keyof typeof persistConfigs]; - if (!persistConfig) { + const sliceConfig = SLICE_CONFIGS[key as keyof typeof SLICE_CONFIGS]; + if (!sliceConfig?.persistConfig) { throw new Error(`No persist config for slice "${key}"`); } + const { getInitialState, persistConfig, undoableConfig } = sliceConfig; let state; try { - const { initialState, migrate } = persistConfig; + const initialState = getInitialState(); + const parsed = JSON.parse(data); // strip out old keys const stripped = pick(deepClone(parsed), keys(initialState)); // run (additive) migrations - const migrated = migrate(stripped); + const migrated = persistConfig.migrate(stripped); /* * Merge in initial state as default values, covering any missing keys. You might be tempted to use _.defaultsDeep, * but that merges arrays by index and partial objects by key. Using an identity function as the customizer results @@ -158,7 +138,7 @@ const unserialize: UnserializeFunction = (data, key) => { log.debug( { persistedData: parsed, - rehydratedData: transformed, + rehydratedData: transformed as JsonObject, diff: diff(parsed, transformed) as JsonObject, // this is always serializable }, `Rehydrated slice "${key}"` @@ -169,12 +149,10 @@ const unserialize: UnserializeFunction = (data, key) => { { error: serializeError(err as Error) }, `Error rehydrating slice "${key}", falling back to default initial state` ); - state = persistConfig.initialState; + state = getInitialState(); } - // If the slice is undoable, we need to wrap it in a new history - only nodes and canvas are undoable at the moment. - // TODO(psyche): make this automatic & remove the hard-coding for specific slices. - if (key === nodesSlice.name || key === canvasSlice.name) { + if (undoableConfig) { return newHistory([], state, []); } else { return state; @@ -182,16 +160,20 @@ const unserialize: UnserializeFunction = (data, key) => { }; const serialize: SerializeFunction = (data, key) => { - const persistConfig = persistConfigs[key as keyof typeof persistConfigs]; - if (!persistConfig) { + const sliceConfig = SLICE_CONFIGS[key as keyof typeof SLICE_CONFIGS]; + if (!sliceConfig?.persistConfig) { throw new Error(`No persist config for slice "${key}"`); } // Heuristic to determine if the slice is undoable - could just hardcode it in the persistConfig const isUndoable = 'present' in data && 'past' in data && 'future' in data && '_latestUnfiltered' in data; - const result = omit(isUndoable ? data.present : data, persistConfig.persistDenylist); + + const result = omit(isUndoable ? data.present : data, sliceConfig.persistConfig.persistDenylist ?? []); return JSON.stringify(result); }; +const PERSISTED_SLICE_CONFIGS = Object.values(SLICE_CONFIGS).filter(({ persistConfig }) => !!persistConfig); +const PERSISTED_KEYS = PERSISTED_SLICE_CONFIGS.map(({ slice }) => slice.name); + export const createStore = (uniqueStoreKey?: string, persist = true) => configureStore({ reducer: rememberedRootReducer, @@ -211,7 +193,7 @@ export const createStore = (uniqueStoreKey?: string, persist = true) => const enhancers = getDefaultEnhancers(); if (persist) { const res = enhancers.prepend( - rememberEnhancer(serverBackedDriver, keys(persistConfigs), { + rememberEnhancer(serverBackedDriver, PERSISTED_KEYS, { persistDebounce: 3000, serialize, unserialize, @@ -288,9 +270,14 @@ addSetDefaultSettingsListener(startAppListening); const addPersistenceListener = (startAppListening: AppStartListening) => { startAppListening({ predicate: (action, currentRootState, originalRootState) => { - for (const { name, persistDenylist } of Object.values(persistConfigs)) { - const originalState = originalRootState[name]; - const currentState = currentRootState[name]; + for (const { slice, persistConfig } of Object.values(PERSISTED_SLICE_CONFIGS)) { + if (!persistConfig) { + // shouldn't get here, we filtered out slices without persistConfig + return false; + } + const persistDenylist: string[] = persistConfig.persistDenylist ?? []; + const originalState = originalRootState[slice.name]; + const currentState = currentRootState[slice.name]; for (const [k, v] of Object.entries(currentState)) { if (persistDenylist.includes(k)) { continue; diff --git a/invokeai/frontend/web/src/app/store/types.ts b/invokeai/frontend/web/src/app/store/types.ts new file mode 100644 index 0000000000..f5cc7211d4 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/types.ts @@ -0,0 +1,34 @@ +import type { Slice, UnknownAction } from '@reduxjs/toolkit'; +import type { UndoableOptions } from 'redux-undo'; + +export type SliceConfig = { + slice: Slice; + /** + * A function that returns the initial state of the slice. + */ + getInitialState: () => T; + /** + * The optional persist configuration for this slice. If omitted, the slice will not be persisted. + */ + persistConfig?: { + /** + * Migrate the state to the current version during rehydration. + * @param state The rehydrated state. + * @returns A correctly-shaped state. + */ + migrate: (state: unknown) => T; + /** + * Keys to omit from the persisted state. + */ + persistDenylist?: (keyof T)[]; + }; + /** + * The optional undoable configuration for this slice. If omitted, the slice will not be undoable. + */ + undoableConfig?: { + /** + * The options to be passed into redux-undo. + */ + reduxUndoOptions: UndoableOptions; + }; +}; diff --git a/invokeai/frontend/web/src/features/changeBoardModal/store/slice.ts b/invokeai/frontend/web/src/features/changeBoardModal/store/slice.ts index c7c690d38c..f7d6bffc2b 100644 --- a/invokeai/frontend/web/src/features/changeBoardModal/store/slice.ts +++ b/invokeai/frontend/web/src/features/changeBoardModal/store/slice.ts @@ -1,10 +1,15 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import type { RootState } from 'app/store/store'; +import type { SliceConfig } from 'app/store/types'; +import { deepClone } from 'common/util/deepClone'; import { initialState } from './initialState'; +import type { ChangeBoardModalState } from './types'; -export const changeBoardModalSlice = createSlice({ +const getInitialState = () => deepClone(initialState); + +export const slice = createSlice({ name: 'changeBoardModal', initialState, reducers: { @@ -21,6 +26,11 @@ export const changeBoardModalSlice = createSlice({ }, }); -export const { isModalOpenChanged, imagesToChangeSelected, changeBoardReset } = changeBoardModalSlice.actions; +export const { isModalOpenChanged, imagesToChangeSelected, changeBoardReset } = slice.actions; export const selectChangeBoardModalSlice = (state: RootState) => state.changeBoardModal; + +export const changeBoardModalSliceConfig: SliceConfig = { + slice, + getInitialState, +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts index 5830fe1f12..130ff0d4bb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts @@ -1,6 +1,7 @@ import type { PayloadAction, Selector } from '@reduxjs/toolkit'; import { createSelector, createSlice } from '@reduxjs/toolkit'; -import type { PersistConfig, RootState } from 'app/store/store'; +import type { RootState } from 'app/store/store'; +import type { SliceConfig } from 'app/store/types'; import { zRgbaColor } from 'features/controlLayers/store/types'; import { z } from 'zod'; @@ -94,7 +95,7 @@ const zCanvasSettingsState = z.object({ type CanvasSettingsState = z.infer; const getInitialState = () => zCanvasSettingsState.parse({}); -export const canvasSettingsSlice = createSlice({ +const slice = createSlice({ name: 'canvasSettings', initialState: getInitialState(), reducers: { @@ -184,18 +185,19 @@ export const { settingsRuleOfThirdsToggled, settingsSaveAllImagesToGalleryToggled, settingsStagingAreaAutoSwitchChanged, -} = canvasSettingsSlice.actions; +} = slice.actions; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ const migrate = (state: any): any => { return state; }; -export const canvasSettingsPersistConfig: PersistConfig = { - name: canvasSettingsSlice.name, - initialState: getInitialState(), - migrate, - persistDenylist: [], +export const canvasSettingsSliceConfig: SliceConfig = { + slice, + getInitialState, + persistConfig: { + migrate, + }, }; export const selectCanvasSettingsSlice = (s: RootState) => s.canvasSettings; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index f304f3a1ee..5877b813b8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -1,6 +1,6 @@ import type { PayloadAction, UnknownAction } from '@reduxjs/toolkit'; import { createSlice, isAnyOf } from '@reduxjs/toolkit'; -import type { PersistConfig } from 'app/store/store'; +import type { SliceConfig } from 'app/store/types'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { deepClone } from 'common/util/deepClone'; import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple'; @@ -95,7 +95,7 @@ import { initialT2IAdapter, } from './util'; -export const canvasSlice = createSlice({ +const slice = createSlice({ name: 'canvas', initialState: getInitialCanvasState(), reducers: { @@ -1675,20 +1675,13 @@ export const { inpaintMaskDenoiseLimitChanged, inpaintMaskDenoiseLimitDeleted, // inpaintMaskRecalled, -} = canvasSlice.actions; +} = slice.actions; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ const migrate = (state: any): any => { return state; }; -export const canvasPersistConfig: PersistConfig = { - name: canvasSlice.name, - initialState: getInitialCanvasState(), - migrate, - persistDenylist: [], -}; - const syncScaledSize = (state: CanvasState) => { if (API_BASE_MODELS.includes(state.bbox.modelBase)) { // Imagen3 has fixed sizes. Scaled bbox is not supported. @@ -1717,7 +1710,7 @@ export const canvasUndoableConfig: UndoableOptions = clearHistoryType: canvasClearHistory.type, filter: (action, _state, _history) => { // Ignore all actions from other slices - if (!action.type.startsWith(canvasSlice.name)) { + if (!action.type.startsWith(slice.name)) { return false; } // Throttle rapid actions of the same type @@ -1728,6 +1721,17 @@ export const canvasUndoableConfig: UndoableOptions = // debug: import.meta.env.MODE === 'development', }; +export const canvasSliceConfig: SliceConfig = { + slice, + getInitialState: getInitialCanvasState, + persistConfig: { + migrate, + }, + undoableConfig: { + reduxUndoOptions: canvasUndoableConfig, + }, +}; + const doNotGroupMatcher = isAnyOf(entityBrushLineAdded, entityEraserLineAdded, entityRectAdded); // Store rapid actions of the same type at most once every x time. diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts index b00d8f100f..c2e7593606 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts @@ -1,7 +1,8 @@ import { createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit'; import { EMPTY_ARRAY } from 'app/store/constants'; -import type { PersistConfig, RootState } from 'app/store/store'; +import type { RootState } from 'app/store/store'; import { useAppSelector } from 'app/store/storeHooks'; +import type { SliceConfig } from 'app/store/types'; import { deepClone } from 'common/util/deepClone'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { useMemo } from 'react'; @@ -21,7 +22,7 @@ const INITIAL_STATE: CanvasStagingAreaState = { const getInitialState = (): CanvasStagingAreaState => deepClone(INITIAL_STATE); -export const canvasSessionSlice = createSlice({ +const slice = createSlice({ name: 'canvasSession', initialState: getInitialState(), reducers: { @@ -48,7 +49,7 @@ export const canvasSessionSlice = createSlice({ }, }); -export const { canvasSessionReset, canvasQueueItemDiscarded } = canvasSessionSlice.actions; +export const { canvasSessionReset, canvasQueueItemDiscarded } = slice.actions; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ const migrate = (state: any): any => { @@ -60,14 +61,13 @@ const migrate = (state: any): any => { return state; }; -export const canvasStagingAreaPersistConfig: PersistConfig = { - name: canvasSessionSlice.name, - initialState: getInitialState(), - migrate, - persistDenylist: [], +export const canvasSessionSliceConfig: SliceConfig = { + slice, + getInitialState, + persistConfig: { migrate }, }; -export const selectCanvasSessionSlice = (s: RootState) => s[canvasSessionSlice.name]; +export const selectCanvasSessionSlice = (s: RootState) => s[slice.name]; export const selectCanvasSessionId = createSelector(selectCanvasSessionSlice, ({ canvasSessionId }) => canvasSessionId); const selectDiscardedItems = createSelector( diff --git a/invokeai/frontend/web/src/features/controlLayers/store/lorasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/lorasSlice.ts index f2b78ac046..b2baa9c72b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/lorasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/lorasSlice.ts @@ -1,6 +1,6 @@ import { createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit'; -import type { PersistConfig, RootState } from 'app/store/store'; -import { deepClone } from 'common/util/deepClone'; +import type { RootState } from 'app/store/store'; +import type { SliceConfig } from 'app/store/types'; import { paramsReset } from 'features/controlLayers/store/paramsSlice'; import type { LoRA } from 'features/controlLayers/store/types'; import { zModelIdentifierField } from 'features/nodes/types/common'; @@ -16,15 +16,15 @@ const defaultLoRAConfig: Pick = { isEnabled: true, }; -const initialState: LoRAsState = { +const getInitialState = (): LoRAsState => ({ loras: [], -}; +}); const selectLoRA = (state: LoRAsState, id: string) => state.loras.find((lora) => lora.id === id); -export const lorasSlice = createSlice({ +const slice = createSlice({ name: 'loras', - initialState, + initialState: getInitialState(), reducers: { loraAdded: { reducer: (state, action: PayloadAction<{ model: LoRAModelConfig; id: string }>) => { @@ -66,24 +66,25 @@ export const lorasSlice = createSlice({ extraReducers(builder) { builder.addCase(paramsReset, () => { // When a new session is requested, clear all LoRAs - return deepClone(initialState); + return getInitialState(); }); }, }); export const { loraAdded, loraRecalled, loraDeleted, loraWeightChanged, loraIsEnabledChanged, loraAllDeleted } = - lorasSlice.actions; + slice.actions; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ const migrate = (state: any): any => { return state; }; -export const lorasPersistConfig: PersistConfig = { - name: lorasSlice.name, - initialState, - migrate, - persistDenylist: [], +export const lorasSliceConfig: SliceConfig = { + slice, + getInitialState, + persistConfig: { + migrate, + }, }; export const selectLoRAsSlice = (state: RootState) => state.loras; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts index dc29f16fa1..a267ec057d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts @@ -1,6 +1,7 @@ import type { PayloadAction, Selector } from '@reduxjs/toolkit'; import { createSelector, createSlice } from '@reduxjs/toolkit'; -import type { PersistConfig, RootState } from 'app/store/store'; +import type { RootState } from 'app/store/store'; +import type { SliceConfig } from 'app/store/types'; import { deepClone } from 'common/util/deepClone'; import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple'; import { clamp } from 'es-toolkit/compat'; @@ -40,7 +41,7 @@ import { getGridSize, getIsSizeOptimal, getOptimalDimension } from 'features/par import { modelConfigsAdapterSelectors, selectModelConfigsQuery } from 'services/api/endpoints/models'; import { isNonRefinerMainModelConfig } from 'services/api/types'; -export const paramsSlice = createSlice({ +const slice = createSlice({ name: 'params', initialState: getInitialParamsState(), reducers: { @@ -397,18 +398,17 @@ export const { syncedToOptimalDimension, paramsReset, -} = paramsSlice.actions; +} = slice.actions; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ const migrate = (state: any): any => { return state; }; -export const paramsPersistConfig: PersistConfig = { - name: paramsSlice.name, - initialState: getInitialParamsState(), - migrate, - persistDenylist: [], +export const paramsSliceConfig: SliceConfig = { + slice, + getInitialState: getInitialParamsState, + persistConfig: { migrate }, }; export const selectParamsSlice = (state: RootState) => state.params; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts index d52f202b58..3ebb0059c4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts @@ -2,7 +2,8 @@ import { objectEquals } from '@observ33r/object-equals'; import type { PayloadAction } from '@reduxjs/toolkit'; import { createSelector, createSlice } from '@reduxjs/toolkit'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import type { PersistConfig, RootState } from 'app/store/store'; +import type { RootState } from 'app/store/store'; +import type { SliceConfig } from 'app/store/types'; import { clamp } from 'es-toolkit/compat'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { FLUXReduxImageInfluence, RefImagesState } from 'features/controlLayers/store/types'; @@ -36,7 +37,7 @@ type PayloadActionWithId = T extends void } & T >; -export const refImagesSlice = createSlice({ +export const slice = createSlice({ name: 'refImages', initialState: getInitialRefImagesState(), reducers: { @@ -263,19 +264,21 @@ export const { refImageFLUXReduxImageInfluenceChanged, refImageIsEnabledToggled, refImagesRecalled, -} = refImagesSlice.actions; +} = slice.actions; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ const migrate = (state: any): any => { return state; }; -export const refImagesPersistConfig: PersistConfig = { - name: refImagesSlice.name, - initialState: getInitialRefImagesState(), - migrate, - persistDenylist: ['selectedEntityId', 'isPanelOpen'], -}; +export const refImagesSliceConfig: SliceConfig = { + slice, + getInitialState: getInitialRefImagesState, + persistConfig: { + migrate, + persistDenylist: ['selectedEntityId', 'isPanelOpen'], + }, +} as const; export const selectRefImagesSlice = (state: RootState) => state.refImages; diff --git a/invokeai/frontend/web/src/features/dynamicPrompts/store/dynamicPromptsSlice.ts b/invokeai/frontend/web/src/features/dynamicPrompts/store/dynamicPromptsSlice.ts index 979728f3a3..8999143ec6 100644 --- a/invokeai/frontend/web/src/features/dynamicPrompts/store/dynamicPromptsSlice.ts +++ b/invokeai/frontend/web/src/features/dynamicPrompts/store/dynamicPromptsSlice.ts @@ -1,6 +1,7 @@ import type { PayloadAction, Selector } from '@reduxjs/toolkit'; import { createSelector, createSlice } from '@reduxjs/toolkit'; -import type { PersistConfig, RootState } from 'app/store/store'; +import type { RootState } from 'app/store/store'; +import type { SliceConfig } from 'app/store/types'; import { buildZodTypeGuard } from 'common/util/zodUtils'; import { z } from 'zod'; @@ -19,7 +20,7 @@ export interface DynamicPromptsState { seedBehaviour: SeedBehaviour; } -const initialDynamicPromptsState: DynamicPromptsState = { +const getInitialState = (): DynamicPromptsState => ({ _version: 1, maxPrompts: 100, combinatorial: true, @@ -28,11 +29,11 @@ const initialDynamicPromptsState: DynamicPromptsState = { isError: false, isLoading: false, seedBehaviour: 'PER_ITERATION', -}; +}); -export const dynamicPromptsSlice = createSlice({ +export const slice = createSlice({ name: 'dynamicPrompts', - initialState: initialDynamicPromptsState, + initialState: getInitialState(), reducers: { maxPromptsChanged: (state, action: PayloadAction) => { state.maxPrompts = action.payload; @@ -63,21 +64,23 @@ export const { isErrorChanged, isLoadingChanged, seedBehaviourChanged, -} = dynamicPromptsSlice.actions; +} = slice.actions; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -const migrateDynamicPromptsState = (state: any): any => { +const migrate = (state: any): any => { if (!('_version' in state)) { state._version = 1; } return state; }; -export const dynamicPromptsPersistConfig: PersistConfig = { - name: dynamicPromptsSlice.name, - initialState: initialDynamicPromptsState, - migrate: migrateDynamicPromptsState, - persistDenylist: ['prompts', 'parsingError', 'isError', 'isLoading'], +export const dynamicPromptsSliceConfig: SliceConfig = { + slice, + getInitialState, + persistConfig: { + migrate, + persistDenylist: ['prompts', 'parsingError', 'isError', 'isLoading'], + }, }; export const selectDynamicPromptsSlice = (state: RootState) => state.dynamicPrompts; diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts index 35b2f204bb..9a8e4ee6df 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts @@ -1,13 +1,14 @@ import { objectEquals } from '@observ33r/object-equals'; import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; -import type { PersistConfig, RootState } from 'app/store/store'; +import type { RootState } from 'app/store/store'; +import type { SliceConfig } from 'app/store/types'; import { uniq } from 'es-toolkit/compat'; import type { BoardRecordOrderBy } from 'services/api/types'; import type { BoardId, ComparisonMode, GalleryState, GalleryView, OrderDir } from './types'; -const initialGalleryState: GalleryState = { +const getInitialState = (): GalleryState => ({ selection: [], shouldAutoSwitch: true, autoAssignBoardOnClick: true, @@ -26,11 +27,11 @@ const initialGalleryState: GalleryState = { shouldShowArchivedBoards: false, boardsListOrderBy: 'created_at', boardsListOrderDir: 'DESC', -}; +}); -export const gallerySlice = createSlice({ +const slice = createSlice({ name: 'gallery', - initialState: initialGalleryState, + initialState: getInitialState(), reducers: { imageSelected: (state, action: PayloadAction) => { // Let's be efficient here and not update the selection unless it has actually changed. This helps to prevent @@ -187,21 +188,23 @@ export const { searchTermChanged, boardsListOrderByChanged, boardsListOrderDirChanged, -} = gallerySlice.actions; +} = slice.actions; export const selectGallerySlice = (state: RootState) => state.gallery; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -const migrateGalleryState = (state: any): any => { +const migrate = (state: any): any => { if (!('_version' in state)) { state._version = 1; } return state; }; -export const galleryPersistConfig: PersistConfig = { - name: gallerySlice.name, - initialState: initialGalleryState, - migrate: migrateGalleryState, - persistDenylist: ['selection', 'selectedBoardId', 'galleryView', 'imageToCompare'], +export const gallerySliceConfig: SliceConfig = { + slice, + getInitialState, + persistConfig: { + migrate, + persistDenylist: ['selection', 'selectedBoardId', 'galleryView', 'imageToCompare'], + }, }; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/store/modelManagerV2Slice.ts b/invokeai/frontend/web/src/features/modelManagerV2/store/modelManagerV2Slice.ts index ec77205711..0d2e5239c7 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/store/modelManagerV2Slice.ts +++ b/invokeai/frontend/web/src/features/modelManagerV2/store/modelManagerV2Slice.ts @@ -1,6 +1,7 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSelector, createSlice } from '@reduxjs/toolkit'; -import type { PersistConfig, RootState } from 'app/store/store'; +import type { RootState } from 'app/store/store'; +import type { SliceConfig } from 'app/store/types'; import type { ModelType } from 'services/api/types'; export type FilterableModelType = Exclude | 'refiner'; @@ -15,7 +16,7 @@ type ModelManagerState = { shouldInstallInPlace: boolean; }; -const initialModelManagerState: ModelManagerState = { +const getInitialState = (): ModelManagerState => ({ _version: 1, selectedModelKey: null, selectedModelMode: 'view', @@ -23,11 +24,11 @@ const initialModelManagerState: ModelManagerState = { searchTerm: '', scanPath: undefined, shouldInstallInPlace: true, -}; +}); -export const modelManagerV2Slice = createSlice({ +const slice = createSlice({ name: 'modelmanagerV2', - initialState: initialModelManagerState, + initialState: getInitialState(), reducers: { setSelectedModelKey: (state, action: PayloadAction) => { state.selectedModelMode = 'view'; @@ -58,21 +59,23 @@ export const { setSelectedModelMode, setScanPath, shouldInstallInPlaceChanged, -} = modelManagerV2Slice.actions; +} = slice.actions; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -const migrateModelManagerState = (state: any): any => { +const migrate = (state: any): any => { if (!('_version' in state)) { state._version = 1; } return state; }; -export const modelManagerV2PersistConfig: PersistConfig = { - name: modelManagerV2Slice.name, - initialState: initialModelManagerState, - migrate: migrateModelManagerState, - persistDenylist: ['selectedModelKey', 'selectedModelMode', 'filteredModelType', 'searchTerm'], +export const modelManagerSliceConfig: SliceConfig = { + slice, + getInitialState, + persistConfig: { + migrate, + persistDenylist: ['selectedModelKey', 'selectedModelMode', 'filteredModelType', 'searchTerm'], + }, }; export const selectModelManagerV2Slice = (state: RootState) => state.modelmanagerV2; diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 018ce29b4b..f273437251 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -11,7 +11,7 @@ import type { XYPosition, } from '@xyflow/react'; import { applyEdgeChanges, applyNodeChanges, getConnectedEdges, getIncomers, getOutgoers } from '@xyflow/react'; -import type { PersistConfig } from 'app/store/store'; +import type { SliceConfig } from 'app/store/types'; import { deepClone } from 'common/util/deepClone'; import { addElement, @@ -151,11 +151,11 @@ export const getInitialWorkflow = (): Omit ({ _version: 1, formFieldInitialValues: {}, ...getInitialWorkflow(), -}; +}); type FieldValueAction = PayloadAction<{ nodeId: string; @@ -208,9 +208,9 @@ const fieldValueReducer = ( field.value = result.data; }; -export const nodesSlice = createSlice({ +const slice = createSlice({ name: 'nodes', - initialState: initialState, + initialState: getInitialState(), reducers: { nodesChanged: (state, action: PayloadAction[]>) => { // In v12.7.0, @xyflow/react added a `domAttributes` property to the node data. One DOM attribute is @@ -588,7 +588,7 @@ export const nodesSlice = createSlice({ } node.data.notes = value; }, - nodeEditorReset: () => deepClone(initialState), + nodeEditorReset: () => getInitialState(), workflowNameChanged: (state, action: PayloadAction) => { state.name = action.payload; }, @@ -673,7 +673,7 @@ export const nodesSlice = createSlice({ const formFieldInitialValues = getFormFieldInitialValues(workflowExtra.form, nodes); return { - ...deepClone(initialState), + ...getInitialState(), ...deepClone(workflowExtra), formFieldInitialValues, nodes: nodes.map((node) => ({ ...SHARED_NODE_PROPERTIES, ...node })), @@ -758,7 +758,15 @@ export const { workflowLoaded, undo, redo, -} = nodesSlice.actions; +} = slice.actions; + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +const migrate = (state: any): any => { + if (!('_version' in state)) { + state._version = 1; + } + return state; +}; export const $cursorPos = atom(null); export const $templates = atom({}); @@ -775,21 +783,6 @@ export const $lastEdgeUpdateMouseEvent = atom(null); export const $viewport = atom({ x: 0, y: 0, zoom: 1 }); export const $addNodeCmdk = atom(false); -/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -const migrateNodesState = (state: any): any => { - if (!('_version' in state)) { - state._version = 1; - } - return state; -}; - -export const nodesPersistConfig: PersistConfig = { - name: nodesSlice.name, - initialState: initialState, - migrate: migrateNodesState, - persistDenylist: [], -}; - type NodeSelectionAction = { type: ReturnType['type']; payload: NodeSelectionChange[]; @@ -893,10 +886,10 @@ const isHighFrequencyWorkflowDetailsAction = isAnyOf( // a note in a notes node, we don't want to create a new undo group for every keystroke. const isHighFrequencyNodeScopedAction = isAnyOf(nodeLabelChanged, nodeNotesChanged, notesNodeValueChanged); -export const nodesUndoableConfig: UndoableOptions = { +const reduxUndoOptions: UndoableOptions = { limit: 64, - undoType: nodesSlice.actions.undo.type, - redoType: nodesSlice.actions.redo.type, + undoType: slice.actions.undo.type, + redoType: slice.actions.redo.type, groupBy: (action, _state, _history) => { if (isHighFrequencyFieldChangeAction(action)) { // Group by type, node id and field name @@ -928,7 +921,7 @@ export const nodesUndoableConfig: UndoableOptions = { }, filter: (action, _state, _history) => { // Ignore all actions from other slices - if (!action.type.startsWith(nodesSlice.name)) { + if (!action.type.startsWith(slice.name)) { return false; } // Ignore actions that only select or deselect nodes and edges @@ -943,6 +936,17 @@ export const nodesUndoableConfig: UndoableOptions = { }, }; +export const nodesSliceConfig: SliceConfig = { + slice, + getInitialState, + persistConfig: { + migrate, + }, + undoableConfig: { + reduxUndoOptions, + }, +}; + // The form builder's initial values are based on the current values of the node fields in the workflow. export const getFormFieldInitialValues = (form: BuilderForm, nodes: NodesState['nodes']) => { const formFieldInitialValues: Record = {}; diff --git a/invokeai/frontend/web/src/features/nodes/store/workflowLibrarySlice.ts b/invokeai/frontend/web/src/features/nodes/store/workflowLibrarySlice.ts index 097b76bfb7..2020de7448 100644 --- a/invokeai/frontend/web/src/features/nodes/store/workflowLibrarySlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/workflowLibrarySlice.ts @@ -1,6 +1,7 @@ import type { PayloadAction, Selector } from '@reduxjs/toolkit'; import { createSelector, createSlice } from '@reduxjs/toolkit'; -import type { PersistConfig, RootState } from 'app/store/store'; +import type { RootState } from 'app/store/store'; +import type { SliceConfig } from 'app/store/types'; import type { WorkflowMode } from 'features/nodes/store/types'; import type { WorkflowCategory } from 'features/nodes/types/workflow'; import { atom, computed } from 'nanostores'; @@ -17,18 +18,18 @@ type WorkflowLibraryState = { selectedTags: string[]; }; -const initialWorkflowLibraryState: WorkflowLibraryState = { +const getInitialState = (): WorkflowLibraryState => ({ mode: 'view', searchTerm: '', orderBy: 'opened_at', direction: 'DESC', selectedTags: [], view: 'defaults', -}; +}); -export const workflowLibrarySlice = createSlice({ +const slice = createSlice({ name: 'workflowLibrary', - initialState: initialWorkflowLibraryState, + initialState: getInitialState(), reducers: { workflowModeChanged: (state, action: PayloadAction) => { state.mode = action.payload; @@ -73,16 +74,17 @@ export const { workflowLibraryTagToggled, workflowLibraryTagsReset, workflowLibraryViewChanged, -} = workflowLibrarySlice.actions; +} = slice.actions; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -const migrateWorkflowLibraryState = (state: any): any => state; +const migrate = (state: any): any => state; -export const workflowLibraryPersistConfig: PersistConfig = { - name: workflowLibrarySlice.name, - initialState: initialWorkflowLibraryState, - migrate: migrateWorkflowLibraryState, - persistDenylist: [], +export const workflowLibrarySliceConfig: SliceConfig = { + slice, + getInitialState, + persistConfig: { + migrate, + }, }; const selectWorkflowLibrarySlice = (state: RootState) => state.workflowLibrary; diff --git a/invokeai/frontend/web/src/features/nodes/store/workflowSettingsSlice.ts b/invokeai/frontend/web/src/features/nodes/store/workflowSettingsSlice.ts index 76ec124961..f5cafd39f0 100644 --- a/invokeai/frontend/web/src/features/nodes/store/workflowSettingsSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/workflowSettingsSlice.ts @@ -1,7 +1,8 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSelector, createSlice } from '@reduxjs/toolkit'; import { SelectionMode } from '@xyflow/react'; -import type { PersistConfig, RootState } from 'app/store/store'; +import type { RootState } from 'app/store/store'; +import type { SliceConfig } from 'app/store/types'; import type { Selector } from 'react-redux'; import z from 'zod'; @@ -29,7 +30,7 @@ export type WorkflowSettingsState = { selectionMode: SelectionMode; }; -const initialState: WorkflowSettingsState = { +const getInitialState = (): WorkflowSettingsState => ({ _version: 1, shouldShowMinimapPanel: true, layeringStrategy: 'network-simplex', @@ -44,11 +45,11 @@ const initialState: WorkflowSettingsState = { shouldShowEdgeLabels: false, nodeOpacity: 1, selectionMode: SelectionMode.Partial, -}; +}); -export const workflowSettingsSlice = createSlice({ +const slice = createSlice({ name: 'workflowSettings', - initialState, + initialState: getInitialState(), reducers: { shouldShowMinimapPanelChanged: (state, action: PayloadAction) => { state.shouldShowMinimapPanel = action.payload; @@ -106,21 +107,22 @@ export const { shouldValidateGraphChanged, nodeOpacityChanged, selectionModeChanged, -} = workflowSettingsSlice.actions; +} = slice.actions; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -const migrateWorkflowSettingsState = (state: any): any => { +const migrate = (state: any): any => { if (!('_version' in state)) { state._version = 1; } return state; }; -export const workflowSettingsPersistConfig: PersistConfig = { - name: workflowSettingsSlice.name, - initialState, - migrate: migrateWorkflowSettingsState, - persistDenylist: [], +export const workflowSettingsSliceConfig: SliceConfig = { + slice, + getInitialState, + persistConfig: { + migrate, + }, }; export const selectWorkflowSettingsSlice = (state: RootState) => state.workflowSettings; diff --git a/invokeai/frontend/web/src/features/parameters/store/upscaleSlice.ts b/invokeai/frontend/web/src/features/parameters/store/upscaleSlice.ts index 294d88fb48..33d5ddc328 100644 --- a/invokeai/frontend/web/src/features/parameters/store/upscaleSlice.ts +++ b/invokeai/frontend/web/src/features/parameters/store/upscaleSlice.ts @@ -1,6 +1,7 @@ import type { PayloadAction, Selector } from '@reduxjs/toolkit'; import { createSelector, createSlice } from '@reduxjs/toolkit'; -import type { PersistConfig, RootState } from 'app/store/store'; +import type { RootState } from 'app/store/store'; +import type { SliceConfig } from 'app/store/types'; import type { ParameterSpandrelImageToImageModel } from 'features/parameters/types/parameterSchemas'; import type { ControlNetModelConfig, ImageDTO } from 'services/api/types'; @@ -17,7 +18,7 @@ export interface UpscaleState { tileOverlap: number; } -const initialUpscaleState: UpscaleState = { +const getInitialState = (): UpscaleState => ({ _version: 1, upscaleModel: null, upscaleInitialImage: null, @@ -28,11 +29,11 @@ const initialUpscaleState: UpscaleState = { postProcessingModel: null, tileSize: 1024, tileOverlap: 128, -}; +}); -export const upscaleSlice = createSlice({ +export const slice = createSlice({ name: 'upscale', - initialState: initialUpscaleState, + initialState: getInitialState(), reducers: { upscaleModelChanged: (state, action: PayloadAction) => { state.upscaleModel = action.payload; @@ -74,21 +75,22 @@ export const { postProcessingModelChanged, tileSizeChanged, tileOverlapChanged, -} = upscaleSlice.actions; +} = slice.actions; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -const migrateUpscaleState = (state: any): any => { +const migrate = (state: any): any => { if (!('_version' in state)) { state._version = 1; } return state; }; -export const upscalePersistConfig: PersistConfig = { - name: upscaleSlice.name, - initialState: initialUpscaleState, - migrate: migrateUpscaleState, - persistDenylist: [], +export const upscaleSliceConfig: SliceConfig = { + slice, + getInitialState, + persistConfig: { + migrate, + }, }; export const selectUpscaleSlice = (state: RootState) => state.upscale; diff --git a/invokeai/frontend/web/src/features/queue/store/queueSlice.ts b/invokeai/frontend/web/src/features/queue/store/queueSlice.ts index 9f0225149d..29812b32cc 100644 --- a/invokeai/frontend/web/src/features/queue/store/queueSlice.ts +++ b/invokeai/frontend/web/src/features/queue/store/queueSlice.ts @@ -1,6 +1,7 @@ import type { PayloadAction, Selector } from '@reduxjs/toolkit'; import { createSelector, createSlice } from '@reduxjs/toolkit'; import type { RootState } from 'app/store/store'; +import type { SliceConfig } from 'app/store/types'; interface QueueState { listCursor: number | undefined; @@ -9,16 +10,16 @@ interface QueueState { resumeProcessorOnEnqueue: boolean; } -const initialQueueState: QueueState = { +const getInitialState = (): QueueState => ({ listCursor: undefined, listPriority: undefined, selectedQueueItem: undefined, resumeProcessorOnEnqueue: true, -}; +}); -export const queueSlice = createSlice({ +const slice = createSlice({ name: 'queue', - initialState: initialQueueState, + initialState: getInitialState(), reducers: { listCursorChanged: (state, action: PayloadAction) => { state.listCursor = action.payload; @@ -33,7 +34,12 @@ export const queueSlice = createSlice({ }, }); -export const { listCursorChanged, listPriorityChanged, listParamsReset } = queueSlice.actions; +export const { listCursorChanged, listPriorityChanged, listParamsReset } = slice.actions; + +export const queueSliceConfig: SliceConfig = { + slice, + getInitialState, +}; const selectQueueSlice = (state: RootState) => state.queue; const createQueueSelector = (selector: Selector) => createSelector(selectQueueSlice, selector); diff --git a/invokeai/frontend/web/src/features/stylePresets/store/stylePresetSlice.ts b/invokeai/frontend/web/src/features/stylePresets/store/stylePresetSlice.ts index df93d82c63..2753dee2f5 100644 --- a/invokeai/frontend/web/src/features/stylePresets/store/stylePresetSlice.ts +++ b/invokeai/frontend/web/src/features/stylePresets/store/stylePresetSlice.ts @@ -1,23 +1,23 @@ import type { PayloadAction, Selector } from '@reduxjs/toolkit'; import { createSelector, createSlice } from '@reduxjs/toolkit'; -import type { PersistConfig, RootState } from 'app/store/store'; -import { deepClone } from 'common/util/deepClone'; +import type { RootState } from 'app/store/store'; +import type { SliceConfig } from 'app/store/types'; import { paramsReset } from 'features/controlLayers/store/paramsSlice'; import { atom } from 'nanostores'; import { stylePresetsApi } from 'services/api/endpoints/stylePresets'; import type { StylePresetState } from './types'; -const initialState: StylePresetState = { +const getInitialState = (): StylePresetState => ({ activeStylePresetId: null, searchTerm: '', viewMode: false, showPromptPreviews: false, -}; +}); -export const stylePresetSlice = createSlice({ +export const slice = createSlice({ name: 'stylePreset', - initialState: initialState, + initialState: getInitialState(), reducers: { activeStylePresetIdChanged: (state, action: PayloadAction) => { state.activeStylePresetId = action.payload; @@ -34,7 +34,7 @@ export const stylePresetSlice = createSlice({ }, extraReducers(builder) { builder.addCase(paramsReset, () => { - return deepClone(initialState); + return getInitialState(); }); builder.addMatcher(stylePresetsApi.endpoints.deleteStylePreset.matchFulfilled, (state, action) => { if (state.activeStylePresetId === null) { @@ -58,21 +58,22 @@ export const stylePresetSlice = createSlice({ }); export const { activeStylePresetIdChanged, searchTermChanged, viewModeChanged, showPromptPreviewsChanged } = - stylePresetSlice.actions; + slice.actions; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -const migrateStylePresetState = (state: any): any => { +const migrate = (state: any): any => { if (!('_version' in state)) { state._version = 1; } return state; }; -export const stylePresetPersistConfig: PersistConfig = { - name: stylePresetSlice.name, - initialState, - migrate: migrateStylePresetState, - persistDenylist: [], +export const stylePresetSliceConfig: SliceConfig = { + slice, + getInitialState, + persistConfig: { + migrate, + }, }; export const selectStylePresetSlice = (state: RootState) => state.stylePreset; diff --git a/invokeai/frontend/web/src/features/system/store/configSlice.ts b/invokeai/frontend/web/src/features/system/store/configSlice.ts index 7bd392559f..d8c365d56e 100644 --- a/invokeai/frontend/web/src/features/system/store/configSlice.ts +++ b/invokeai/frontend/web/src/features/system/store/configSlice.ts @@ -1,6 +1,7 @@ import type { PayloadAction, Selector } from '@reduxjs/toolkit'; import { createSelector, createSlice } from '@reduxjs/toolkit'; import type { RootState } from 'app/store/store'; +import type { SliceConfig } from 'app/store/types'; import type { AppConfig, NumericalParameterConfig, PartialAppConfig } from 'app/types/invokeai'; import { merge } from 'es-toolkit/compat'; @@ -14,7 +15,9 @@ const baseDimensionConfig: NumericalParameterConfig = { coarseStep: 64, }; -const initialConfigState: AppConfig & { didLoad: boolean } = { +type ConfigState = AppConfig & { didLoad: boolean }; + +const getInitialState = (): ConfigState => ({ didLoad: false, isLocal: true, shouldUpdateImagesOnConnect: false, @@ -183,11 +186,11 @@ const initialConfigState: AppConfig & { didLoad: boolean } = { coarseStep: 0.5, }, }, -}; +}); -export const configSlice = createSlice({ +const slice = createSlice({ name: 'config', - initialState: initialConfigState, + initialState: getInitialState(), reducers: { configChanged: (state, action: PayloadAction) => { merge(state, action.payload); @@ -196,11 +199,15 @@ export const configSlice = createSlice({ }, }); -export const { configChanged } = configSlice.actions; +export const { configChanged } = slice.actions; + +export const configSliceConfig: SliceConfig = { + slice, + getInitialState, +}; export const selectConfigSlice = (state: RootState) => state.config; -const createConfigSelector = (selector: Selector) => - createSelector(selectConfigSlice, selector); +const createConfigSelector = (selector: Selector) => createSelector(selectConfigSlice, selector); export const selectWidthConfig = createConfigSelector((config) => config.sd.width); export const selectHeightConfig = createConfigSelector((config) => config.sd.height); diff --git a/invokeai/frontend/web/src/features/system/store/systemSlice.ts b/invokeai/frontend/web/src/features/system/store/systemSlice.ts index 1a0a3426d2..5839d5f2f2 100644 --- a/invokeai/frontend/web/src/features/system/store/systemSlice.ts +++ b/invokeai/frontend/web/src/features/system/store/systemSlice.ts @@ -3,12 +3,13 @@ import { createSelector, createSlice } from '@reduxjs/toolkit'; import type { LogNamespace } from 'app/logging/logger'; import { zLogNamespace } from 'app/logging/logger'; import { EMPTY_ARRAY } from 'app/store/constants'; -import type { PersistConfig, RootState } from 'app/store/store'; +import type { RootState } from 'app/store/store'; +import type { SliceConfig } from 'app/store/types'; import { uniq } from 'es-toolkit/compat'; import type { Language, SystemState } from './types'; -const initialSystemState: SystemState = { +const getInitialState = (): SystemState => ({ _version: 2, shouldConfirmOnDelete: true, shouldAntialiasProgressImage: false, @@ -23,11 +24,11 @@ const initialSystemState: SystemState = { logNamespaces: [...zLogNamespace.options], shouldShowInvocationProgressDetail: false, shouldHighlightFocusedRegions: false, -}; +}); -export const systemSlice = createSlice({ +export const slice = createSlice({ name: 'system', - initialState: initialSystemState, + initialState: getInitialState(), reducers: { setShouldConfirmOnDelete: (state, action: PayloadAction) => { state.shouldConfirmOnDelete = action.payload; @@ -89,10 +90,10 @@ export const { shouldConfirmOnNewSessionToggled, setShouldShowInvocationProgressDetail, setShouldHighlightFocusedRegions, -} = systemSlice.actions; +} = slice.actions; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -const migrateSystemState = (state: any): any => { +const migrate = (state: any): any => { if (!('_version' in state)) { state._version = 1; } @@ -103,11 +104,12 @@ const migrateSystemState = (state: any): any => { return state; }; -export const systemPersistConfig: PersistConfig = { - name: systemSlice.name, - initialState: initialSystemState, - migrate: migrateSystemState, - persistDenylist: [], +export const systemSliceConfig: SliceConfig = { + slice, + getInitialState, + persistConfig: { + migrate, + }, }; export const selectSystemSlice = (state: RootState) => state.system; diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts index c124a9a382..7a932f4901 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts @@ -1,13 +1,16 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; -import type { PersistConfig, RootState } from 'app/store/store'; +import type { RootState } from 'app/store/store'; +import type { SliceConfig } from 'app/store/types'; +import { deepClone } from 'common/util/deepClone'; -import type { UIState } from './uiTypes'; -import { getInitialUIState } from './uiTypes'; +import { INITIAL_STATE, type UIState } from './uiTypes'; -export const uiSlice = createSlice({ +export const getInitialState = (): UIState => deepClone(INITIAL_STATE); + +const slice = createSlice({ name: 'ui', - initialState: getInitialUIState(), + initialState: getInitialState(), reducers: { setActiveTab: (state, action: PayloadAction) => { state.activeTab = action.payload; @@ -81,12 +84,12 @@ export const { textAreaSizesStateChanged, dockviewStorageKeyChanged, pickerCompactViewStateChanged, -} = uiSlice.actions; +} = slice.actions; export const selectUiSlice = (state: RootState) => state.ui; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -const migrateUIState = (state: any): any => { +const migrate = (state: any): any => { if (!('_version' in state)) { state._version = 1; } @@ -101,9 +104,11 @@ const migrateUIState = (state: any): any => { return state; }; -export const uiPersistConfig: PersistConfig = { - name: uiSlice.name, - initialState: getInitialUIState(), - migrate: migrateUIState, - persistDenylist: ['shouldShowImageDetails'], +export const uiSliceConfig: SliceConfig = { + slice, + getInitialState, + persistConfig: { + migrate, + persistDenylist: ['shouldShowImageDetails'], + }, }; diff --git a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts index b7cd02d93c..044ea4e3ef 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts @@ -1,4 +1,3 @@ -import { deepClone } from 'common/util/deepClone'; import { isPlainObject } from 'es-toolkit'; import { z } from 'zod'; @@ -25,6 +24,5 @@ const zUIState = z.object({ shouldShowNotificationV2: z.boolean().default(true), pickerCompactViewStates: z.record(z.string(), z.boolean()).default(() => ({})), }); -const INITIAL_STATE = zUIState.parse({}); +export const INITIAL_STATE = zUIState.parse({}); export type UIState = z.infer; -export const getInitialUIState = (): UIState => deepClone(INITIAL_STATE);