refactor(ui): cleaner slice definitions

This commit is contained in:
psychedelicious
2025-07-22 11:38:40 +10:00
parent 28633c9983
commit e872c253b1
22 changed files with 393 additions and 314 deletions

View File

@@ -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<T = any> = {
/**
* 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;

View File

@@ -0,0 +1,34 @@
import type { Slice, UnknownAction } from '@reduxjs/toolkit';
import type { UndoableOptions } from 'redux-undo';
export type SliceConfig<T> = {
slice: Slice<T>;
/**
* 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<T, UnknownAction>;
};
};

View File

@@ -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<ChangeBoardModalState> = {
slice,
getInitialState,
};

View File

@@ -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<typeof zCanvasSettingsState>;
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<CanvasSettingsState> = {
name: canvasSettingsSlice.name,
initialState: getInitialState(),
migrate,
persistDenylist: [],
export const canvasSettingsSliceConfig: SliceConfig<CanvasSettingsState> = {
slice,
getInitialState,
persistConfig: {
migrate,
},
};
export const selectCanvasSettingsSlice = (s: RootState) => s.canvasSettings;

View File

@@ -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<CanvasState> = {
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<CanvasState, UnknownAction> =
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<CanvasState, UnknownAction> =
// debug: import.meta.env.MODE === 'development',
};
export const canvasSliceConfig: SliceConfig<CanvasState> = {
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.

View File

@@ -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<CanvasStagingAreaState> = {
name: canvasSessionSlice.name,
initialState: getInitialState(),
migrate,
persistDenylist: [],
export const canvasSessionSliceConfig: SliceConfig<CanvasStagingAreaState> = {
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(

View File

@@ -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<LoRA, 'weight' | 'isEnabled'> = {
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<LoRAsState> = {
name: lorasSlice.name,
initialState,
migrate,
persistDenylist: [],
export const lorasSliceConfig: SliceConfig<LoRAsState> = {
slice,
getInitialState,
persistConfig: {
migrate,
},
};
export const selectLoRAsSlice = (state: RootState) => state.loras;

View File

@@ -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<ParamsState> = {
name: paramsSlice.name,
initialState: getInitialParamsState(),
migrate,
persistDenylist: [],
export const paramsSliceConfig: SliceConfig<ParamsState> = {
slice,
getInitialState: getInitialParamsState,
persistConfig: { migrate },
};
export const selectParamsSlice = (state: RootState) => state.params;

View File

@@ -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 = void> = 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<RefImagesState> = {
name: refImagesSlice.name,
initialState: getInitialRefImagesState(),
migrate,
persistDenylist: ['selectedEntityId', 'isPanelOpen'],
};
export const refImagesSliceConfig: SliceConfig<RefImagesState> = {
slice,
getInitialState: getInitialRefImagesState,
persistConfig: {
migrate,
persistDenylist: ['selectedEntityId', 'isPanelOpen'],
},
} as const;
export const selectRefImagesSlice = (state: RootState) => state.refImages;

View File

@@ -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<number>) => {
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<DynamicPromptsState> = {
name: dynamicPromptsSlice.name,
initialState: initialDynamicPromptsState,
migrate: migrateDynamicPromptsState,
persistDenylist: ['prompts', 'parsingError', 'isError', 'isLoading'],
export const dynamicPromptsSliceConfig: SliceConfig<DynamicPromptsState> = {
slice,
getInitialState,
persistConfig: {
migrate,
persistDenylist: ['prompts', 'parsingError', 'isError', 'isLoading'],
},
};
export const selectDynamicPromptsSlice = (state: RootState) => state.dynamicPrompts;

View File

@@ -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<string | null>) => {
// 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<GalleryState> = {
name: gallerySlice.name,
initialState: initialGalleryState,
migrate: migrateGalleryState,
persistDenylist: ['selection', 'selectedBoardId', 'galleryView', 'imageToCompare'],
export const gallerySliceConfig: SliceConfig<GalleryState> = {
slice,
getInitialState,
persistConfig: {
migrate,
persistDenylist: ['selection', 'selectedBoardId', 'galleryView', 'imageToCompare'],
},
};

View File

@@ -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<ModelType, 'onnx'> | '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<string | null>) => {
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<ModelManagerState> = {
name: modelManagerV2Slice.name,
initialState: initialModelManagerState,
migrate: migrateModelManagerState,
persistDenylist: ['selectedModelKey', 'selectedModelMode', 'filteredModelType', 'searchTerm'],
export const modelManagerSliceConfig: SliceConfig<ModelManagerState> = {
slice,
getInitialState,
persistConfig: {
migrate,
persistDenylist: ['selectedModelKey', 'selectedModelMode', 'filteredModelType', 'searchTerm'],
},
};
export const selectModelManagerV2Slice = (state: RootState) => state.modelmanagerV2;

View File

@@ -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<NodesState, 'mode' | 'formFieldInitia
};
};
const initialState: NodesState = {
const getInitialState = (): NodesState => ({
_version: 1,
formFieldInitialValues: {},
...getInitialWorkflow(),
};
});
type FieldValueAction<T extends FieldValue> = PayloadAction<{
nodeId: string;
@@ -208,9 +208,9 @@ const fieldValueReducer = <T extends FieldValue>(
field.value = result.data;
};
export const nodesSlice = createSlice({
const slice = createSlice({
name: 'nodes',
initialState: initialState,
initialState: getInitialState(),
reducers: {
nodesChanged: (state, action: PayloadAction<NodeChange<AnyNode>[]>) => {
// 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<string>) => {
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<XYPosition | null>(null);
export const $templates = atom<Templates>({});
@@ -775,21 +783,6 @@ export const $lastEdgeUpdateMouseEvent = atom<MouseEvent | null>(null);
export const $viewport = atom<Viewport>({ 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<NodesState> = {
name: nodesSlice.name,
initialState: initialState,
migrate: migrateNodesState,
persistDenylist: [],
};
type NodeSelectionAction = {
type: ReturnType<typeof nodesChanged>['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<NodesState, UnknownAction> = {
const reduxUndoOptions: UndoableOptions<NodesState, UnknownAction> = {
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<NodesState, UnknownAction> = {
},
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<NodesState, UnknownAction> = {
},
};
export const nodesSliceConfig: SliceConfig<NodesState> = {
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<string, StatefulFieldValue> = {};

View File

@@ -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<WorkflowMode>) => {
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<WorkflowLibraryState> = {
name: workflowLibrarySlice.name,
initialState: initialWorkflowLibraryState,
migrate: migrateWorkflowLibraryState,
persistDenylist: [],
export const workflowLibrarySliceConfig: SliceConfig<WorkflowLibraryState> = {
slice,
getInitialState,
persistConfig: {
migrate,
},
};
const selectWorkflowLibrarySlice = (state: RootState) => state.workflowLibrary;

View File

@@ -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<boolean>) => {
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<WorkflowSettingsState> = {
name: workflowSettingsSlice.name,
initialState,
migrate: migrateWorkflowSettingsState,
persistDenylist: [],
export const workflowSettingsSliceConfig: SliceConfig<WorkflowSettingsState> = {
slice,
getInitialState,
persistConfig: {
migrate,
},
};
export const selectWorkflowSettingsSlice = (state: RootState) => state.workflowSettings;

View File

@@ -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<ParameterSpandrelImageToImageModel | null>) => {
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<UpscaleState> = {
name: upscaleSlice.name,
initialState: initialUpscaleState,
migrate: migrateUpscaleState,
persistDenylist: [],
export const upscaleSliceConfig: SliceConfig<UpscaleState> = {
slice,
getInitialState,
persistConfig: {
migrate,
},
};
export const selectUpscaleSlice = (state: RootState) => state.upscale;

View File

@@ -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<number | undefined>) => {
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<QueueState> = {
slice,
getInitialState,
};
const selectQueueSlice = (state: RootState) => state.queue;
const createQueueSelector = <T>(selector: Selector<QueueState, T>) => createSelector(selectQueueSlice, selector);

View File

@@ -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<string | null>) => {
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<StylePresetState> = {
name: stylePresetSlice.name,
initialState,
migrate: migrateStylePresetState,
persistDenylist: [],
export const stylePresetSliceConfig: SliceConfig<StylePresetState> = {
slice,
getInitialState,
persistConfig: {
migrate,
},
};
export const selectStylePresetSlice = (state: RootState) => state.stylePreset;

View File

@@ -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<PartialAppConfig>) => {
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<ConfigState> = {
slice,
getInitialState,
};
export const selectConfigSlice = (state: RootState) => state.config;
const createConfigSelector = <T>(selector: Selector<typeof initialConfigState, T>) =>
createSelector(selectConfigSlice, selector);
const createConfigSelector = <T>(selector: Selector<ConfigState, T>) => createSelector(selectConfigSlice, selector);
export const selectWidthConfig = createConfigSelector((config) => config.sd.width);
export const selectHeightConfig = createConfigSelector((config) => config.sd.height);

View File

@@ -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<boolean>) => {
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<SystemState> = {
name: systemSlice.name,
initialState: initialSystemState,
migrate: migrateSystemState,
persistDenylist: [],
export const systemSliceConfig: SliceConfig<SystemState> = {
slice,
getInitialState,
persistConfig: {
migrate,
},
};
export const selectSystemSlice = (state: RootState) => state.system;

View File

@@ -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<UIState['activeTab']>) => {
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<UIState> = {
name: uiSlice.name,
initialState: getInitialUIState(),
migrate: migrateUIState,
persistDenylist: ['shouldShowImageDetails'],
export const uiSliceConfig: SliceConfig<UIState> = {
slice,
getInitialState,
persistConfig: {
migrate,
persistDenylist: ['shouldShowImageDetails'],
},
};

View File

@@ -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<typeof zUIState>;
export const getInitialUIState = (): UIState => deepClone(INITIAL_STATE);