document when to use redux vs nanostore, update change board flow to use nanostore

This commit is contained in:
Mary Hipp
2025-09-17 10:12:25 -04:00
parent efcb1bea7f
commit 4ca07a859d
7 changed files with 104 additions and 101 deletions

View File

@@ -1,13 +1,8 @@
import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
import { Combobox, ConfirmationAlertDialog, Flex, FormControl, Text } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAppSelector } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import {
changeBoardReset,
isModalOpenChanged,
selectChangeBoardModalSlice,
} from 'features/changeBoardModal/store/slice';
import { useChangeBoardModalApi, useChangeBoardModalState } from 'features/changeBoardModal/store/state';
import { selectSelectedBoardId } from 'features/gallery/store/gallerySelectors';
import { memo, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -15,30 +10,16 @@ import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
import { useAddImagesToBoardMutation, useRemoveImagesFromBoardMutation } from 'services/api/endpoints/images';
import { useAddVideosToBoardMutation, useRemoveVideosFromBoardMutation } from 'services/api/endpoints/videos';
const selectImagesToChange = createSelector(
selectChangeBoardModalSlice,
(changeBoardModal) => changeBoardModal.image_names
);
const selectVideosToChange = createSelector(
selectChangeBoardModalSlice,
(changeBoardModal) => changeBoardModal.video_ids
);
const selectIsModalOpen = createSelector(
selectChangeBoardModalSlice,
(changeBoardModal) => changeBoardModal.isModalOpen
);
const ChangeBoardModal = () => {
useAssertSingleton('ChangeBoardModal');
const dispatch = useAppDispatch();
const currentBoardId = useAppSelector(selectSelectedBoardId);
const [selectedBoardId, setSelectedBoardId] = useState<string | null>();
const { data: boards, isFetching } = useListAllBoardsQuery({ include_archived: true });
const isModalOpen = useAppSelector(selectIsModalOpen);
const imagesToChange = useAppSelector(selectImagesToChange);
const videosToChange = useAppSelector(selectVideosToChange);
const changeBoardModalState = useChangeBoardModalState();
const changeBoardModal = useChangeBoardModalApi();
const imagesToChange = changeBoardModalState.imageNames;
const videosToChange = changeBoardModalState.videoIds;
const isModalOpen = changeBoardModalState.isOpen;
const [addImagesToBoard] = useAddImagesToBoardMutation();
const [removeImagesFromBoard] = useRemoveImagesFromBoardMutation();
const [addVideosToBoard] = useAddVideosToBoardMutation();
@@ -61,9 +42,8 @@ const ChangeBoardModal = () => {
const value = useMemo(() => options.find((o) => o.value === selectedBoardId), [options, selectedBoardId]);
const handleClose = useCallback(() => {
dispatch(changeBoardReset());
dispatch(isModalOpenChanged(false));
}, [dispatch]);
changeBoardModal.close();
}, [changeBoardModal]);
const handleChangeBoard = useCallback(() => {
if (!selectedBoardId || (imagesToChange.length === 0 && videosToChange.length === 0)) {
@@ -90,10 +70,10 @@ const ChangeBoardModal = () => {
});
}
}
dispatch(changeBoardReset());
changeBoardModal.close();
}, [
addImagesToBoard,
dispatch,
changeBoardModal,
imagesToChange,
videosToChange,
removeImagesFromBoard,

View File

@@ -1,44 +0,0 @@
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 z from 'zod';
const zChangeBoardModalState = z.object({
isModalOpen: z.boolean().default(false),
image_names: z.array(z.string()).default(() => []),
video_ids: z.array(z.string()).default(() => []),
});
type ChangeBoardModalState = z.infer<typeof zChangeBoardModalState>;
const getInitialState = (): ChangeBoardModalState => zChangeBoardModalState.parse({});
const slice = createSlice({
name: 'changeBoardModal',
initialState: getInitialState(),
reducers: {
isModalOpenChanged: (state, action: PayloadAction<boolean>) => {
state.isModalOpen = action.payload;
},
imagesToChangeSelected: (state, action: PayloadAction<string[]>) => {
state.image_names = action.payload;
},
videosToChangeSelected: (state, action: PayloadAction<string[]>) => {
state.video_ids = action.payload;
},
changeBoardReset: (state) => {
state.image_names = [];
state.isModalOpen = false;
},
},
});
export const { isModalOpenChanged, imagesToChangeSelected, videosToChangeSelected, changeBoardReset } = slice.actions;
export const selectChangeBoardModalSlice = (state: RootState) => state.changeBoardModal;
export const changeBoardModalSliceConfig: SliceConfig<typeof slice> = {
slice,
schema: zChangeBoardModalState,
getInitialState,
};

View File

@@ -0,0 +1,70 @@
import { useStore } from '@nanostores/react';
import { atom } from 'nanostores';
import { useMemo } from 'react';
type ChangeBoardModalOpenArgs = {
imageNames?: string[];
videoIds?: string[];
};
type ChangeBoardModalState = {
isOpen: boolean;
imageNames: string[];
videoIds: string[];
};
const initialState: ChangeBoardModalState = {
isOpen: false,
imageNames: [],
videoIds: [],
};
const $changeBoardModalState = atom<ChangeBoardModalState>(initialState);
const openModal = ({ imageNames = [], videoIds = [] }: ChangeBoardModalOpenArgs = {}) => {
$changeBoardModalState.set({
isOpen: true,
imageNames: [...imageNames],
videoIds: [...videoIds],
});
};
const closeModal = () => {
$changeBoardModalState.set(initialState);
};
const setImageNames = (imageNames: string[]) => {
const current = $changeBoardModalState.get();
$changeBoardModalState.set({ ...current, imageNames: [...imageNames] });
};
const setVideoIds = (videoIds: string[]) => {
const current = $changeBoardModalState.get();
$changeBoardModalState.set({ ...current, videoIds: [...videoIds] });
};
const resetSelections = () => {
const current = $changeBoardModalState.get();
$changeBoardModalState.set({ ...current, imageNames: [], videoIds: [] });
};
export const useChangeBoardModalState = () => {
return useStore($changeBoardModalState);
};
export const useChangeBoardModalApi = () => {
return useMemo(
() => ({
open: openModal,
openWithImages: (imageNames: string[]) => openModal({ imageNames }),
openWithVideos: (videoIds: string[]) => openModal({ videoIds }),
setImageNames,
setVideoIds,
resetSelections,
close: closeModal,
}),
[]
);
};
export type { ChangeBoardModalState };

View File

@@ -1,29 +1,23 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import {
imagesToChangeSelected,
isModalOpenChanged,
videosToChangeSelected,
} from 'features/changeBoardModal/store/slice';
import { useChangeBoardModalApi } from 'features/changeBoardModal/store/state';
import { useItemDTOContext } from 'features/gallery/contexts/ItemDTOContext';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiFoldersBold } from 'react-icons/pi';
import { isImageDTO } from 'services/api/types';
import { isImageDTO, isVideoDTO } from 'services/api/types';
export const ContextMenuItemChangeBoard = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const itemDTO = useItemDTOContext();
const changeBoardModal = useChangeBoardModalApi();
const onClick = useCallback(() => {
if (isImageDTO(itemDTO)) {
dispatch(imagesToChangeSelected([itemDTO.image_name]));
} else {
dispatch(videosToChangeSelected([itemDTO.video_id]));
changeBoardModal.openWithImages([itemDTO.image_name]);
} else if (isVideoDTO(itemDTO)) {
changeBoardModal.openWithVideos([itemDTO.video_id]);
}
dispatch(isModalOpenChanged(true));
}, [dispatch, itemDTO]);
}, [changeBoardModal, itemDTO]);
return (
<MenuItem icon={<PiFoldersBold />} onClickCapture={onClick}>

View File

@@ -1,8 +1,8 @@
import { MenuDivider, MenuItem } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { $customStarUI } from 'app/store/nanostores/customStarUI';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { imagesToChangeSelected, isModalOpenChanged } from 'features/changeBoardModal/store/slice';
import { useAppSelector } from 'app/store/storeHooks';
import { useChangeBoardModalApi } from 'features/changeBoardModal/store/state';
import { useDeleteImageModalApi } from 'features/deleteImageModal/store/state';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { memo, useCallback } from 'react';
@@ -16,10 +16,10 @@ import {
const MultipleSelectionMenuItems = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const selection = useAppSelector((s) => s.gallery.selection);
const customStarUi = useStore($customStarUI);
const deleteImageModal = useDeleteImageModalApi();
const changeBoardModal = useChangeBoardModalApi();
const isBulkDownloadEnabled = useFeatureStatus('bulkDownload');
@@ -28,9 +28,8 @@ const MultipleSelectionMenuItems = () => {
const [bulkDownload] = useBulkDownloadImagesMutation();
const handleChangeBoard = useCallback(() => {
dispatch(imagesToChangeSelected(selection.map((s) => s.id)));
dispatch(isModalOpenChanged(true));
}, [dispatch, selection]);
changeBoardModal.openWithImages(selection.map((s) => s.id));
}, [changeBoardModal, selection]);
const handleDeleteSelection = useCallback(() => {
deleteImageModal.delete(selection.map((s) => s.id));

View File

@@ -1,8 +1,8 @@
import { MenuDivider, MenuItem } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { $customStarUI } from 'app/store/nanostores/customStarUI';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { isModalOpenChanged, videosToChangeSelected } from 'features/changeBoardModal/store/slice';
import { useAppSelector } from 'app/store/storeHooks';
import { useChangeBoardModalApi } from 'features/changeBoardModal/store/state';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiFoldersBold, PiStarBold, PiStarFill, PiTrashSimpleBold } from 'react-icons/pi';
@@ -10,18 +10,17 @@ import { useDeleteVideosMutation, useStarVideosMutation, useUnstarVideosMutation
const MultipleSelectionMenuItems = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const selection = useAppSelector((s) => s.gallery.selection);
const customStarUi = useStore($customStarUI);
const changeBoardModal = useChangeBoardModalApi();
const [starVideos] = useStarVideosMutation();
const [unstarVideos] = useUnstarVideosMutation();
const [deleteVideos] = useDeleteVideosMutation();
const handleChangeBoard = useCallback(() => {
dispatch(videosToChangeSelected(selection.map((s) => s.id)));
dispatch(isModalOpenChanged(true));
}, [dispatch, selection]);
changeBoardModal.openWithVideos(selection.map((s) => s.id));
}, [changeBoardModal, selection]);
const handleDeleteSelection = useCallback(() => {
// TODO: Add confirm on delete and video usage functionality

View File

@@ -1,6 +1,7 @@
import { EMPTY_ARRAY } from 'app/store/constants';
import { useAppSelector } from 'app/store/storeHooks';
import { selectGetVideoIdsQueryArgs } from 'features/gallery/store/gallerySelectors';
import { selectAllowVideo } from 'features/system/store/configSlice';
import { useGetVideoIdsQuery } from 'services/api/endpoints/videos';
import { useDebounce } from 'use-debounce';
@@ -14,8 +15,12 @@ const getVideoIdsQueryOptions = {
} satisfies Parameters<typeof useGetVideoIdsQuery>[1];
export const useGalleryVideoIds = () => {
const isVideoEnabled = useAppSelector(selectAllowVideo);
const _queryArgs = useAppSelector(selectGetVideoIdsQueryArgs);
const [queryArgs] = useDebounce(_queryArgs, 300);
const { videoIds, isLoading, isFetching } = useGetVideoIdsQuery(queryArgs, getVideoIdsQueryOptions);
const { videoIds, isLoading, isFetching } = useGetVideoIdsQuery(queryArgs, {
...getVideoIdsQueryOptions,
skip: !isVideoEnabled,
});
return { videoIds, isLoading, isFetching, queryArgs };
};