clean up, add context menu to import/download templates

This commit is contained in:
Mary Hipp 2024-08-15 12:39:55 -04:00
parent 68dac6349d
commit 24f298283f
8 changed files with 174 additions and 159 deletions

View File

@ -128,9 +128,9 @@ async def parse_presets_from_file(file: UploadFile) -> list[StylePresetWithoutId
style_presets.append(style_preset) style_presets.append(style_preset)
except pydantic.ValidationError as e: except pydantic.ValidationError as e:
if file.content_type == "text/csv": if file.content_type == "text/csv":
msg = "Invalid CSV format: must include columns 'name', 'prompt', and 'negative_prompt'" msg = "Invalid CSV format: must include columns 'name', 'prompt', and 'negative_prompt' and name cannot be blank"
else: # file.content_type == "application/json": else: # file.content_type == "application/json":
msg = "Invalid JSON format: must be a list of objects with keys 'name', 'prompt', and 'negative_prompt'" msg = "Invalid JSON format: must be a list of objects with keys 'name', 'prompt', and 'negative_prompt' and name cannot be blank"
raise InvalidPresetImportDataError(msg) from e raise InvalidPresetImportDataError(msg) from e
finally: finally:
file.file.close() file.file.close()

View File

@ -1701,10 +1701,14 @@
"deleteImage": "Delete Image", "deleteImage": "Delete Image",
"deleteTemplate": "Delete Template", "deleteTemplate": "Delete Template",
"deleteTemplate2": "Are you sure you want to delete this template? This cannot be undone.", "deleteTemplate2": "Are you sure you want to delete this template? This cannot be undone.",
"downloadCsv": "Download CSV",
"editTemplate": "Edit Template", "editTemplate": "Edit Template",
"exportDownloaded": "Export Downloaded",
"exportFailed": "Unable to generate and download CSV",
"flatten": "Flatten selected template into current prompt", "flatten": "Flatten selected template into current prompt",
"importTemplates": "Import Prompt Templates", "importTemplates": "Import Prompt Templates",
"importTemplatesDesc": "Format must be either a CSV with columns: 'name', 'prompt' or 'positive_prompt', and 'negative_prompt' included, or a JSON file with keys 'name', 'prompt' or 'positive_prompt', and 'negative_prompt", "importTemplatesDesc": "Accepted formats: CSV or JSON",
"importTemplatesDesc2": "Accepted columns/keys: 'name', 'prompt' or 'positive_prompt', 'negative_prompt",
"insertPlaceholder": "Insert placeholder", "insertPlaceholder": "Insert placeholder",
"myTemplates": "My Templates", "myTemplates": "My Templates",
"name": "Name", "name": "Name",
@ -1719,6 +1723,7 @@
"searchByName": "Search by name", "searchByName": "Search by name",
"shared": "Shared", "shared": "Shared",
"sharedTemplates": "Shared Templates", "sharedTemplates": "Shared Templates",
"templateActions": "Template Actions",
"templateDeleted": "Prompt template deleted", "templateDeleted": "Prompt template deleted",
"toggleViewMode": "Toggle View Mode", "toggleViewMode": "Toggle View Mode",
"type": "Type", "type": "Type",

View File

@ -1,69 +0,0 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { IconButton, spinAnimation, Text } from '@invoke-ai/ui-library';
import { toast } from 'features/toast/toast';
import { useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
import { useTranslation } from 'react-i18next';
import { PiSpinner, PiUploadBold } from 'react-icons/pi';
import { useImportStylePresetsMutation } from 'services/api/endpoints/stylePresets';
const loadingStyles: SystemStyleObject = {
svg: { animation: spinAnimation },
};
export const StylePresetImport = () => {
const [importStylePresets, { isLoading }] = useImportStylePresetsMutation();
const { t } = useTranslation();
const onDropAccepted = useCallback(
async (files: File[]) => {
const file = files[0];
if (!file) {
return;
}
try {
await importStylePresets(file).unwrap();
toast({
status: 'success',
title: t('toast.importSuccessful'),
});
} catch (error) {
toast({
status: 'error',
title: t('toast.importFailed'),
});
}
},
[importStylePresets, t]
);
const { getInputProps, getRootProps } = useDropzone({
accept: { 'text/csv': ['.csv'], 'application/json': ['.json'] },
onDropAccepted,
noDrag: true,
multiple: false,
});
return (
<>
<IconButton
icon={!isLoading ? <PiUploadBold /> : <PiSpinner />}
tooltip={
<>
<Text fontWeight="semibold">{t('stylePresets.importTemplates')}</Text>
<Text>{t('stylePresets.importTemplatesDesc')}</Text>
</>
}
aria-label={t('stylePresets.importTemplates')}
size="md"
variant="link"
w={8}
h={8}
sx={isLoading ? loadingStyles : undefined}
isDisabled={isLoading}
{...getRootProps()}
/>
<input {...getInputProps()} />
</>
);
};

View File

@ -1,27 +1,13 @@
import { Flex, IconButton, Text } from '@invoke-ai/ui-library'; import { Flex, Text } from '@invoke-ai/ui-library';
import { EMPTY_ARRAY } from 'app/store/constants'; import { EMPTY_ARRAY } from 'app/store/constants';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { $stylePresetModalState } from 'features/stylePresets/store/stylePresetModal';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiDownloadBold, PiPlusBold } from 'react-icons/pi';
import type { StylePresetRecordWithImage } from 'services/api/endpoints/stylePresets'; import type { StylePresetRecordWithImage } from 'services/api/endpoints/stylePresets';
import { useLazyExportStylePresetsQuery, useListStylePresetsQuery } from 'services/api/endpoints/stylePresets'; import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
import { StylePresetImport } from './StylePresetImport';
import { StylePresetList } from './StylePresetList'; import { StylePresetList } from './StylePresetList';
import { StylePresetMenuActions } from './StylePresetMenuActions/StylePresetMenuActions';
import StylePresetSearch from './StylePresetSearch'; import StylePresetSearch from './StylePresetSearch';
import { toast } from '../../toast/toast';
const generateCSV = (data: any[]) => {
const header = ['Column1', 'Column2', 'Column3'];
const csvRows = [
header.join(','), // add header row
...data.map((row) => row.join(',')), // add data rows
];
return csvRows.join('\n');
};
export const StylePresetMenu = () => { export const StylePresetMenu = () => {
const searchTerm = useAppSelector((s) => s.stylePreset.searchTerm); const searchTerm = useAppSelector((s) => s.stylePreset.searchTerm);
@ -59,73 +45,12 @@ export const StylePresetMenu = () => {
}); });
const { t } = useTranslation(); const { t } = useTranslation();
const [exportStylePresets, { isLoading }] = useLazyExportStylePresetsQuery();
const handleClickAddNew = useCallback(() => {
$stylePresetModalState.set({
prefilledFormData: null,
updatingStylePresetId: null,
isModalOpen: true,
});
}, []);
const handleClickDownloadCsv = useCallback(async () => {
let blob;
try {
const response = await exportStylePresets().unwrap();
blob = new Blob([response], { type: 'text/csv' });
} catch (error) {
toast({
status: 'error',
title: 'Unable to generate and download export',
});
}
if (blob) {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'data.csv';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}
toast({
status: 'success',
title: 'Export downloaded',
});
}, [exportStylePresets]);
return ( return (
<Flex flexDir="column" gap={2} padding={3} layerStyle="second" borderRadius="base"> <Flex flexDir="column" gap={2} padding={3} layerStyle="second" borderRadius="base">
<Flex alignItems="center" gap={2} w="full" justifyContent="space-between"> <Flex alignItems="center" gap={2} w="full" justifyContent="space-between">
<StylePresetSearch /> <StylePresetSearch />
<Flex alignItems="center" justifyContent="space-between"> <StylePresetMenuActions />
<StylePresetImport />
<IconButton
icon={<PiDownloadBold />}
tooltip={t('stylePresets.createPromptTemplate')}
aria-label={t('stylePresets.createPromptTemplate')}
onClick={handleClickDownloadCsv}
size="md"
variant="link"
w={8}
h={8}
isDisabled={isLoading}
/>
<IconButton
icon={<PiPlusBold />}
tooltip={t('stylePresets.createPromptTemplate')}
aria-label={t('stylePresets.createPromptTemplate')}
onClick={handleClickAddNew}
size="md"
variant="link"
w={8}
h={8}
/>
</Flex>
</Flex> </Flex>
{data.presets.length === 0 && data.defaultPresets.length === 0 && ( {data.presets.length === 0 && data.defaultPresets.length === 0 && (
@ -135,11 +60,9 @@ export const StylePresetMenu = () => {
)} )}
<StylePresetList title={t('stylePresets.myTemplates')} data={data.presets} /> <StylePresetList title={t('stylePresets.myTemplates')} data={data.presets} />
{allowPrivateStylePresets && ( {allowPrivateStylePresets && (
<StylePresetList title={t('stylePresets.sharedTemplates')} data={data.sharedPresets} /> <StylePresetList title={t('stylePresets.sharedTemplates')} data={data.sharedPresets} />
)} )}
<StylePresetList title={t('stylePresets.defaultTemplates')} data={data.defaultPresets} /> <StylePresetList title={t('stylePresets.defaultTemplates')} data={data.defaultPresets} />
</Flex> </Flex>
); );

View File

@ -0,0 +1,47 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { toast } from 'features/toast/toast';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiDownloadSimpleBold, PiSpinner } from 'react-icons/pi';
import { useLazyExportStylePresetsQuery } from 'services/api/endpoints/stylePresets';
export const StylePresetExport = ({ onClose }: { onClose: () => void }) => {
const [exportStylePresets, { isLoading }] = useLazyExportStylePresetsQuery();
const { t } = useTranslation();
const handleClickDownloadCsv = useCallback(async () => {
let blob;
try {
const response = await exportStylePresets().unwrap();
blob = new Blob([response], { type: 'text/csv' });
} catch (error) {
toast({
status: 'error',
title: t('stylePresets.exportFailed'),
});
} finally {
onClose();
}
if (blob) {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'data.csv';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
toast({
status: 'success',
title: t('stylePresets.exportDownloaded'),
});
}
}, [exportStylePresets, onClose, t]);
return (
<MenuItem icon={!isLoading ? <PiDownloadSimpleBold /> : <PiSpinner />} onClickCapture={handleClickDownloadCsv}>
{t('stylePresets.downloadCsv')}
</MenuItem>
);
};

