refactor workflow thumbnails to be separate flow/endpoints

This commit is contained in:
Mary Hipp
2025-02-24 16:12:43 -05:00
committed by psychedelicious
parent d4423aa16f
commit ab4433da2f
14 changed files with 323 additions and 272 deletions

View File

@@ -1,5 +1,6 @@
import type { FormControlProps } from '@invoke-ai/ui-library';
import { Flex, FormControl, FormControlGroup, FormLabel, Input, Textarea } from '@invoke-ai/ui-library';
import { skipToken } from '@reduxjs/toolkit/query';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
@@ -11,19 +12,20 @@ import {
workflowNameChanged,
workflowNotesChanged,
workflowTagsChanged,
workflowThumbnailChanged,
workflowVersionChanged,
} from 'features/nodes/store/workflowSlice';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useGetWorkflowQuery } from 'services/api/endpoints/workflows';
import { WorkflowThumbnailField } from './WorkflowThumbnailField';
import { WorkflowThumbnailEditor } from './WorkflowThumbnail/WorkflowThumbnailEditor';
const selector = createMemoizedSelector(selectWorkflowSlice, (workflow) => {
const { author, name, description, tags, version, contact, notes, thumbnail } = workflow;
const { id, author, name, description, tags, version, contact, notes } = workflow;
return {
id,
name,
author,
description,
@@ -31,14 +33,15 @@ const selector = createMemoizedSelector(selectWorkflowSlice, (workflow) => {
version,
contact,
notes,
thumbnail,
};
});
const WorkflowGeneralTab = () => {
const { author, name, description, tags, version, contact, notes, thumbnail } = useAppSelector(selector);
const { id, author, name, description, tags, version, contact, notes } = useAppSelector(selector);
const dispatch = useAppDispatch();
const { data } = useGetWorkflowQuery(id ?? skipToken);
const handleChangeName = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
dispatch(workflowNameChanged(e.target.value));
@@ -83,13 +86,6 @@ const WorkflowGeneralTab = () => {
[dispatch]
);
const handleChangeThumbnail = useCallback(
(localImageUrl: string | null) => {
dispatch(workflowThumbnailChanged(localImageUrl));
},
[dispatch]
);
const { t } = useTranslation();
return (
@@ -100,15 +96,14 @@ const WorkflowGeneralTab = () => {
<FormLabel>{t('nodes.workflowName')}</FormLabel>
<Input variant="darkFilled" value={name} onChange={handleChangeName} />
</FormControl>
<FormControl>
<FormLabel>{t('workflows.workflowThumbnail')}</FormLabel>
<WorkflowThumbnailEditor thumbnailUrl={data?.thumbnail_url || null} workflowId={id} />
</FormControl>
<FormControl>
<FormLabel>{t('nodes.workflowVersion')}</FormLabel>
<Input variant="darkFilled" value={version} onChange={handleChangeVersion} />
</FormControl>
<FormControl>
<FormLabel>Thumbnail</FormLabel>
<WorkflowThumbnailField imageUrl={thumbnail} onChange={handleChangeThumbnail} />
</FormControl>
<FormControl>
<FormLabel>{t('nodes.workflowAuthor')}</FormLabel>
<Input variant="darkFilled" value={author} onChange={handleChangeAuthor} />

View File

@@ -0,0 +1,63 @@
import { Button, Flex } from '@invoke-ai/ui-library';
import { convertImageUrlToBlob } from 'common/util/convertImageUrlToBlob';
import { toast } from 'features/toast/toast';
import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDeleteWorkflowThumbnailMutation, useSetWorkflowThumbnailMutation } from 'services/api/endpoints/workflows';
import { WorkflowThumbnailField } from './WorkflowThumbnailField';
export const WorkflowThumbnailEditor = ({
workflowId,
thumbnailUrl,
}: {
workflowId?: string;
thumbnailUrl: string | null;
}) => {
const { t } = useTranslation();
const [localThumbnailUrl, setLocalThumbnailUrl] = useState<string | null>(null);
const [canSaveChanges, setCanSaveChanges] = useState(false);
const [setThumbnail, { isLoading }] = useSetWorkflowThumbnailMutation();
const [deleteThumbnail, { isLoading: isDeleting }] = useDeleteWorkflowThumbnailMutation();
const handleLocalThumbnailUrlChange = useCallback((url: string | null) => {
setLocalThumbnailUrl(url);
setCanSaveChanges(true);
}, []);
const handleSaveChanges = useCallback(async () => {
if (!workflowId) {
return;
}
try {
if (localThumbnailUrl) {
const blob = await convertImageUrlToBlob(localThumbnailUrl);
if (!blob) {
return;
}
const file = new File([blob], 'workflow_thumbnail.png', { type: 'image/png' });
await setThumbnail({ workflow_id: workflowId, image: file }).unwrap();
} else {
await deleteThumbnail(workflowId).unwrap();
}
setCanSaveChanges(false);
toast({ status: 'success', title: 'Workflow thumbnail updated' });
} catch (error) {
toast({ status: 'error', title: 'Failed to update thumbnail' });
}
}, [deleteThumbnail, setThumbnail, workflowId, localThumbnailUrl]);
return (
<Flex alignItems="center" gap={4}>
<WorkflowThumbnailField imageUrl={thumbnailUrl} onChange={handleLocalThumbnailUrlChange} />
<Button size="sm" isLoading={isLoading || isDeleting} onClick={handleSaveChanges} isDisabled={!canSaveChanges}>
{t('common.saveChanges')}
</Button>
</Flex>
);
};

View File

@@ -10,7 +10,7 @@ export const WorkflowThumbnailField = ({
onChange,
}: {
imageUrl: string | null;
onChange: (localImageUrl: string | null) => void;
onChange: (localThumbnailUrl: string | null) => void;
}) => {
const [thumbnail, setThumbnail] = useState<File | null>(null);
@@ -48,7 +48,8 @@ export const WorkflowThumbnailField = ({
const handleResetImage = useCallback(() => {
setThumbnail(null);
}, []);
onChange(null);
}, [onChange]);
const { getInputProps, getRootProps } = useDropzone({
accept: { 'image/png': ['.png'], 'image/jpeg': ['.jpg', '.jpeg', '.png'] },
@@ -64,9 +65,8 @@ export const WorkflowThumbnailField = ({
src={URL.createObjectURL(thumbnail)}
objectFit="cover"
objectPosition="50% 50%"
w={65}
h={65}
minWidth={65}
w={100}
h={100}
borderRadius="base"
/>
<IconButton
@@ -89,8 +89,8 @@ export const WorkflowThumbnailField = ({
<Tooltip label={t('stylePresets.uploadImage')}>
<Flex
as={Button}
w={65}
h={65}
w={100}
h={100}
opacity={0.3}
borderRadius="base"
alignItems="center"