feat(ui): wip boards via rtk-query

This commit is contained in:
psychedelicious 2023-06-21 00:07:24 +10:00
parent 661a94b3de
commit cfda128e06
20 changed files with 356 additions and 129 deletions

View File

@ -3,6 +3,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
import { PropsWithChildren, createContext, useCallback, useState } from 'react';
import { ImageDTO } from 'services/api';
import { imageAddedToBoard } from '../../services/thunks/board';
import { useAddImageToBoardMutation } from 'services/apiSlice';
export type ImageUsage = {
isInitialImage: boolean;
@ -43,6 +44,8 @@ export const AddImageToBoardContextProvider = (props: Props) => {
const dispatch = useAppDispatch();
const { isOpen, onOpen, onClose } = useDisclosure();
const [addImageToBoard, result] = useAddImageToBoardMutation();
// Clean up after deleting or dismissing the modal
const closeAndClearImageToDelete = useCallback(() => {
setImageToMove(undefined);
@ -63,18 +66,14 @@ export const AddImageToBoardContextProvider = (props: Props) => {
const handleAddToBoard = useCallback(
(boardId: string) => {
if (imageToMove) {
dispatch(
imageAddedToBoard({
requestBody: {
board_id: boardId,
image_name: imageToMove.image_name,
},
})
);
addImageToBoard({
board_id: boardId,
image_name: imageToMove.image_name,
});
closeAndClearImageToDelete();
}
},
[closeAndClearImageToDelete, dispatch, imageToMove]
[addImageToBoard, closeAndClearImageToDelete, imageToMove]
);
return (

View File

@ -73,6 +73,10 @@ import { addImageCategoriesChangedListener } from './listeners/imageCategoriesCh
import { addControlNetImageProcessedListener } from './listeners/controlNetImageProcessed';
import { addControlNetAutoProcessListener } from './listeners/controlNetAutoProcess';
import { addUpdateImageUrlsOnConnectListener } from './listeners/updateImageUrlsOnConnect';
import {
addImageAddedToBoardFulfilledListener,
addImageAddedToBoardRejectedListener,
} from './listeners/imageAddedToBoard';
export const listenerMiddleware = createListenerMiddleware();
@ -183,3 +187,7 @@ addControlNetAutoProcessListener();
// Update image URLs on connect
addUpdateImageUrlsOnConnectListener();
// Boards
addImageAddedToBoardFulfilledListener();
addImageAddedToBoardRejectedListener();

View File

@ -0,0 +1,40 @@
import { log } from 'app/logging/useLogger';
import { startAppListening } from '..';
import { imageMetadataReceived } from 'services/thunks/image';
import { api } from 'services/apiSlice';
const moduleLog = log.child({ namespace: 'boards' });
export const addImageAddedToBoardFulfilledListener = () => {
startAppListening({
matcher: api.endpoints.addImageToBoard.matchFulfilled,
effect: (action, { getState, dispatch }) => {
const { board_id, image_name } = action.meta.arg.originalArgs;
moduleLog.debug(
{ data: { board_id, image_name } },
'Image added to board'
);
dispatch(
imageMetadataReceived({
imageName: image_name,
})
);
},
});
};
export const addImageAddedToBoardRejectedListener = () => {
startAppListening({
matcher: api.endpoints.addImageToBoard.matchRejected,
effect: (action, { getState, dispatch }) => {
const { board_id, image_name } = action.meta.arg.originalArgs;
moduleLog.debug(
{ data: { board_id, image_name } },
'Problem adding image to board'
);
},
});
};

View File

@ -13,6 +13,7 @@ import { resetCanvas } from 'features/canvas/store/canvasSlice';
import { controlNetReset } from 'features/controlNet/store/controlNetSlice';
import { clearInitialImage } from 'features/parameters/store/generationSlice';
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
import { api } from 'services/apiSlice';
const moduleLog = log.child({ namespace: 'addRequestedImageDeletionListener' });
@ -22,7 +23,7 @@ const moduleLog = log.child({ namespace: 'addRequestedImageDeletionListener' });
export const addRequestedImageDeletionListener = () => {
startAppListening({
actionCreator: requestedImageDeletion,
effect: (action, { dispatch, getState }) => {
effect: async (action, { dispatch, getState, condition }) => {
const { image, imageUsage } = action.payload;
const { image_name } = image;
@ -30,7 +31,7 @@ export const addRequestedImageDeletionListener = () => {
const state = getState();
const selectedImage = state.gallery.selectedImage;
if (selectedImage && selectedImage.image_name === image_name) {
if (selectedImage && selectedImage === image_name) {
const ids = selectImagesIds(state);
const entities = selectImagesEntities(state);
@ -51,7 +52,7 @@ export const addRequestedImageDeletionListener = () => {
const newSelectedImage = entities[newSelectedImageId];
if (newSelectedImageId) {
dispatch(imageSelected(newSelectedImage));
dispatch(imageSelected(newSelectedImageId));
} else {
dispatch(imageSelected());
}
@ -79,7 +80,19 @@ export const addRequestedImageDeletionListener = () => {
dispatch(imageRemoved(image_name));
// Delete from server
dispatch(imageDeleted({ imageName: image_name }));
const { requestId } = dispatch(imageDeleted({ imageName: image_name }));
// Wait for successful deletion, then trigger boards to re-fetch
const wasImageDeleted = await condition(
(action) => action.meta.requestId === requestId,
30000
);
if (wasImageDeleted) {
dispatch(
api.util.invalidateTags([{ type: 'Board', id: image.board_id }])
);
}
},
});
};

View File

@ -33,6 +33,7 @@ import { actionsDenylist } from './middleware/devtools/actionsDenylist';
import { serialize } from './enhancers/reduxRemember/serialize';
import { unserialize } from './enhancers/reduxRemember/unserialize';
import { LOCALSTORAGE_PREFIX } from './constants';
import { api } from 'services/apiSlice';
const allReducers = {
canvas: canvasReducer,
@ -49,6 +50,7 @@ const allReducers = {
images: imagesReducer,
controlNet: controlNetReducer,
boards: boardsReducer,
[api.reducerPath]: api.reducer,
// session: sessionReducer,
};
@ -87,6 +89,7 @@ export const store = configureStore({
immutableCheck: false,
serializableCheck: false,
})
.concat(api.middleware)
.concat(dynamicMiddlewares)
.prepend(listenerMiddleware.middleware),
devTools: {

View File

@ -1,19 +1,20 @@
import { Flex, Icon, Text } from '@chakra-ui/react';
import { Flex, Icon, Spinner, Text } from '@chakra-ui/react';
import { useCallback } from 'react';
import { FaPlus } from 'react-icons/fa';
import { useAppDispatch } from '../../../../app/store/storeHooks';
import { boardCreated } from '../../../../services/thunks/board';
import { useCreateBoardMutation } from 'services/apiSlice';
const DEFAULT_BOARD_NAME = 'My Board';
const AddBoardButton = () => {
const dispatch = useAppDispatch();
const [createBoard, { isLoading }] = useCreateBoardMutation();
const handleCreateBoard = useCallback(() => {
dispatch(boardCreated({ requestBody: 'My Board' }));
}, [dispatch]);
createBoard(DEFAULT_BOARD_NAME);
}, [createBoard]);
return (
<Flex
onClick={handleCreateBoard}
onClick={isLoading ? undefined : handleCreateBoard}
sx={{
flexDir: 'column',
justifyContent: 'space-between',
@ -36,7 +37,11 @@ const AddBoardButton = () => {
aspectRatio: '1/1',
}}
>
<Icon boxSize={8} color="base.700" as={FaPlus} />
{isLoading ? (
<Spinner />
) : (
<Icon boxSize={8} color="base.700" as={FaPlus} />
)}
</Flex>
<Text sx={{ color: 'base.200', fontSize: 'xs' }}>New Board</Text>
</Flex>

View File

@ -25,6 +25,7 @@ import { searchBoardsSelector } from '../../store/boardSelectors';
import { useSelector } from 'react-redux';
import IAICollapse from '../../../../common/components/IAICollapse';
import { CloseIcon } from '@chakra-ui/icons';
import { useListBoardsQuery } from 'services/apiSlice';
const selector = createSelector(
[selectBoardsAll, boardsSelector],
@ -40,9 +41,17 @@ const selector = createSelector(
const BoardsList = () => {
const dispatch = useAppDispatch();
const { selectedBoard, searchText } = useAppSelector(selector);
const filteredBoards = useSelector(searchBoardsSelector);
// const filteredBoards = useSelector(searchBoardsSelector);
const { isOpen, onToggle } = useDisclosure();
const { data } = useListBoardsQuery({ offset: 0, limit: 8 });
const filteredBoards = searchText
? data?.items.filter((board) =>
board.board_name.toLowerCase().includes(searchText.toLowerCase())
)
: data.items;
const [searchMode, setSearchMode] = useState(false);
const handleBoardSearch = (searchTerm: string) => {
@ -100,13 +109,14 @@ const BoardsList = () => {
<AllImagesBoard isSelected={!selectedBoard} />
</>
)}
{filteredBoards.map((board) => (
<HoverableBoard
key={board.board_id}
board={board}
isSelected={selectedBoard?.board_id === board.board_id}
/>
))}
{filteredBoards &&
filteredBoards.map((board) => (
<HoverableBoard
key={board.board_id}
board={board}
isSelected={selectedBoard?.board_id === board.board_id}
/>
))}
</Grid>
</OverlayScrollbarsComponent>
</>

View File

@ -26,6 +26,10 @@ import IAIDndImage from '../../../../common/components/IAIDndImage';
import { defaultSelectorOptions } from '../../../../app/store/util/defaultMemoizeOptions';
import { createSelector } from '@reduxjs/toolkit';
import { RootState } from '../../../../app/store/store';
import {
useDeleteBoardMutation,
useUpdateBoardMutation,
} from 'services/apiSlice';
const coverImageSelector = (imageName: string | undefined) =>
createSelector(
@ -59,19 +63,20 @@ const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
dispatch(boardIdSelected(board_id));
}, [board_id, dispatch]);
const handleDeleteBoard = useCallback(() => {
dispatch(boardDeleted(board_id));
}, [board_id, dispatch]);
const [updateBoard, { isLoading: isUpdateBoardLoading }] =
useUpdateBoardMutation();
const [deleteBoard, { isLoading: isDeleteBoardLoading }] =
useDeleteBoardMutation();
const handleUpdateBoardName = (newBoardName: string) => {
dispatch(
boardUpdated({
boardId: board_id,
requestBody: { board_name: newBoardName },
})
);
updateBoard({ board_id, changes: { board_name: newBoardName } });
};
const handleDeleteBoard = useCallback(() => {
deleteBoard(board_id);
}, [board_id, deleteBoard]);
const handleDrop = useCallback(
(droppedImage: ImageDTO) => {
if (droppedImage.board_id === board_id) {

View File

@ -9,6 +9,7 @@ import {
Divider,
Flex,
Select,
Spinner,
Text,
} from '@chakra-ui/react';
import IAIButton from 'common/components/IAIButton';
@ -19,9 +20,11 @@ import { useSelector } from 'react-redux';
import { selectBoardsAll } from '../../store/boardSlice';
import IAISelect from '../../../../common/components/IAISelect';
import IAIMantineSelect from 'common/components/IAIMantineSelect';
import { useListAllBoardsQuery } from 'services/apiSlice';
const UpdateImageBoardModal = () => {
const boards = useSelector(selectBoardsAll);
// const boards = useSelector(selectBoardsAll);
const { data: boards, isFetching } = useListAllBoardsQuery();
const { isOpen, onClose, handleAddToBoard, image } = useContext(
AddImageToBoardContext
);
@ -29,9 +32,9 @@ const UpdateImageBoardModal = () => {
const cancelRef = useRef<HTMLButtonElement>(null);
const currentBoard = boards.filter(
const currentBoard = boards?.find(
(board) => board.board_id === image?.board_id
)[0];
);
return (
<AlertDialog
@ -55,15 +58,19 @@ const UpdateImageBoardModal = () => {
<strong>{currentBoard.board_name}</strong> to
</Text>
)}
<IAIMantineSelect
placeholder="Select Board"
onChange={(v) => setSelectedBoard(v)}
value={selectedBoard}
data={boards.map((board) => ({
label: board.board_name,
value: board.board_id,
}))}
/>
{isFetching ? (
<Spinner />
) : (
<IAIMantineSelect
placeholder="Select Board"
onChange={(v) => setSelectedBoard(v)}
value={selectedBoard}
data={(boards ?? []).map((board) => ({
label: board.board_name,
value: board.board_id,
}))}
/>
)}
</Flex>
</Box>
</AlertDialogBody>
@ -73,7 +80,9 @@ const UpdateImageBoardModal = () => {
isDisabled={!selectedBoard}
colorScheme="accent"
onClick={() => {
if (selectedBoard) handleAddToBoard(selectedBoard);
if (selectedBoard) {
handleAddToBoard(selectedBoard);
}
}}
ml={3}
>

View File

@ -51,9 +51,12 @@ import { useAppToaster } from 'app/components/Toaster';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
import { DeleteImageContext } from 'app/contexts/DeleteImageContext';
import { DeleteImageButton } from './DeleteImageModal';
import { selectImagesById } from '../store/imagesSlice';
import { RootState } from 'app/store/store';
const currentImageButtonsSelector = createSelector(
[
(state: RootState) => state,
systemSelector,
gallerySelector,
postprocessingSelector,
@ -61,7 +64,7 @@ const currentImageButtonsSelector = createSelector(
lightboxSelector,
activeTabNameSelector,
],
(system, gallery, postprocessing, ui, lightbox, activeTabName) => {
(state, system, gallery, postprocessing, ui, lightbox, activeTabName) => {
const {
isProcessing,
isConnected,
@ -81,6 +84,8 @@ const currentImageButtonsSelector = createSelector(
shouldShowProgressInViewer,
} = ui;
const imageDTO = selectImagesById(state, gallery.selectedImage ?? '');
const { selectedImage } = gallery;
return {
@ -97,10 +102,10 @@ const currentImageButtonsSelector = createSelector(
activeTabName,
isLightboxOpen,
shouldHidePreview,
image: selectedImage,
seed: selectedImage?.metadata?.seed,
prompt: selectedImage?.metadata?.positive_conditioning,
negativePrompt: selectedImage?.metadata?.negative_conditioning,
image: imageDTO,
seed: imageDTO?.metadata?.seed,
prompt: imageDTO?.metadata?.positive_conditioning,
negativePrompt: imageDTO?.metadata?.negative_conditioning,
shouldShowProgressInViewer,
};
},

View File

@ -15,6 +15,8 @@ import { imageSelected } from '../store/gallerySlice';
import IAIDndImage from 'common/components/IAIDndImage';
import { ImageDTO } from 'services/api';
import { IAIImageFallback } from 'common/components/IAIImageFallback';
import { RootState } from 'app/store/store';
import { selectImagesById } from '../store/imagesSlice';
export const imagesSelector = createSelector(
[uiSelector, gallerySelector, systemSelector],
@ -29,7 +31,7 @@ export const imagesSelector = createSelector(
return {
shouldShowImageDetails,
shouldHidePreview,
image: selectedImage,
selectedImage,
progressImage,
shouldShowProgressInViewer,
shouldAntialiasProgressImage,
@ -45,11 +47,16 @@ export const imagesSelector = createSelector(
const CurrentImagePreview = () => {
const {
shouldShowImageDetails,
image,
selectedImage,
progressImage,
shouldShowProgressInViewer,
shouldAntialiasProgressImage,
} = useAppSelector(imagesSelector);
const image = useAppSelector((state: RootState) =>
selectImagesById(state, selectedImage ?? '')
);
const dispatch = useAppDispatch();
const handleDrop = useCallback(
@ -57,7 +64,7 @@ const CurrentImagePreview = () => {
if (droppedImage.image_name === image?.image_name) {
return;
}
dispatch(imageSelected(droppedImage));
dispatch(imageSelected(droppedImage.image_name));
},
[dispatch, image?.image_name]
);

View File

@ -72,17 +72,10 @@ interface HoverableImageProps {
isSelected: boolean;
}
const memoEqualityCheck = (
prev: HoverableImageProps,
next: HoverableImageProps
) =>
prev.image.image_name === next.image.image_name &&
prev.isSelected === next.isSelected;
/**
* Gallery image component with delete/use all/use seed buttons on hover.
*/
const HoverableImage = memo((props: HoverableImageProps) => {
const HoverableImage = (props: HoverableImageProps) => {
const dispatch = useAppDispatch();
const {
activeTabName,
@ -121,7 +114,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
const handleMouseOut = () => setIsHovered(false);
const handleSelectImage = useCallback(() => {
dispatch(imageSelected(image));
dispatch(imageSelected(image.image_name));
}, [image, dispatch]);
// Recall parameters handlers
@ -260,7 +253,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
</MenuItem>
)}
<MenuItem icon={<FaFolder />} onClickCapture={handleAddToBoard}>
Add to Board
{image.board_id ? 'Change Board' : 'Add to Board'}
</MenuItem>
<MenuItem
sx={{ color: 'error.300' }}
@ -357,8 +350,6 @@ const HoverableImage = memo((props: HoverableImageProps) => {
</ContextMenu>
</Box>
);
}, memoEqualityCheck);
};
HoverableImage.displayName = 'HoverableImage';
export default HoverableImage;
export default memo(HoverableImage);

View File

@ -201,12 +201,6 @@ const ImageGalleryContent = () => {
dispatch(setGalleryView('boards'));
}, [dispatch]);
const [newBoardName, setNewBoardName] = useState('');
const handleCreateNewBoard = () => {
dispatch(boardCreated({ requestBody: newBoardName }));
};
return (
<Flex
sx={{
@ -323,9 +317,7 @@ const ImageGalleryContent = () => {
<HoverableImage
key={`${item.image_name}-${item.thumbnail_url}`}
image={item}
isSelected={
selectedImage?.image_name === item?.image_name
}
isSelected={selectedImage === item?.image_name}
/>
</Flex>
)}
@ -344,9 +336,7 @@ const ImageGalleryContent = () => {
<HoverableImage
key={`${item.image_name}-${item.thumbnail_url}`}
image={item}
isSelected={
selectedImage?.image_name === item?.image_name
}
isSelected={selectedImage === item?.image_name}
/>
)}
/>

View File

@ -93,19 +93,11 @@ type ImageMetadataViewerProps = {
image: ImageDTO;
};
// TODO: I don't know if this is needed.
const memoEqualityCheck = (
prev: ImageMetadataViewerProps,
next: ImageMetadataViewerProps
) => prev.image.image_name === next.image.image_name;
// TODO: Show more interesting information in this component.
/**
* Image metadata viewer overlays currently selected image and provides
* access to any of its metadata for use in processing.
*/
const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
const dispatch = useAppDispatch();
const {
recallBothPrompts,
@ -333,8 +325,6 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
</Flex>
</Flex>
);
}, memoEqualityCheck);
};
ImageMetadataViewer.displayName = 'ImageMetadataViewer';
export default ImageMetadataViewer;
export default memo(ImageMetadataViewer);

View File

@ -42,7 +42,7 @@ export const nextPrevImageButtonsSelector = createSelector(
}
const currentImageIndex = filteredImageIds.findIndex(
(i) => i === selectedImage.image_name
(i) => i === selectedImage
);
const nextImageIndex = clamp(
@ -71,6 +71,8 @@ export const nextPrevImageButtonsSelector = createSelector(
!isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1,
nextImage,
prevImage,
nextImageId,
prevImageId,
};
},
{
@ -84,7 +86,7 @@ const NextPrevImageButtons = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const { isOnFirstImage, isOnLastImage, nextImage, prevImage } =
const { isOnFirstImage, isOnLastImage, nextImageId, prevImageId } =
useAppSelector(nextPrevImageButtonsSelector);
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] =
@ -99,19 +101,19 @@ const NextPrevImageButtons = () => {
}, []);
const handlePrevImage = useCallback(() => {
dispatch(imageSelected(prevImage));
}, [dispatch, prevImage]);
dispatch(imageSelected(prevImageId));
}, [dispatch, prevImageId]);
const handleNextImage = useCallback(() => {
dispatch(imageSelected(nextImage));
}, [dispatch, nextImage]);
dispatch(imageSelected(nextImageId));
}, [dispatch, nextImageId]);
useHotkeys(
'left',
() => {
handlePrevImage();
},
[prevImage]
[prevImageId]
);
useHotkeys(
@ -119,7 +121,7 @@ const NextPrevImageButtons = () => {
() => {
handleNextImage();
},
[nextImage]
[nextImageId]
);
return (

View File

@ -7,7 +7,7 @@ import { imageUrlsReceived } from 'services/thunks/image';
type GalleryImageObjectFitType = 'contain' | 'cover';
export interface GalleryState {
selectedImage?: ImageDTO;
selectedImage?: string;
galleryImageMinimumWidth: number;
galleryImageObjectFit: GalleryImageObjectFitType;
shouldAutoSwitchToNewImages: boolean;
@ -27,7 +27,7 @@ export const gallerySlice = createSlice({
name: 'gallery',
initialState: initialGalleryState,
reducers: {
imageSelected: (state, action: PayloadAction<ImageDTO | undefined>) => {
imageSelected: (state, action: PayloadAction<string | undefined>) => {
state.selectedImage = action.payload;
// TODO: if the user selects an image, disable the auto switch?
// state.shouldAutoSwitchToNewImages = false;
@ -63,17 +63,17 @@ export const gallerySlice = createSlice({
state.shouldAutoSwitchToNewImages &&
action.payload.image_category === 'general'
) {
state.selectedImage = action.payload;
state.selectedImage = action.payload.image_name;
}
});
builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
const { image_name, image_url, thumbnail_url } = action.payload;
// builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
// const { image_name, image_url, thumbnail_url } = action.payload;
if (state.selectedImage?.image_name === image_name) {
state.selectedImage.image_url = image_url;
state.selectedImage.thumbnail_url = thumbnail_url;
}
});
// if (state.selectedImage?.image_name === image_name) {
// state.selectedImage.image_url = image_url;
// state.selectedImage.thumbnail_url = thumbnail_url;
// }
// });
},
});

View File

@ -23,13 +23,9 @@ export type BoardDTO = {
*/
updated_at: string;
/**
* The name of the cover image of the board.
* The name of the board's cover image.
*/
cover_image_name?: string;
/**
* The URL of the thumbnail of the board's cover image.
*/
cover_image_url?: string;
/**
* The number of images in the board.
*/

View File

@ -17,13 +17,18 @@ export class BoardsService {
/**
* List Boards
* Gets a list of boards
* @returns OffsetPaginatedResults_BoardDTO_ Successful Response
* @returns any Successful Response
* @throws ApiError
*/
public static listBoards({
all,
offset,
limit = 10,
limit,
}: {
/**
* Whether to list all boards
*/
all?: boolean,
/**
* The page offset
*/
@ -32,11 +37,12 @@ export class BoardsService {
* The number of boards per page
*/
limit?: number,
}): CancelablePromise<OffsetPaginatedResults_BoardDTO_> {
}): CancelablePromise<(OffsetPaginatedResults_BoardDTO_ | Array<BoardDTO>)> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/boards/',
query: {
'all': all,
'offset': offset,
'limit': limit,
},
@ -53,15 +59,19 @@ export class BoardsService {
* @throws ApiError
*/
public static createBoard({
requestBody,
boardName,
}: {
requestBody: string,
/**
* The name of the board to create
*/
boardName: string,
}): CancelablePromise<BoardDTO> {
return __request(OpenAPI, {
method: 'POST',
url: '/api/v1/boards/',
body: requestBody,
mediaType: 'application/json',
query: {
'board_name': boardName,
},
errors: {
422: `Validation Error`,
},

View File

@ -0,0 +1,144 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { BoardDTO } from './api/models/BoardDTO';
import { OffsetPaginatedResults_BoardDTO_ } from './api/models/OffsetPaginatedResults_BoardDTO_';
import { BoardChanges } from './api/models/BoardChanges';
import { OffsetPaginatedResults_ImageDTO_ } from './api/models/OffsetPaginatedResults_ImageDTO_';
type ListBoardsArg = { offset: number; limit: number };
type UpdateBoardArg = { board_id: string; changes: BoardChanges };
type AddImageToBoardArg = { board_id: string; image_name: string };
type RemoveImageFromBoardArg = { board_id: string; image_name: string };
type ListBoardImagesArg = { board_id: string; offset: number; limit: number };
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:5173/api/v1/' }),
reducerPath: 'api',
tagTypes: ['Board'],
endpoints: (build) => ({
/**
* Boards Queries
*/
listBoards: build.query<OffsetPaginatedResults_BoardDTO_, ListBoardsArg>({
query: (arg) => ({ url: 'boards/', params: arg }),
providesTags: (result, error, arg) => {
if (!result) {
// Provide the broad 'Board' tag until there is a response
return ['Board'];
}
// Provide the broad 'Board' tab, and individual tags for each board
return [
...result.items.map(({ board_id }) => ({
type: 'Board' as const,
id: board_id,
})),
'Board',
];
},
}),
listAllBoards: build.query<Array<BoardDTO>, void>({
query: () => ({
url: 'boards/',
params: { all: true },
}),
providesTags: (result, error, arg) => {
if (!result) {
// Provide the broad 'Board' tag until there is a response
return ['Board'];
}
// Provide the broad 'Board' tab, and individual tags for each board
return [
...result.map(({ board_id }) => ({
type: 'Board' as const,
id: board_id,
})),
'Board',
];
},
}),
/**
* Boards Mutations
*/
createBoard: build.mutation<BoardDTO, string>({
query: (board_name) => ({
url: `boards/`,
method: 'POST',
params: { board_name },
}),
invalidatesTags: ['Board'],
}),
updateBoard: build.mutation<BoardDTO, UpdateBoardArg>({
query: ({ board_id, changes }) => ({
url: `boards/${board_id}`,
method: 'PATCH',
body: changes,
}),
invalidatesTags: (result, error, arg) => [
{ type: 'Board', id: arg.board_id },
],
}),
deleteBoard: build.mutation<void, string>({
query: (board_id) => ({ url: `boards/${board_id}`, method: 'DELETE' }),
invalidatesTags: (result, error, arg) => [{ type: 'Board', id: arg }],
}),
/**
* Board Images Queries
*/
listBoardImages: build.query<
OffsetPaginatedResults_ImageDTO_,
ListBoardImagesArg
>({
query: ({ board_id, offset, limit }) => ({
url: `board_images/${board_id}`,
method: 'DELETE',
body: { offset, limit },
}),
}),
/**
* Board Images Mutations
*/
addImageToBoard: build.mutation<void, AddImageToBoardArg>({
query: ({ board_id, image_name }) => ({
url: `board_images/`,
method: 'POST',
body: { board_id, image_name },
}),
invalidatesTags: ['Board'],
// invalidatesTags: (result, error, arg) => [
// { type: 'Board', id: arg.board_id },
// ],
}),
removeImageFromBoard: build.mutation<void, RemoveImageFromBoardArg>({
query: ({ board_id, image_name }) => ({
url: `board_images/`,
method: 'DELETE',
body: { board_id, image_name },
}),
invalidatesTags: (result, error, arg) => [
{ type: 'Board', id: arg.board_id },
],
}),
}),
});
export const {
useListBoardsQuery,
useListAllBoardsQuery,
useCreateBoardMutation,
useUpdateBoardMutation,
useDeleteBoardMutation,
useAddImageToBoardMutation,
useRemoveImageFromBoardMutation,
useListBoardImagesQuery,
} = api;

View File

@ -42,7 +42,7 @@ export const boardUpdated = createAppAsyncThunk(
type ImageAddedToBoardArg = Parameters<
(typeof BoardsService)['createBoardImage']
>[0];
>[0]['requestBody'];
export const imageAddedToBoard = createAppAsyncThunk(
'api/imageAddedToBoard',