Skip to content

Commit 7826180

Browse files
feat(ui): port UI slice to zod
1 parent 90ff9a4 commit 7826180

File tree

2 files changed

+66
-65
lines changed

2 files changed

+66
-65
lines changed

invokeai/frontend/web/src/features/ui/store/uiSlice.ts

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,55 +3,67 @@ import { createSelector, createSlice } from '@reduxjs/toolkit';
33
import type { PersistConfig, RootState } from 'app/store/store';
44
import { canvasReset } from 'features/controlLayers/store/actions';
55
import { canvasSessionReset, generateSessionReset } from 'features/controlLayers/store/canvasStagingAreaSlice';
6-
import type { Dimensions } from 'features/controlLayers/store/types';
76
import { workflowLoaded } from 'features/nodes/store/nodesSlice';
87
import { atom } from 'nanostores';
98

10-
import type { CanvasRightPanelTabName, TabName, UIState } from './uiTypes';
11-
12-
const initialUIState: UIState = {
13-
_version: 3,
14-
activeTab: 'canvas',
15-
activeTabCanvasRightPanel: 'gallery',
16-
shouldShowImageDetails: false,
17-
shouldShowProgressInViewer: true,
18-
accordions: {},
19-
expanders: {},
20-
textAreaSizes: {},
21-
shouldShowNotificationV2: true,
22-
};
9+
import type { TabName, UIState } from './uiTypes';
10+
import { getInitialUIState } from './uiTypes';
2311

