Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion invokeai/frontend/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"@invoke-ai/ui-library": "^0.0.47",
"@nanostores/react": "^1.0.0",
"@observ33r/object-equals": "^1.1.5",
"@reduxjs/toolkit": "2.8.2",
"@reduxjs/toolkit": "2.9.0",
"@roarr/browser-log-writer": "^1.3.0",
"@xyflow/react": "^12.8.2",
"ag-psd": "^28.2.2",
Expand Down
10 changes: 5 additions & 5 deletions invokeai/frontend/web/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { AppStartListening } from 'app/store/store';
import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { selectCanvases } from 'features/controlLayers/store/selectors';
import { getImageUsage } from 'features/deleteImageModal/store/state';
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
import { selectNodesSlice } from 'features/nodes/store/selectors';
Expand All @@ -19,12 +19,12 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS

const state = getState();
const nodes = selectNodesSlice(state);
const canvas = selectCanvasSlice(state);
const canvases = selectCanvases(state);
const upscale = selectUpscaleSlice(state);
const refImages = selectRefImagesSlice(state);

deleted_images.forEach((image_name) => {
const imageUsage = getImageUsage(nodes, canvas, upscale, refImages, image_name);
const imageUsage = getImageUsage(nodes, canvases, upscale, refImages, image_name);

if (imageUsage.isNodesImage && !wasNodeEditorReset) {
dispatch(nodeEditorReset());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/store';
import { bboxSyncedToOptimalDimension, rgRefImageModelChanged } from 'features/controlLayers/store/canvasSlice';
import { buildSelectIsStaging, selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
import {
buildSelectIsStagingBySessionId,
selectSelectedCanvasSessionId,
} from 'features/controlLayers/store/canvasStagingAreaSlice';
import { loraIsEnabledChanged } from 'features/controlLayers/store/lorasSlice';
import { modelChanged, syncedToOptimalDimension, vaeSelected } from 'features/controlLayers/store/paramsSlice';
import { refImageModelChanged, selectReferenceImageEntities } from 'features/controlLayers/store/refImagesSlice';
import {
selectAllEntitiesOfType,
selectBboxModelBase,
selectCanvasSlice,
selectSelectedCanvas,
} from 'features/controlLayers/store/selectors';
import { getEntityIdentifier } from 'features/controlLayers/store/types';
import { modelSelected } from 'features/parameters/store/actions';
Expand Down Expand Up @@ -118,7 +121,7 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
const newRegionalRefImageModel = selectRegionalRefImageModels(state)[0] ?? null;

// All regional guidance entities are updated to use the same new model.
const canvasState = selectCanvasSlice(state);
const canvasState = selectSelectedCanvas(state);
const canvasRegionalGuidanceEntities = selectAllEntitiesOfType(canvasState, 'regional_guidance');
for (const entity of canvasRegionalGuidanceEntities) {
for (const refImage of entity.referenceImages) {
Expand Down Expand Up @@ -159,7 +162,9 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
if (modelBase !== state.params.model?.base) {
// Sync generate tab settings whenever the model base changes
dispatch(syncedToOptimalDimension());
const isStaging = buildSelectIsStaging(selectCanvasSessionId(state))(state);
const sessionId = selectSelectedCanvasSessionId(state);
const selectIsStaging = buildSelectIsStagingBySessionId(sessionId);
const isStaging = selectIsStaging(state);
if (!isStaging) {
// Canvas tab only syncs if not staging
dispatch(bboxSyncedToOptimalDimension());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
vaeSelected,
} from 'features/controlLayers/store/paramsSlice';
import { refImageModelChanged, selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { selectSelectedCanvas } from 'features/controlLayers/store/selectors';
import {
getEntityIdentifier,
isFLUXReduxConfig,
Expand Down Expand Up @@ -221,7 +221,7 @@ const handleVideoModels: ModelHandler = (models, state, dispatch, log) => {

const handleControlAdapterModels: ModelHandler = (models, state, dispatch, log) => {
const caModels = models.filter(isControlLayerModelConfig);
selectCanvasSlice(state).controlLayers.entities.forEach((entity) => {
selectSelectedCanvas(state).controlLayers.entities.forEach((entity) => {
const selectedControlAdapterModel = entity.controlAdapter.model;
// `null` is a valid control adapter model - no need to do anything.
if (!selectedControlAdapterModel) {
Expand Down Expand Up @@ -256,7 +256,7 @@ const handleIPAdapterModels: ModelHandler = (models, state, dispatch, log) => {
dispatch(refImageModelChanged({ id: entity.id, modelConfig: null }));
});

selectCanvasSlice(state).regionalGuidance.entities.forEach((entity) => {
selectSelectedCanvas(state).regionalGuidance.entities.forEach((entity) => {
entity.referenceImages.forEach(({ id: referenceImageId, config }) => {
if (!isRegionalGuidanceIPAdapterConfig(config)) {
return;
Expand Down Expand Up @@ -299,7 +299,7 @@ const handleFLUXReduxModels: ModelHandler = (models, state, dispatch, log) => {
dispatch(refImageModelChanged({ id: entity.id, modelConfig: null }));
});

selectCanvasSlice(state).regionalGuidance.entities.forEach((entity) => {
selectSelectedCanvas(state).regionalGuidance.entities.forEach((entity) => {
entity.referenceImages.forEach(({ id: referenceImageId, config }) => {
if (!isRegionalGuidanceFLUXReduxConfig(config)) {
return;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { AppStartListening } from 'app/store/store';
import { isNil } from 'es-toolkit';
import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/store/canvasSlice';
import { buildSelectIsStaging, selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
import {
buildSelectIsStagingBySessionId,
selectSelectedCanvasSessionId,
} from 'features/controlLayers/store/canvasStagingAreaSlice';
import {
heightChanged,
setCfgRescaleMultiplier,
Expand Down Expand Up @@ -115,7 +118,9 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni
}
const setSizeOptions = { updateAspectRatio: true, clamp: true };

const isStaging = buildSelectIsStaging(selectCanvasSessionId(state))(state);
const sessionId = selectSelectedCanvasSessionId(state);
const selectIsStaging = buildSelectIsStagingBySessionId(sessionId);
const isStaging = selectIsStaging(state);

const activeTab = selectActiveTab(getState());
if (activeTab === 'generate') {
Expand Down
31 changes: 9 additions & 22 deletions invokeai/frontend/web/src/app/store/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ import { merge } from 'es-toolkit';
import { omit, pick } from 'es-toolkit/compat';
import { changeBoardModalSliceConfig } from 'features/changeBoardModal/store/slice';
import { canvasSettingsSliceConfig } from 'features/controlLayers/store/canvasSettingsSlice';
import { canvasSliceConfig } from 'features/controlLayers/store/canvasSlice';
import { canvasSliceConfig, migrateCanvas, undoableCanvasesReducer } 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 { nodesSliceConfig, undoableNodesSliceReducer } 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';
Expand All @@ -44,7 +44,6 @@ import { diff } from 'jsondiffpatch';
import dynamicMiddlewares from 'redux-dynamic-middlewares';
import type { SerializeFunction, UnserializeFunction } from 'redux-remember';
import { REMEMBER_REHYDRATED, rememberEnhancer, rememberReducer } from 'redux-remember';
import undoable, { newHistory } from 'redux-undo';
import { serializeError } from 'serialize-error';
import { api } from 'services/api';
import { authToastMiddleware } from 'services/api/authToastMiddleware';
Expand Down Expand Up @@ -91,22 +90,14 @@ const ALL_REDUCERS = {
[api.reducerPath]: api.reducer,
[canvasSessionSliceConfig.slice.reducerPath]: canvasSessionSliceConfig.slice.reducer,
[canvasSettingsSliceConfig.slice.reducerPath]: canvasSettingsSliceConfig.slice.reducer,
// Undoable!
[canvasSliceConfig.slice.reducerPath]: undoable(
canvasSliceConfig.slice.reducer,
canvasSliceConfig.undoableConfig?.reduxUndoOptions
),
[canvasSliceConfig.slice.reducerPath]: undoableCanvasesReducer,
[changeBoardModalSliceConfig.slice.reducerPath]: changeBoardModalSliceConfig.slice.reducer,
[configSliceConfig.slice.reducerPath]: configSliceConfig.slice.reducer,
[dynamicPromptsSliceConfig.slice.reducerPath]: dynamicPromptsSliceConfig.slice.reducer,
[gallerySliceConfig.slice.reducerPath]: gallerySliceConfig.slice.reducer,
[lorasSliceConfig.slice.reducerPath]: lorasSliceConfig.slice.reducer,
[modelManagerSliceConfig.slice.reducerPath]: modelManagerSliceConfig.slice.reducer,
// Undoable!
[nodesSliceConfig.slice.reducerPath]: undoable(
nodesSliceConfig.slice.reducer,
nodesSliceConfig.undoableConfig?.reduxUndoOptions
),
[nodesSliceConfig.slice.reducerPath]: undoableNodesSliceReducer,
[paramsSliceConfig.slice.reducerPath]: paramsSliceConfig.slice.reducer,
[queueSliceConfig.slice.reducerPath]: queueSliceConfig.slice.reducer,
[refImagesSliceConfig.slice.reducerPath]: refImagesSliceConfig.slice.reducer,
Expand All @@ -128,7 +119,7 @@ const unserialize: UnserializeFunction = (data, key) => {
if (!sliceConfig?.persistConfig) {
throw new Error(`No persist config for slice "${key}"`);
}
const { getInitialState, persistConfig, undoableConfig } = sliceConfig;
const { getInitialState, persistConfig } = sliceConfig;
let state;
try {
const initialState = getInitialState();
Expand Down Expand Up @@ -160,12 +151,7 @@ const unserialize: UnserializeFunction = (data, key) => {
state = getInitialState();
}

// Undoable slices must be wrapped in a history!
if (undoableConfig) {
return newHistory([], state, []);
} else {
return state;
}
return persistConfig.wrapState ? persistConfig.wrapState(state) : state;
};

const serialize: SerializeFunction = (data, key) => {
Expand All @@ -175,7 +161,7 @@ const serialize: SerializeFunction = (data, key) => {
}

const result = omit(
sliceConfig.undoableConfig ? data.present : data,
sliceConfig.persistConfig.unwrapState ? sliceConfig.persistConfig.unwrapState(data) : data,
sliceConfig.persistConfig.persistDenylist ?? []
);

Expand Down Expand Up @@ -233,8 +219,9 @@ export const createStore = (options?: { persist?: boolean; persistDebounce?: num
// Once-off listener to support waiting for rehydration before rendering the app
startAppListening({
actionCreator: createAction(REMEMBER_REHYDRATED),
effect: (action, { unsubscribe }) => {
effect: (action, { dispatch, unsubscribe }) => {
unsubscribe();
dispatch(migrateCanvas());
options?.onRehydrated?.();
},
});
Expand Down
26 changes: 15 additions & 11 deletions invokeai/frontend/web/src/app/store/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import type { Slice } from '@reduxjs/toolkit';
import type { UndoableOptions } from 'redux-undo';
import type { ZodType } from 'zod';

type StateFromSlice<T extends Slice> = T extends Slice<infer U> ? U : never;

export type SliceConfig<T extends Slice> = {
export type SliceConfig<T extends Slice, TInternalState = StateFromSlice<T>, TSerializedState = StateFromSlice<T>> = {
/**
* The redux slice (return of createSlice).
*/
Expand All @@ -16,7 +15,7 @@ export type SliceConfig<T extends Slice> = {
/**
* A function that returns the initial state of the slice.
*/
getInitialState: () => StateFromSlice<T>;
getInitialState: () => TSerializedState;
/**
* The optional persist configuration for this slice. If omitted, the slice will not be persisted.
*/
Expand All @@ -28,19 +27,24 @@ export type SliceConfig<T extends Slice> = {
* @param state The rehydrated state.
* @returns A correctly-shaped state.
*/
migrate: (state: unknown) => StateFromSlice<T>;
migrate: (state: unknown) => TSerializedState;
/**
* Keys to omit from the persisted state.
*/
persistDenylist?: (keyof StateFromSlice<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.
* Wraps state into state with history
*
* @param state The state without history
* @returns The state with history
*/
wrapState?: (state: unknown) => TInternalState;
/**
* Unwraps state with history
*
* @param state The state with history
* @returns The state without history
*/
reduxUndoOptions: UndoableOptions<StateFromSlice<T>>;
unwrapState?: (state: TInternalState) => TSerializedState;
};
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { allEntitiesDeleted, inpaintMaskAdded } from 'features/controlLayers/store/canvasSlice';
import { $canvasManager } from 'features/controlLayers/store/ephemeral';
import { paramsReset } from 'features/controlLayers/store/paramsSlice';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo, useCallback } from 'react';
Expand All @@ -12,12 +12,13 @@ export const SessionMenuItems = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const tab = useAppSelector(selectActiveTab);
const canvasManager = useCanvasManagerSafe();

const resetCanvasLayers = useCallback(() => {
dispatch(allEntitiesDeleted());
dispatch(inpaintMaskAdded({ isSelected: true, isBookmarked: true }));
$canvasManager.get()?.stage.fitBboxToStage();
}, [dispatch]);
canvasManager?.stage.fitBboxToStage();
}, [dispatch, canvasManager]);
const resetGenerationSettings = useCallback(() => {
dispatch(paramsReset());
}, [dispatch]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next';

export const CanvasAlertsPreserveMask = memo(() => {
const { t } = useTranslation();
const preserveMask = useAppSelector(selectPreserveMask);
const preserveMask = useAppSelector((state) => selectPreserveMask(state));

if (!preserveMask) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next';

export const CanvasAlertsSaveAllImagesToGallery = memo(() => {
const { t } = useTranslation();
const saveAllImagesToGallery = useAppSelector(selectSaveAllImagesToGallery);
const saveAllImagesToGallery = useAppSelector((state) => selectSaveAllImagesToGallery(state));

if (!saveAllImagesToGallery) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import { useEntityTitle } from 'features/controlLayers/hooks/useEntityTitle';
import { useEntityTypeIsHidden } from 'features/controlLayers/hooks/useEntityTypeIsHidden';
import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEntity/types';
import {
selectCanvasSlice,
selectEntityOrThrow,
selectSelectedCanvas,
selectSelectedEntityIdentifier,
} from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
Expand All @@ -32,13 +32,13 @@ type AlertData = {

const buildSelectIsEnabled = (entityIdentifier: CanvasEntityIdentifier) =>
createSelector(
selectCanvasSlice,
selectSelectedCanvas,
(canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'CanvasAlertsSelectedEntityStatusContent').isEnabled
);

const buildSelectIsLocked = (entityIdentifier: CanvasEntityIdentifier) =>
createSelector(
selectCanvasSlice,
selectSelectedCanvas,
(canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'CanvasAlertsSelectedEntityStatusContent').isLocked
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next';
export const CanvasAutoProcessSwitch = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const autoProcess = useAppSelector(selectAutoProcess);
const autoProcess = useAppSelector((state) => selectAutoProcess(state));

const onChange = useCallback(() => {
dispatch(settingsAutoProcessToggled());
Expand Down
Loading