(ui) the most basic crud ui: view list of presets, create a new preset, edit/delete existing presets

This commit is contained in:
Mary Hipp 2024-08-05 15:48:23 -04:00
parent af9110e964
commit fd7a635777
11 changed files with 290 additions and 3 deletions

View File

@ -13,6 +13,7 @@ import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardMo
import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageModal'; import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageModal';
import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal'; import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal';
import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast'; import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast';
import { StylePresetModal } from 'features/stylePresets/components/StylePresetModal';
import { configChanged } from 'features/system/store/configSlice'; import { configChanged } from 'features/system/store/configSlice';
import { languageSelector } from 'features/system/store/systemSelectors'; import { languageSelector } from 'features/system/store/systemSelectors';
import InvokeTabs from 'features/ui/components/InvokeTabs'; import InvokeTabs from 'features/ui/components/InvokeTabs';
@ -104,6 +105,7 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage, destination }: Props) =>
<DeleteImageModal /> <DeleteImageModal />
<ChangeBoardModal /> <ChangeBoardModal />
<DynamicPromptsModal /> <DynamicPromptsModal />
<StylePresetModal />
<PreselectedImage selectedImage={selectedImage} /> <PreselectedImage selectedImage={selectedImage} />
</ErrorBoundary> </ErrorBoundary>
); );

View File

@ -28,6 +28,7 @@ import { generationPersistConfig, generationSlice } from 'features/parameters/st
import { upscalePersistConfig, upscaleSlice } from 'features/parameters/store/upscaleSlice'; import { upscalePersistConfig, upscaleSlice } from 'features/parameters/store/upscaleSlice';
import { queueSlice } from 'features/queue/store/queueSlice'; import { queueSlice } from 'features/queue/store/queueSlice';
import { sdxlPersistConfig, sdxlSlice } from 'features/sdxl/store/sdxlSlice'; import { sdxlPersistConfig, sdxlSlice } from 'features/sdxl/store/sdxlSlice';
import { stylePresetModalSlice } from 'features/stylePresets/store/slice';
import { configSlice } from 'features/system/store/configSlice'; import { configSlice } from 'features/system/store/configSlice';
import { systemPersistConfig, systemSlice } from 'features/system/store/systemSlice'; import { systemPersistConfig, systemSlice } from 'features/system/store/systemSlice';
import { uiPersistConfig, uiSlice } from 'features/ui/store/uiSlice'; import { uiPersistConfig, uiSlice } from 'features/ui/store/uiSlice';
@ -69,6 +70,7 @@ const allReducers = {
[workflowSettingsSlice.name]: workflowSettingsSlice.reducer, [workflowSettingsSlice.name]: workflowSettingsSlice.reducer,
[api.reducerPath]: api.reducer, [api.reducerPath]: api.reducer,
[upscaleSlice.name]: upscaleSlice.reducer, [upscaleSlice.name]: upscaleSlice.reducer,
[stylePresetModalSlice.name]: stylePresetModalSlice.reducer
}; };
const rootReducer = combineReducers(allReducers); const rootReducer = combineReducers(allReducers);

View File