View File

@ -0,0 +1,62 @@
import { Flex, MenuItem, Text } from '@invoke-ai/ui-library';
import { toast } from 'features/toast/toast';
import { useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
import { useTranslation } from 'react-i18next';
import { PiSpinner, PiUploadSimpleBold } from 'react-icons/pi';
import { useImportStylePresetsMutation } from 'services/api/endpoints/stylePresets';
export const StylePresetImport = ({ onClose }: { onClose: () => void }) => {
const [importStylePresets, { isLoading }] = useImportStylePresetsMutation();
const { t } = useTranslation();
const onDropAccepted = useCallback(
(files: File[]) => {
const file = files[0];
if (!file) {
return;
}
importStylePresets(file)
.unwrap()
.then(() => {
toast({
status: 'success',
title: t('toast.importSuccessful'),
});
})
.catch((error) => {
toast({
status: 'error',
title: t('toast.importFailed'),
description: error ? `${error.data.detail}` : undefined,
});
})
.finally(() => {
onClose();
});
},
[importStylePresets, t, onClose]
);
const { getInputProps, getRootProps } = useDropzone({
accept: { 'text/csv': ['.csv'], 'application/json': ['.json'] },
onDropAccepted,
noDrag: true,
multiple: false,
});
return (
<MenuItem icon={!isLoading ? <PiUploadSimpleBold /> : <PiSpinner />} alignItems="flex-start" {...getRootProps()}>
<Flex flexDir="column">
<Text>{t('stylePresets.importTemplates')}</Text>
<Text maxW="200px" fontSize="xs" variant="subtext">
{t('stylePresets.importTemplatesDesc')}
</Text>
<Text maxW="200px" fontSize="xs" variant="subtext">
{t('stylePresets.importTemplatesDesc2')}
</Text>
</Flex>
<input {...getInputProps()} />
</MenuItem>
);
};

View File

@ -0,0 +1,50 @@
import { Flex, IconButton, Menu, MenuButton, MenuList, useDisclosure } from '@invoke-ai/ui-library';
import { $stylePresetModalState } from 'features/stylePresets/store/stylePresetModal';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiDotsThreeOutlineFill, PiPlusBold } from 'react-icons/pi';
import { StylePresetExport } from './StylePresetExport';
import { StylePresetImport } from './StylePresetImport';
export const StylePresetMenuActions = () => {
const { isOpen, onClose, onToggle } = useDisclosure();
const handleClickAddNew = useCallback(() => {
$stylePresetModalState.set({
prefilledFormData: null,
updatingStylePresetId: null,
isModalOpen: true,
});
}, []);
const { t } = useTranslation();
return (
<Flex alignItems="center" justifyContent="space-between">
<IconButton
icon={<PiPlusBold />}
tooltip={t('stylePresets.createPromptTemplate')}
aria-label={t('stylePresets.createPromptTemplate')}
onClick={handleClickAddNew}
size="md"
variant="ghost"
w={8}
h={8}
/>
<Menu isOpen={isOpen}>
<MenuButton
variant="ghost"
as={IconButton}
aria-label={t('stylePresets.templateActions')}
tooltip={t('stylePresets.templateActions')}
icon={<PiDotsThreeOutlineFill />}
onClick={onToggle}
/>
<MenuList>
<StylePresetImport onClose={onClose} />
<StylePresetExport onClose={onClose} />
</MenuList>
</Menu>
</Flex>
);
};

View File

@ -92,13 +92,10 @@ export const stylePresetsApi = api.injectEndpoints({
}), }),
providesTags: ['FetchOnReconnect', { type: 'StylePreset', id: LIST_TAG }], providesTags: ['FetchOnReconnect', { type: 'StylePreset', id: LIST_TAG }],
}), }),
exportStylePresets: build.query< exportStylePresets: build.query<string, void>({
string,
void
>({
query: () => ({ query: () => ({
url: buildStylePresetsUrl("/export"), url: buildStylePresetsUrl('/export'),
responseHandler: response => response.text() responseHandler: (response) => response.text(),
}), }),
providesTags: ['FetchOnReconnect', { type: 'StylePreset', id: LIST_TAG }], providesTags: ['FetchOnReconnect', { type: 'StylePreset', id: LIST_TAG }],
}), }),