feat(ui): use buttons instead of menu for preset import/export

This commit is contained in:
psychedelicious 2024-08-16 09:58:19 +10:00
parent 7a3eaa8da9
commit 26bfbdec7f
7 changed files with 149 additions and 120 deletions

View File

@ -1701,14 +1701,16 @@
"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", "exportPromptTemplates": "Export My Prompt Templates (CSV)",
"editTemplate": "Edit Template", "editTemplate": "Edit Template",
"exportDownloaded": "Export Downloaded", "exportDownloaded": "Export Downloaded",
"exportFailed": "Unable to generate and download CSV", "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 (CSV/JSON)",
"importTemplatesDesc": "Accepted formats: CSV or JSON", "acceptedColumnsKeys": "Accepted columns/keys:",
"importTemplatesDesc2": "Accepted columns/keys: 'name', 'prompt' or 'positive_prompt', 'negative_prompt", "nameColumn": "'name'",
"positivePromptColumn": "'prompt' or 'positive_prompt'",
"negativePromptColumn": "'negative_prompt'",
"insertPlaceholder": "Insert placeholder", "insertPlaceholder": "Insert placeholder",
"myTemplates": "My Templates", "myTemplates": "My Templates",
"name": "Name", "name": "Name",

View File

@ -0,0 +1,30 @@
import { IconButton } from '@invoke-ai/ui-library';
import { $stylePresetModalState } from 'features/stylePresets/store/stylePresetModal';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPlusBold } from 'react-icons/pi';
export const StylePresetCreateButton = () => {
const handleClickAddNew = useCallback(() => {
$stylePresetModalState.set({
prefilledFormData: null,
updatingStylePresetId: null,
isModalOpen: true,
});
}, []);
const { t } = useTranslation();
return (
<IconButton
icon={<PiPlusBold />}
tooltip={t('stylePresets.createPromptTemplate')}
aria-label={t('stylePresets.createPromptTemplate')}
onClick={handleClickAddNew}
size="md"
variant="ghost"
w={8}
h={8}
/>
);
};

View File

@ -0,0 +1,68 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { IconButton, spinAnimation } from '@invoke-ai/ui-library';
import { EMPTY_ARRAY } from 'app/store/constants';
import { toast } from 'features/toast/toast';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiDownloadSimpleBold, PiSpinner } from 'react-icons/pi';
import { useLazyExportStylePresetsQuery, useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
const loadingStyles: SystemStyleObject = {
svg: { animation: spinAnimation },
};
export const StylePresetExportButton = () => {
const [exportStylePresets, { isLoading }] = useLazyExportStylePresetsQuery();
const { t } = useTranslation();
const { presetCount } = useListStylePresetsQuery(undefined, {
selectFromResult: ({ data }) => {
const userPresets = data?.filter((preset) => preset.type === 'user') ?? EMPTY_ARRAY;
return {
presetCount: userPresets.length,
};
},
});
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'),
});
return;
}
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, t]);
return (
<IconButton
onClick={handleClickDownloadCsv}
icon={!isLoading ? <PiDownloadSimpleBold /> : <PiSpinner />}
tooltip={t('stylePresets.exportPromptTemplates')}
aria-label={t('stylePresets.exportPromptTemplates')}
size="md"
variant="link"
w={8}
h={8}
sx={isLoading ? loadingStyles : undefined}
isDisabled={isLoading || presetCount === 0}
/>
);
};

View File