@ -7,6 +7,7 @@ import { ParamPositivePrompt } from 'features/parameters/components/Core/ParamPo
import { selectGenerationSlice } from 'features/parameters/store/generationSlice'; import { selectGenerationSlice } from 'features/parameters/store/generationSlice';
import { ParamSDXLNegativeStylePrompt } from 'features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt'; import { ParamSDXLNegativeStylePrompt } from 'features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt';
import { ParamSDXLPositiveStylePrompt } from 'features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt'; import { ParamSDXLPositiveStylePrompt } from 'features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt';
import { StylePresetMenuTrigger } from 'features/stylePresets/components/StylePresetMenuTrigger';
import { memo } from 'react'; import { memo } from 'react';
const concatPromptsSelector = createSelector( const concatPromptsSelector = createSelector(
@ -20,6 +21,7 @@ export const Prompts = memo(() => {
const shouldConcatPrompts = useAppSelector(concatPromptsSelector); const shouldConcatPrompts = useAppSelector(concatPromptsSelector);
return ( return (
<Flex flexDir="column" gap={2}> <Flex flexDir="column" gap={2}>
<StylePresetMenuTrigger />
<ParamPositivePrompt /> <ParamPositivePrompt />
{!shouldConcatPrompts && <ParamSDXLPositiveStylePrompt />} {!shouldConcatPrompts && <ParamSDXLPositiveStylePrompt />}
<ParamNegativePrompt /> <ParamNegativePrompt />

View File

@ -0,0 +1,88 @@
import { Button, Flex, FormControl, FormLabel, Input, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { isModalOpenChanged,updatingStylePresetChanged } from 'features/stylePresets/store/slice';
import { toast } from 'features/toast/toast';
import type { ChangeEventHandler} from 'react';
import { useCallback, useEffect, useState } from 'react';
import type {
StylePresetRecordDTO} from 'services/api/endpoints/stylePresets';
import {
useCreateStylePresetMutation,
useUpdateStylePresetMutation,
} from 'services/api/endpoints/stylePresets';
export const StylePresetForm = ({ updatingPreset }: { updatingPreset: StylePresetRecordDTO | null }) => {
const [createStylePreset] = useCreateStylePresetMutation();
const [updateStylePreset] = useUpdateStylePresetMutation();
const dispatch = useAppDispatch();
const [name, setName] = useState(updatingPreset ? updatingPreset.name : '');
const [posPrompt, setPosPrompt] = useState(updatingPreset ? updatingPreset.preset_data.positive_prompt : '');
const [negPrompt, setNegPrompt] = useState(updatingPreset ? updatingPreset.preset_data.negative_prompt : '');
const handleChangeName = useCallback<ChangeEventHandler<HTMLInputElement>>((e) => {
setName(e.target.value);
}, []);
const handleChangePosPrompt = useCallback<ChangeEventHandler<HTMLTextAreaElement>>((e) => {
setPosPrompt(e.target.value);
}, []);
const handleChangeNegPrompt = useCallback<ChangeEventHandler<HTMLTextAreaElement>>((e) => {
setNegPrompt(e.target.value);
}, []);
useEffect(() => {
if (updatingPreset) {
setName(updatingPreset.name);
setPosPrompt(updatingPreset.preset_data.positive_prompt);
setNegPrompt(updatingPreset.preset_data.negative_prompt);
} else {
setName('');
setPosPrompt('');
setNegPrompt('');
}
}, [updatingPreset]);
const handleClickSave = useCallback(async () => {
try {
if (updatingPreset) {
await updateStylePreset({
id: updatingPreset.id,
changes: { name, preset_data: { positive_prompt: posPrompt, negative_prompt: negPrompt } },
}).unwrap();
} else {
await createStylePreset({
name: name,
preset_data: { positive_prompt: posPrompt, negative_prompt: negPrompt },
}).unwrap();
}
} catch (error) {
toast({
status: 'error',
title: 'Failed to save style preset',
});
}
dispatch(updatingStylePresetChanged(null));
dispatch(isModalOpenChanged(false));
}, [dispatch, updatingPreset, name, posPrompt, negPrompt, updateStylePreset, createStylePreset]);
return (
<Flex flexDir="column" gap="4">
<FormControl>
<FormLabel>Name</FormLabel>
<Input value={name} onChange={handleChangeName} />
</FormControl>
<FormControl>
<FormLabel>Positive Prompt</FormLabel>
<Textarea value={posPrompt} onChange={handleChangePosPrompt} />
</FormControl>
<FormControl>
<FormLabel>Negative Prompt</FormLabel>
<Textarea value={negPrompt} onChange={handleChangeNegPrompt} />
</FormControl>
<Button onClick={handleClickSave}>Save</Button>
</Flex>
);
};

View File

@ -0,0 +1,46 @@
import { Button, Flex, Text } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { isModalOpenChanged, updatingStylePresetChanged } from 'features/stylePresets/store/slice';
import { useCallback } from 'react';
import type { StylePresetRecordDTO} from 'services/api/endpoints/stylePresets';
import { useDeleteStylePresetMutation } from 'services/api/endpoints/stylePresets';
export const StylePresetListItem = ({ preset }: { preset: StylePresetRecordDTO }) => {
const dispatch = useAppDispatch();
const [deleteStylePreset] = useDeleteStylePresetMutation();
const handleClickEdit = useCallback(() => {
dispatch(updatingStylePresetChanged(preset));
dispatch(isModalOpenChanged(true));
}, [dispatch, preset]);
const handleDeletePreset = useCallback(async () => {
try {
await deleteStylePreset(preset.id);
} catch (error) {}
}, [preset]);
return (
<>
<Flex flexDir="column" gap="2">
<Text fontSize="md">{preset.name}</Text>
<Flex flexDir="column" layerStyle="third" borderRadius="base" padding="10px">
<Text fontSize="xs">
<Text as="span" fontWeight="semibold">
Positive prompt:
</Text>{' '}
{preset.preset_data.positive_prompt}
</Text>
<Text fontSize="xs">
<Text as="span" fontWeight="semibold">
Negative prompt:
</Text>{' '}
{preset.preset_data.negative_prompt}
</Text>
<Button onClick={handleClickEdit}>Edit</Button>
<Button onClick={handleDeletePreset}>Delete</Button>
</Flex>
</Flex>
</>
);
};

View File

@ -0,0 +1,34 @@
import { Button, Flex, Text } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { isModalOpenChanged, updatingStylePresetChanged } from 'features/stylePresets/store/slice';
import { useCallback } from 'react';
import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
import { StylePresetListItem } from './StylePresetListItem';
export const StylePresetMenu = () => {
const { data } = useListStylePresetsQuery({});
const dispatch = useAppDispatch();
const handleClickAddNew = useCallback(() => {
dispatch(updatingStylePresetChanged(null));
dispatch(isModalOpenChanged(true));
}, [dispatch]);
return (
<>
<Flex flexDir="column" gap="2">
<Flex alignItems="center" gap="10" w="full" justifyContent="space-between">
<Text fontSize="sm" fontWeight="semibold" userSelect="none" color="base.500">
Style Presets
</Text>
<Button size="sm" onClick={handleClickAddNew}>
Add New
</Button>
</Flex>
{data?.items.map((preset) => <StylePresetListItem preset={preset} key={preset.id} />)}
</Flex>
</>
);
};

View File

@ -0,0 +1,24 @@
import {
Button,
Popover,
PopoverBody,
PopoverContent,
PopoverTrigger,
} from '@invoke-ai/ui-library';
import { StylePresetMenu } from './StylePresetMenu';
export const StylePresetMenuTrigger = () => {
return (
<Popover isLazy>
<PopoverTrigger>
<Button size="sm">Style Presets</Button>
</PopoverTrigger>
<PopoverContent>
<PopoverBody>
<StylePresetMenu />
</PopoverBody>
</PopoverContent>
</Popover>
);
};

View File

@ -0,0 +1,43 @@
import {
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
} from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { isModalOpenChanged, updatingStylePresetChanged } from 'features/stylePresets/store/slice';
import { useCallback, useMemo } from 'react';
import { StylePresetForm } from './StylePresetForm';
export const StylePresetModal = () => {
const dispatch = useAppDispatch();
const isModalOpen = useAppSelector((s) => s.stylePresetModal.isModalOpen);
const updatingStylePreset = useAppSelector((s) => s.stylePresetModal.updatingStylePreset);
const modalTitle = useMemo(() => {
return updatingStylePreset ? `Update Style Preset` : `Create Style Preset`;
}, [updatingStylePreset]);
const handleCloseModal = useCallback(() => {
dispatch(updatingStylePresetChanged(null));
dispatch(isModalOpenChanged(false));
}, [dispatch]);
return (
<Modal isOpen={isModalOpen} onClose={handleCloseModal} isCentered size="2xl">
<ModalOverlay />
<ModalContent>
<ModalHeader>{modalTitle}</ModalHeader>
<ModalCloseButton />
<ModalBody display="flex" flexDir="column" gap={4}>
<StylePresetForm updatingPreset={updatingStylePreset} />
</ModalBody>
<ModalFooter />
</ModalContent>
</Modal>
);
};

View File

@ -0,0 +1,30 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { RootState } from 'app/store/store';
import type { StylePresetRecordDTO } from 'services/api/endpoints/stylePresets';
import type { StylePresetState } from './types';
export const initialState: StylePresetState = {
isModalOpen: false,
updatingStylePreset: null,
};
export const stylePresetModalSlice = createSlice({
name: 'stylePresetModal',
initialState: initialState,
reducers: {
isModalOpenChanged: (state, action: PayloadAction<boolean>) => {
state.isModalOpen = action.payload;
},
updatingStylePresetChanged: (state, action: PayloadAction<StylePresetRecordDTO | null>) => {
state.updatingStylePreset = action.payload;
},
},
});
export const { isModalOpenChanged, updatingStylePresetChanged } = stylePresetModalSlice.actions;
export const selectStylePresetModalSlice = (state: RootState) => state.stylePresetModal;

View File

@ -0,0 +1,8 @@
import type { StylePresetRecordDTO } from "services/api/endpoints/stylePresets";
export type StylePresetState = {
isModalOpen: boolean;
updatingStylePreset: StylePresetRecordDTO | null;
};

View File

@ -2,6 +2,8 @@ import type { paths } from 'services/api/schema';
import { api, buildV1Url, LIST_TAG } from '..'; import { api, buildV1Url, LIST_TAG } from '..';
export type StylePresetRecordDTO = paths['/api/v1/style_presets/i/{style_preset_id}']['get']['responses']['200']['content']['application/json']
/** /**
* Builds an endpoint URL for the style_presets router * Builds an endpoint URL for the style_presets router
* @example * @example
@ -17,7 +19,10 @@ export const stylePresetsApi = api.injectEndpoints({
string string
>({ >({
query: (style_preset_id) => buildStylePresetsUrl(`i/${style_preset_id}`), query: (style_preset_id) => buildStylePresetsUrl(`i/${style_preset_id}`),
providesTags: (result, error, style_preset_id) => [{ type: 'StylePreset', id: style_preset_id }, 'FetchOnReconnect'], providesTags: (result, error, style_preset_id) => [
{ type: 'StylePreset', id: style_preset_id },
'FetchOnReconnect',
],
}), }),
deleteStylePreset: build.mutation<void, string>({ deleteStylePreset: build.mutation<void, string>({
query: (style_preset_id) => ({ query: (style_preset_id) => ({
@ -45,14 +50,17 @@ export const stylePresetsApi = api.injectEndpoints({
}), }),
updateStylePreset: build.mutation< updateStylePreset: build.mutation<
paths['/api/v1/style_presets/i/{style_preset_id}']['patch']['responses']['200']['content']['application/json'], paths['/api/v1/style_presets/i/{style_preset_id}']['patch']['responses']['200']['content']['application/json'],
{ id: string, changes: paths['/api/v1/style_presets/i/{style_preset_id}']['patch']['requestBody']['content']['application/json']['changes'] } {
id: string;
changes: paths['/api/v1/style_presets/i/{style_preset_id}']['patch']['requestBody']['content']['application/json']['changes'];
}
>({ >({
query: ({ id, changes }) => ({ query: ({ id, changes }) => ({
url: buildStylePresetsUrl(`i/${id}`), url: buildStylePresetsUrl(`i/${id}`),
method: 'PATCH', method: 'PATCH',
body: { changes }, body: { changes },
}), }),
invalidatesTags: (response, error, { id, changes }) => [ invalidatesTags: (response, error, { id }) => [
{ type: 'StylePreset', id: LIST_TAG }, { type: 'StylePreset', id: LIST_TAG },
{ type: 'StylePreset', id: id }, { type: 'StylePreset', id: id },
], ],