Skip to content

Commit fba4085

Browse files
authored
ui: boards 2: electric boogaloo (#3869)
## What type of PR is this? (check all applicable) - [x] Refactor - [ ] Feature - [ ] Bug Fix - [ ] Optimization - [ ] Documentation Update - [ ] Community Node Submission ## Have you discussed this change with the InvokeAI team? - [x] Yes - [ ] No, because: ## Description Revised boards logic and UI ## Related Tickets & Documents <!-- For pull requests that relate or close an issue, please include them below. For example having the text: "closes #1234" would connect the current pull request to issue 1234. And when we merge the pull request, Github will automatically close the issue. --> - Related Issue # discord convos - Closes # ## QA Instructions, Screenshots, Recordings <!-- Please provide steps on how to test changes, any hardware or software specifications as well as any other pertinent information. --> ## Added/updated tests? - [ ] Yes - [x] No : n/a ## [optional] Are there any post deployment tasks we need to perform?
2 parents e06f222 + 9ce4bd1 commit fba4085

30 files changed

+952
-746
lines changed

invokeai/frontend/web/src/app/components/ImageDnd/typesafeDnd.tsx

+1-3
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,7 @@ export const isValidDrop = (
175175
const destinationBoard = overData.context.boardId;
176176

177177
const isSameBoard = currentBoard === destinationBoard;
178-
const isDestinationValid = !currentBoard
179-
? destinationBoard !== 'no_board'
180-
: true;
178+
const isDestinationValid = !currentBoard ? destinationBoard : true;
181179

182180
return !isSameBoard && isDestinationValid;
183181
}

invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardIdSelected.ts

+18-9
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,42 @@
11
import { log } from 'app/logging/useLogger';
22
import {
3+
ASSETS_CATEGORIES,
4+
IMAGE_CATEGORIES,
35
boardIdSelected,
6+
galleryViewChanged,
47
imageSelected,
58
} from 'features/gallery/store/gallerySlice';
6-
import {
7-
getBoardIdQueryParamForBoard,
8-
getCategoriesQueryParamForBoard,
9-
} from 'features/gallery/store/util';
109
import { imagesApi } from 'services/api/endpoints/images';
1110
import { startAppListening } from '..';
11+
import { isAnyOf } from '@reduxjs/toolkit';
1212

1313
const moduleLog = log.child({ namespace: 'boards' });
1414

1515
export const addBoardIdSelectedListener = () => {
1616
startAppListening({
17-
actionCreator: boardIdSelected,
17+
matcher: isAnyOf(boardIdSelected, galleryViewChanged),
1818
effect: async (
1919
action,
2020
{ getState, dispatch, condition, cancelActiveListeners }
2121
) => {
2222
// Cancel any in-progress instances of this listener, we don't want to select an image from a previous board
2323
cancelActiveListeners();
2424

25-
const _board_id = action.payload;
25+
const state = getState();
26+
27+
const board_id = boardIdSelected.match(action)
28+
? action.payload
29+
: state.gallery.selectedBoardId;
30+
31+
const galleryView = galleryViewChanged.match(action)
32+
? action.payload
33+
: state.gallery.galleryView;
34+
2635
// when a board is selected, we need to wait until the board has loaded *some* images, then select the first one
36+
const categories =
37+
galleryView === 'images' ? IMAGE_CATEGORIES : ASSETS_CATEGORIES;
2738

28-
const categories = getCategoriesQueryParamForBoard(_board_id);
29-
const board_id = getBoardIdQueryParamForBoard(_board_id);
30-
const queryArgs = { board_id, categories };
39+
const queryArgs = { board_id: board_id ?? 'none', categories };
3140

3241
// wait until the board has some images - maybe it already has some from a previous fetch
3342
// must use getState() to ensure we do not have stale state

invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts

+4-10
Original file line numberDiff line numberDiff line change
@@ -156,14 +156,13 @@ export const addImageDroppedListener = () => {
156156
if (
157157
overData.actionType === 'MOVE_BOARD' &&
158158
activeData.payloadType === 'IMAGE_DTO' &&
159-
activeData.payload.imageDTO &&
160-
overData.context.boardId
159+
activeData.payload.imageDTO
161160
) {
162161
const { imageDTO } = activeData.payload;
163162
const { boardId } = overData.context;
164163

165-
// if the board is "No Board", this is a remove action
166-
if (boardId === 'no_board') {
164+
// image was droppe on the "NoBoardBoard"
165+
if (!boardId) {
167166
dispatch(
168167
imagesApi.endpoints.removeImageFromBoard.initiate({
169168
imageDTO,
@@ -172,12 +171,7 @@ export const addImageDroppedListener = () => {
172171
return;
173172
}
174173

175-
// Handle adding image to batch
176-
if (boardId === 'batch') {
177-
// TODO
178-
}
179-
180-
// Otherwise, add the image to the board
174+
// image was dropped on a user board
181175
dispatch(
182176
imagesApi.endpoints.addImageToBoard.initiate({
183177
imageDTO,

invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUpdated.ts

+23-23
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,30 @@ import { startAppListening } from '..';
55
const moduleLog = log.child({ namespace: 'image' });
66

77
export const addImageUpdatedFulfilledListener = () => {
8-
startAppListening({
9-
matcher: imagesApi.endpoints.updateImage.matchFulfilled,
10-
effect: (action, { dispatch, getState }) => {
11-
moduleLog.debug(
12-
{
13-
data: {
14-
oldImage: action.meta.arg.originalArgs,
15-
updatedImage: action.payload,
16-
},
17-
},
18-
'Image updated'
19-
);
20-
},
21-
});
8+
// startAppListening({
9+
// matcher: imagesApi.endpoints.updateImage.matchFulfilled,
10+
// effect: (action, { dispatch, getState }) => {
11+
// moduleLog.debug(
12+
// {
13+
// data: {
14+
// oldImage: action.meta.arg.originalArgs,
15+
// updatedImage: action.payload,
16+
// },
17+
// },
18+
// 'Image updated'
19+
// );
20+
// },
21+
// });
2222
};
2323

2424
export const addImageUpdatedRejectedListener = () => {
25-
startAppListening({
26-
matcher: imagesApi.endpoints.updateImage.matchRejected,
27-
effect: (action, { dispatch }) => {
28-
moduleLog.debug(
29-
{ data: action.meta.arg.originalArgs },
30-
'Image update failed'
31-
);
32-
},
33-
});
25+
// startAppListening({
26+
// matcher: imagesApi.endpoints.updateImage.matchRejected,
27+
// effect: (action, { dispatch }) => {
28+
// moduleLog.debug(
29+
// { data: action.meta.arg.originalArgs },
30+
// 'Image update failed'
31+
// );
32+
// },
33+
// });
3434
};

invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts

+26-29
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { addImageToStagingArea } from 'features/canvas/store/canvasSlice';
33
import {
44
IMAGE_CATEGORIES,
55
boardIdSelected,
6+
galleryViewChanged,
67
imageSelected,
78
} from 'features/gallery/store/gallerySlice';
89
import { progressImageSet } from 'features/system/store/systemSlice';
@@ -55,34 +56,6 @@ export const addInvocationCompleteEventListener = () => {
5556
}
5657

5758
if (!imageDTO.is_intermediate) {
58-
// update the cache for 'All Images'
59-
dispatch(
60-
imagesApi.util.updateQueryData(
61-
'listImages',
62-
{
63-
categories: IMAGE_CATEGORIES,
64-
},
65-
(draft) => {
66-
imagesAdapter.addOne(draft, imageDTO);
67-
draft.total = draft.total + 1;
68-
}
69-
)
70-
);
71-
72-
// update the cache for 'No Board'
73-
dispatch(
74-
imagesApi.util.updateQueryData(
75-
'listImages',
76-
{
77-
board_id: 'none',
78-
},
79-
(draft) => {
80-
imagesAdapter.addOne(draft, imageDTO);
81-
draft.total = draft.total + 1;
82-
}
83-
)
84-
);
85-
8659
const { autoAddBoardId } = gallery;
8760

8861
// add image to the board if auto-add is enabled
@@ -93,17 +66,41 @@ export const addInvocationCompleteEventListener = () => {
9366
imageDTO,
9467
})
9568
);
69+
} else {
70+
// add to no board board
71+
// update the cache for 'No Board'
72+
dispatch(
73+
imagesApi.util.updateQueryData(
74+
'listImages',
75+
{
76+
board_id: 'none',
77+
categories: IMAGE_CATEGORIES,
78+
},
79+
(draft) => {
80+
imagesAdapter.addOne(draft, imageDTO);
81+
draft.total = draft.total + 1;
82+
}
83+
)
84+
);
9685
}
9786

87+
dispatch(
88+
imagesApi.util.invalidateTags([
89+
{ type: 'BoardImagesTotal', id: autoAddBoardId ?? 'none' },
90+
{ type: 'BoardAssetsTotal', id: autoAddBoardId ?? 'none' },
91+
])
92+
);
93+
9894
const { selectedBoardId, shouldAutoSwitch } = gallery;
9995

10096
// If auto-switch is enabled, select the new image
10197
if (shouldAutoSwitch) {
10298
// if auto-add is enabled, switch the board as the image comes in
10399
if (autoAddBoardId && autoAddBoardId !== selectedBoardId) {
104100
dispatch(boardIdSelected(autoAddBoardId));
101+
dispatch(galleryViewChanged('images'));
105102
} else if (!autoAddBoardId) {
106-
dispatch(boardIdSelected('images'));
103+
dispatch(galleryViewChanged('images'));
107104
}
108105
dispatch(imageSelected(imageDTO.image_name));
109106
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Badge, Flex } from '@chakra-ui/react';
2+
3+
const AutoAddIcon = () => {
4+
return (
5+
<Flex
6+
sx={{
7+
position: 'absolute',
8+
insetInlineStart: 0,
9+
top: 0,
10+
p: 1,
11+
}}
12+
>
13+
<Badge
14+
variant="solid"
15+
sx={{ fontSize: 10, bg: 'accent.400', _dark: { bg: 'accent.500' } }}
16+
>
17+
auto
18+
</Badge>
19+
</Flex>
20+
);
21+
};
22+
23+
export default AutoAddIcon;

invokeai/frontend/web/src/features/gallery/components/Boards/BoardAutoAddSelect.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ const BoardAutoAddSelect = () => {
5252
return;
5353
}
5454

55-
dispatch(autoAddBoardIdChanged(v === 'none' ? null : v));
55+
dispatch(autoAddBoardIdChanged(v === 'none' ? undefined : v));
5656
},
5757
[dispatch]
5858
);

invokeai/frontend/web/src/features/gallery/components/Boards/BoardContextMenu.tsx

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Box, MenuItem, MenuList } from '@chakra-ui/react';
1+
import { MenuItem, MenuList } from '@chakra-ui/react';
22
import { useAppDispatch } from 'app/store/storeHooks';
33
import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu';
44
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
@@ -7,21 +7,23 @@ import { FaFolder } from 'react-icons/fa';
77
import { BoardDTO } from 'services/api/types';
88
import { menuListMotionProps } from 'theme/components/menu';
99
import GalleryBoardContextMenuItems from './GalleryBoardContextMenuItems';
10-
import SystemBoardContextMenuItems from './SystemBoardContextMenuItems';
10+
import NoBoardContextMenuItems from './NoBoardContextMenuItems';
1111

1212
type Props = {
1313
board?: BoardDTO;
14-
board_id: string;
14+
board_id?: string;
1515
children: ContextMenuProps<HTMLDivElement>['children'];
1616
setBoardToDelete?: (board?: BoardDTO) => void;
1717
};
1818

1919
const BoardContextMenu = memo(
2020
({ board, board_id, setBoardToDelete, children }: Props) => {
2121
const dispatch = useAppDispatch();
22+
2223
const handleSelectBoard = useCallback(() => {
2324
dispatch(boardIdSelected(board?.board_id ?? board_id));
2425
}, [board?.board_id, board_id, dispatch]);
26+
2527
return (
2628
<ContextMenu<HTMLDivElement>
2729
menuProps={{ size: 'sm', isLazy: true }}
@@ -37,7 +39,7 @@ const BoardContextMenu = memo(
3739
<MenuItem icon={<FaFolder />} onClickCapture={handleSelectBoard}>
3840
Select Board
3941
</MenuItem>
40-
{!board && <SystemBoardContextMenuItems board_id={board_id} />}
42+
{!board && <NoBoardContextMenuItems />}
4143
{board && (
4244
<GalleryBoardContextMenuItems
4345
board={board}

invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsList.tsx

+5-52
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import AddBoardButton from './AddBoardButton';
1616
import BoardsSearch from './BoardsSearch';
1717
import GalleryBoard from './GalleryBoard';
1818
import SystemBoardButton from './SystemBoardButton';
19+
import NoBoardBoard from './NoBoardBoard';
1920

2021
const selector = createSelector(
2122
[stateSelector],
@@ -42,10 +43,6 @@ const BoardsList = (props: Props) => {
4243
)
4344
: boards;
4445
const [boardToDelete, setBoardToDelete] = useState<BoardDTO>();
45-
const [isSearching, setIsSearching] = useState(false);
46-
const handleClickSearchIcon = useCallback(() => {
47-
setIsSearching((v) => !v);
48-
}, []);
4946

5047
return (
5148
<>
@@ -61,54 +58,7 @@ const BoardsList = (props: Props) => {
6158
}}
6259
>
6360
<Flex sx={{ gap: 2, alignItems: 'center' }}>
64-
<AnimatePresence mode="popLayout">
65-
{isSearching ? (
66-
<motion.div
67-
key="boards-search"
68-
initial={{
69-
opacity: 0,
70-
}}
71-
exit={{
72-
opacity: 0,
73-
}}
74-
animate={{
75-
opacity: 1,
76-
transition: { duration: 0.1 },
77-
}}
78-
style={{ width: '100%' }}
79-
>
80-
<BoardsSearch setIsSearching={setIsSearching} />
81-
</motion.div>
82-
) : (
83-
<motion.div
84-
key="system-boards-select"
85-
initial={{
86-
opacity: 0,
87-
}}
88-
exit={{
89-
opacity: 0,
90-
}}
91-
animate={{
92-
opacity: 1,
93-
transition: { duration: 0.1 },
94-
}}
95-
style={{ width: '100%' }}
96-
>
97-
<ButtonGroup sx={{ w: 'full', ps: 1.5 }} isAttached>
98-
<SystemBoardButton board_id="images" />
99-
<SystemBoardButton board_id="assets" />
100-
<SystemBoardButton board_id="no_board" />
101-
</ButtonGroup>
102-
</motion.div>
103-
)}
104-
</AnimatePresence>
105-
<IAIIconButton
106-
aria-label="Search Boards"
107-
size="sm"
108-
isChecked={isSearching}
109-
onClick={handleClickSearchIcon}
110-
icon={<FaSearch />}
111-
/>
61+
<BoardsSearch />
11262
<AddBoardButton />
11363
</Flex>
11464
<OverlayScrollbarsComponent
@@ -130,6 +80,9 @@ const BoardsList = (props: Props) => {
13080
maxH: 346,
13181
}}
13282
>
83+
<GridItem sx={{ p: 1.5 }}>
84+
<NoBoardBoard isSelected={selectedBoardId === undefined} />
85+
</GridItem>
13386
{filteredBoards &&
13487
filteredBoards.map((board) => (
13588
<GridItem key={board.board_id} sx={{ p: 1.5 }}>

0 commit comments

Comments
 (0)