mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-02-13 06:14:58 -05:00
refactor(ui): cleaner slice definitions
This commit is contained in:
@@ -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;
|
||||
|
||||
34
invokeai/frontend/web/src/app/store/types.ts
Normal file
34
invokeai/frontend/web/src/app/store/types.ts
Normal 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>;
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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> = {};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user