2412
export const uiSlice = createSlice({
2513
name: 'ui',
26-
initialState: initialUIState,
14+
initialState: getInitialUIState(),
2715
reducers: {
28-
setActiveTab: (state, action: PayloadAction<TabName>) => {
16+
setActiveTab: (state, action: PayloadAction<UIState['activeTab']>) => {
2917
state.activeTab = action.payload;
3018
},
31-
activeTabCanvasRightPanelChanged: (state, action: PayloadAction<CanvasRightPanelTabName>) => {
19+
activeTabCanvasRightPanelChanged: (state, action: PayloadAction<UIState['activeTabCanvasRightPanel']>) => {
3220
state.activeTabCanvasRightPanel = action.payload;
3321
},
34-
setShouldShowImageDetails: (state, action: PayloadAction<boolean>) => {
22+
setShouldShowImageDetails: (state, action: PayloadAction<UIState['shouldShowImageDetails']>) => {
3523
state.shouldShowImageDetails = action.payload;
3624
},
37-
setShouldShowProgressInViewer: (state, action: PayloadAction<boolean>) => {
25+
setShouldShowProgressInViewer: (state, action: PayloadAction<UIState['shouldShowProgressInViewer']>) => {
3826
state.shouldShowProgressInViewer = action.payload;
3927
},
40-
accordionStateChanged: (state, action: PayloadAction<{ id: string; isOpen: boolean }>) => {
28+
accordionStateChanged: (
29+
state,
30+
action: PayloadAction<{
31+
id: keyof UIState['accordions'];
32+
isOpen: UIState['accordions'][keyof UIState['accordions']];
33+
}>
34+
) => {
4135
const { id, isOpen } = action.payload;
4236
state.accordions[id] = isOpen;
4337
},
44-
expanderStateChanged: (state, action: PayloadAction<{ id: string; isOpen: boolean }>) => {
38+
expanderStateChanged: (
39+
state,
40+
action: PayloadAction<{
41+
id: keyof UIState['expanders'];
42+
isOpen: UIState['expanders'][keyof UIState['expanders']];
43+
}>
44+
) => {
4545
const { id, isOpen } = action.payload;
4646
state.expanders[id] = isOpen;
4747
},
48-
textAreaSizesStateChanged: (state, action: PayloadAction<{ id: string; size: Partial<Dimensions> }>) => {
48+
textAreaSizesStateChanged: (
49+
state,
50+
action: PayloadAction<{
51+
id: keyof UIState['textAreaSizes'];
52+
size: UIState['textAreaSizes'][keyof UIState['textAreaSizes']];
53+
}>
54+
) => {
4955
const { id, size } = action.payload;
5056
state.textAreaSizes[id] = size;
5157
},
52-
shouldShowNotificationChanged: (state, action: PayloadAction<boolean>) => {
58+
shouldShowNotificationChanged: (state, action: PayloadAction<UIState['shouldShowNotificationV2']>) => {
5359
state.shouldShowNotificationV2 = action.payload;
5460
},
61+
showGenerateTabSplashScreenChanged: (state, action: PayloadAction<UIState['showGenerateTabSplashScreen']>) => {
62+
state.showGenerateTabSplashScreen = action.payload;
63+
},
64+
showCanvasTabSplashScreenChanged: (state, action: PayloadAction<UIState['showCanvasTabSplashScreen']>) => {
65+
state.showCanvasTabSplashScreen = action.payload;
66+
},
5567
},
5668
extraReducers(builder) {
5769
builder.addCase(workflowLoaded, (state) => {
@@ -81,6 +93,8 @@ export const {
8193
expanderStateChanged,
8294
shouldShowNotificationChanged,
8395
textAreaSizesStateChanged,
96+
showGenerateTabSplashScreenChanged,
97+
showCanvasTabSplashScreenChanged,
8498
} = uiSlice.actions;
8599

86100
export const selectUiSlice = (state: RootState) => state.ui;
@@ -103,7 +117,7 @@ const migrateUIState = (state: any): any => {
103117

104118
export const uiPersistConfig: PersistConfig<UIState> = {
105119
name: uiSlice.name,
106-
initialState: initialUIState,
120+
initialState: getInitialUIState(),
107121
migrate: migrateUIState,
108122
persistDenylist: ['shouldShowImageDetails'],
109123
};
Lines changed: 28 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,30 @@
1-
import type { Dimensions } from 'features/controlLayers/store/types';
1+
import { deepClone } from 'common/util/deepClone';
2+
import { z } from 'zod';
23

3-
export type TabName = 'generate' | 'canvas' | 'upscaling' | 'workflows' | 'models' | 'queue';
4-
export type CanvasRightPanelTabName = 'layers' | 'gallery';
4+
const zTabName = z.enum(['generate', 'canvas', 'upscaling', 'workflows', 'models', 'queue']);
5+
export type TabName = z.infer<typeof zTabName>;
6+
const zCanvasRightPanelTabName = z.enum(['layers', 'gallery']);
7+
export type CanvasRightPanelTabName = z.infer<typeof zCanvasRightPanelTabName>;
58

6-
export interface UIState {
7-
/**
8-
* Slice schema version.
9-
*/
10-
_version: 3;
11-
/**
12-
* The currently active tab.
13-
*/
14-
activeTab: TabName;
15-
/**
16-
* The currently active right panel canvas tab
17-
*/
18-
activeTabCanvasRightPanel: CanvasRightPanelTabName;
19-
/**
20-
* Whether or not to show image details, e.g. metadata, workflow, etc.
21-
*/
22-
shouldShowImageDetails: boolean;
23-
/**
24-
* Whether or not to show progress in the viewer.
25-
*/
26-
shouldShowProgressInViewer: boolean;
27-
/**
28-
* The state of accordions. The key is the id of the accordion, and the value is a boolean representing the open state.
29-
*/
30-
accordions: Record<string, boolean>;
31-
/**
32-
* The state of expanders. The key is the id of the expander, and the value is a boolean representing the open state.
33-
*/
34-
expanders: Record<string, boolean>;
35-
/**
36-
* The size of textareas. The key is the id of the text area, and the value is an object representing its width and/or height.
37-
*/
38-
textAreaSizes: Record<string, Partial<Dimensions>>;
39-
/**
40-
* Whether or not to show the user the open notification. Bump version to reset users who may have closed previous version.
41-
*/
42-
shouldShowNotificationV2: boolean;
43-
}
9+
const zPartialDimensions = z.object({
10+
width: z.number().optional(),
11+
height: z.number().optional(),
12+
});
13+
export type PartialDimensions = z.infer<typeof zPartialDimensions>;
14+
15+
export const zUIState = z.object({
16+
_version: z.literal(3).default(3),
17+
activeTab: zTabName.default('canvas'),
18+
activeTabCanvasRightPanel: zCanvasRightPanelTabName.default('gallery'),
19+
shouldShowImageDetails: z.boolean().default(false),
20+
shouldShowProgressInViewer: z.boolean().default(true),
21+
accordions: z.record(z.string(), z.boolean()).default(() => ({})),
22+
expanders: z.record(z.string(), z.boolean()).default(() => ({})),
23+
textAreaSizes: z.record(z.string(), zPartialDimensions).default({}),
24+
shouldShowNotificationV2: z.boolean().default(true),
25+
showGenerateTabSplashScreen: z.boolean().default(true),
26+
showCanvasTabSplashScreen: z.boolean().default(true),
27+
});
28+
const INITIAL_STATE = zUIState.parse({});
29+
export type UIState = z.infer<typeof zUIState>;
30+
export const getInitialUIState = (): UIState => deepClone(INITIAL_STATE);

0 commit comments

Comments
 (0)