@ -1,4 +1,5 @@
import { Flex, MenuItem, Text } from '@invoke-ai/ui-library'; import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex, IconButton, ListItem, spinAnimation, Text, UnorderedList } from '@invoke-ai/ui-library';
import { toast } from 'features/toast/toast'; import { toast } from 'features/toast/toast';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useDropzone } from 'react-dropzone'; import { useDropzone } from 'react-dropzone';
@ -6,7 +7,11 @@ import { useTranslation } from 'react-i18next';
import { PiSpinner, PiUploadSimpleBold } from 'react-icons/pi'; import { PiSpinner, PiUploadSimpleBold } from 'react-icons/pi';
import { useImportStylePresetsMutation } from 'services/api/endpoints/stylePresets'; import { useImportStylePresetsMutation } from 'services/api/endpoints/stylePresets';
export const StylePresetImport = ({ onClose }: { onClose: () => void }) => { const loadingStyles: SystemStyleObject = {
svg: { animation: spinAnimation },
};
export const StylePresetImportButton = () => {
const [importStylePresets, { isLoading }] = useImportStylePresetsMutation(); const [importStylePresets, { isLoading }] = useImportStylePresetsMutation();
const { t } = useTranslation(); const { t } = useTranslation();
@ -30,12 +35,9 @@ export const StylePresetImport = ({ onClose }: { onClose: () => void }) => {
title: t('toast.importFailed'), title: t('toast.importFailed'),
description: error ? `${error.data.detail}` : undefined, description: error ? `${error.data.detail}` : undefined,
}); });
})
.finally(() => {
onClose();
}); });
}, },
[importStylePresets, t, onClose] [importStylePresets, t]
); );
const { getInputProps, getRootProps } = useDropzone({ const { getInputProps, getRootProps } = useDropzone({
@ -46,17 +48,37 @@ export const StylePresetImport = ({ onClose }: { onClose: () => void }) => {
}); });
return ( return (
<MenuItem icon={!isLoading ? <PiUploadSimpleBold /> : <PiSpinner />} alignItems="flex-start" {...getRootProps()}> <>
<Flex flexDir="column"> <IconButton
<Text>{t('stylePresets.importTemplates')}</Text> icon={!isLoading ? <PiUploadSimpleBold /> : <PiSpinner />}
<Text maxW="200px" fontSize="xs" variant="subtext"> tooltip={<TooltipContent />}
{t('stylePresets.importTemplatesDesc')} aria-label={t('stylePresets.importTemplates')}
</Text> size="md"
<Text maxW="200px" fontSize="xs" variant="subtext"> variant="link"
{t('stylePresets.importTemplatesDesc2')} w={8}
</Text> h={8}
</Flex> sx={isLoading ? loadingStyles : undefined}
isDisabled={isLoading}
{...getRootProps()}
/>
<input {...getInputProps()} /> <input {...getInputProps()} />
</MenuItem> </>
);
};
const TooltipContent = () => {
const { t } = useTranslation();
return (
<Flex flexDir="column">
<Text pb={1} fontWeight="semibold">
{t('stylePresets.importTemplates')}
</Text>
<Text>{t('stylePresets.acceptedColumnsKeys')}</Text>
<UnorderedList>
<ListItem>{t('stylePresets.nameColumn')}</ListItem>
<ListItem>{t('stylePresets.positivePromptColumn')}</ListItem>
<ListItem>{t('stylePresets.negativePromptColumn')}</ListItem>
</UnorderedList>
</Flex>
); );
}; };

View File

@ -1,12 +1,14 @@
import { Flex, 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 { StylePresetExportButton } from 'features/stylePresets/components/StylePresetExportButton';
import { StylePresetImportButton } from 'features/stylePresets/components/StylePresetImportButton';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { StylePresetRecordWithImage } from 'services/api/endpoints/stylePresets'; import type { StylePresetRecordWithImage } from 'services/api/endpoints/stylePresets';
import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets'; import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
import { StylePresetCreateButton } from './StylePresetCreateButton';
import { StylePresetList } from './StylePresetList'; import { StylePresetList } from './StylePresetList';
import { StylePresetMenuActions } from './StylePresetMenuActions/StylePresetMenuActions';
import StylePresetSearch from './StylePresetSearch'; import StylePresetSearch from './StylePresetSearch';
export const StylePresetMenu = () => { export const StylePresetMenu = () => {
@ -50,7 +52,9 @@ export const StylePresetMenu = () => {
<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 />
<StylePresetMenuActions /> <StylePresetCreateButton />
<StylePresetImportButton />
<StylePresetExportButton />
</Flex> </Flex>
{data.presets.length === 0 && data.defaultPresets.length === 0 && ( {data.presets.length === 0 && data.defaultPresets.length === 0 && (

View File

@ -1,47 +0,0 @@
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

@ -1,50 +0,0 @@
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>
);
};