mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-02-14 02:55:11 -05:00
refactor workflow thumbnails to be separate flow/endpoints
This commit is contained in:
committed by
psychedelicious
parent
d4423aa16f
commit
ab4433da2f
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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"
|
||||
@@ -109,10 +109,6 @@ export const workflowSlice = createSlice({
|
||||
state.name = action.payload;
|
||||
state.isTouched = true;
|
||||
},
|
||||
workflowThumbnailChanged: (state, action: PayloadAction<string | null>) => {
|
||||
state.thumbnail = action.payload;
|
||||
state.isTouched = true;
|
||||
},
|
||||
workflowCategoryChanged: (state, action: PayloadAction<WorkflowCategory | undefined>) => {
|
||||
if (action.payload) {
|
||||
state.meta.category = action.payload;
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
import type { ToastId } from '@invoke-ai/ui-library';
|
||||
import { useToast } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { convertImageUrlToBlob } from 'common/util/convertImageUrlToBlob';
|
||||
import { $builtWorkflow } from 'features/nodes/hooks/useWorkflowWatcher';
|
||||
import {
|
||||
formFieldInitialValuesChanged,
|
||||
selectWorkflowThumbnail,
|
||||
workflowIDChanged,
|
||||
workflowSaved,
|
||||
} from 'features/nodes/store/workflowSlice';
|
||||
import { formFieldInitialValuesChanged, workflowIDChanged, workflowSaved } from 'features/nodes/store/workflowSlice';
|
||||
import type { WorkflowV3 } from 'features/nodes/types/workflow';
|
||||
import { useGetFormFieldInitialValues } from 'features/workflowLibrary/hooks/useGetFormInitialValues';
|
||||
import { workflowUpdated } from 'features/workflowLibrary/store/actions';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useCreateWorkflowMutation, useUpdateWorkflowMutation, workflowsApi } from 'services/api/endpoints/workflows';
|
||||
import type { SetRequired } from 'type-fest';
|
||||
|
||||
@@ -33,7 +26,6 @@ export const useSaveLibraryWorkflow: UseSaveLibraryWorkflow = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const getFormFieldInitialValues = useGetFormFieldInitialValues();
|
||||
const thumbnail = useSelector(selectWorkflowThumbnail);
|
||||
const [updateWorkflow, updateWorkflowResult] = useUpdateWorkflowMutation();
|
||||
const [createWorkflow, createWorkflowResult] = useCreateWorkflowMutation();
|
||||
const toast = useToast();
|
||||
@@ -50,13 +42,11 @@ export const useSaveLibraryWorkflow: UseSaveLibraryWorkflow = () => {
|
||||
isClosable: false,
|
||||
});
|
||||
try {
|
||||
const blob = thumbnail ? await convertImageUrlToBlob(thumbnail) : null;
|
||||
const image = blob ? new File([blob], 'thumbnail.png', { type: 'image/png' }) : null;
|
||||
if (isWorkflowWithID(workflow)) {
|
||||
await updateWorkflow({ workflow, image }).unwrap();
|
||||
await updateWorkflow(workflow).unwrap();
|
||||
dispatch(workflowUpdated());
|
||||
} else {
|
||||
const data = await createWorkflow({ workflow, image }).unwrap();
|
||||
const data = await createWorkflow(workflow).unwrap();
|
||||
dispatch(workflowIDChanged(data.workflow.id));
|
||||
}
|
||||
dispatch(workflowSaved());
|
||||
@@ -83,7 +73,7 @@ export const useSaveLibraryWorkflow: UseSaveLibraryWorkflow = () => {
|
||||
toast.close(toastRef.current);
|
||||
}
|
||||
}
|
||||
}, [toast, t, dispatch, getFormFieldInitialValues, updateWorkflow, createWorkflow, thumbnail]);
|
||||
}, [toast, t, dispatch, getFormFieldInitialValues, updateWorkflow, createWorkflow]);
|
||||
return {
|
||||
saveWorkflow,
|
||||
isLoading: updateWorkflowResult.isLoading || createWorkflowResult.isLoading,
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import type { ToastId } from '@invoke-ai/ui-library';
|
||||
import { useToast } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { convertImageUrlToBlob } from 'common/util/convertImageUrlToBlob';
|
||||
import { $builtWorkflow } from 'features/nodes/hooks/useWorkflowWatcher';
|
||||
import {
|
||||
formFieldInitialValuesChanged,
|
||||
selectWorkflowThumbnail,
|
||||
workflowCategoryChanged,
|
||||
workflowIDChanged,
|
||||
workflowNameChanged,
|
||||
@@ -16,7 +14,6 @@ import { useGetFormFieldInitialValues } from 'features/workflowLibrary/hooks/use
|
||||
import { newWorkflowSaved } from 'features/workflowLibrary/store/actions';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useCreateWorkflowMutation, workflowsApi } from 'services/api/endpoints/workflows';
|
||||
|
||||
type SaveWorkflowAsArg = {
|
||||
@@ -40,7 +37,6 @@ export const useSaveWorkflowAs: UseSaveWorkflowAs = () => {
|
||||
const [createWorkflow, createWorkflowResult] = useCreateWorkflowMutation();
|
||||
const getFormFieldInitialValues = useGetFormFieldInitialValues();
|
||||
|
||||
const thumbnail = useSelector(selectWorkflowThumbnail);
|
||||
const toast = useToast();
|
||||
const toastRef = useRef<ToastId | undefined>();
|
||||
const saveWorkflowAs = useCallback(
|
||||
@@ -59,10 +55,8 @@ export const useSaveWorkflowAs: UseSaveWorkflowAs = () => {
|
||||
workflow.id = undefined;
|
||||
workflow.name = newName;
|
||||
workflow.meta.category = category;
|
||||
const blob = thumbnail ? await convertImageUrlToBlob(thumbnail) : null;
|
||||
const image = blob ? new File([blob], 'thumbnail.png', { type: 'image/png' }) : null;
|
||||
|
||||
const data = await createWorkflow({ workflow, image }).unwrap();
|
||||
const data = await createWorkflow(workflow).unwrap();
|
||||
dispatch(workflowIDChanged(data.workflow.id));
|
||||
dispatch(workflowNameChanged(data.workflow.name));
|
||||
dispatch(workflowCategoryChanged(data.workflow.meta.category));
|
||||
@@ -92,7 +86,7 @@ export const useSaveWorkflowAs: UseSaveWorkflowAs = () => {
|
||||
}
|
||||
}
|
||||
},
|
||||
[toast, t, createWorkflow, dispatch, getFormFieldInitialValues, thumbnail]
|
||||
[toast, t, createWorkflow, dispatch, getFormFieldInitialValues]
|
||||
);
|
||||
return {
|
||||
saveWorkflowAs,
|
||||
|
||||
Reference in New Issue
Block a user