mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): add workflow loading, deleting to workflow library UI
This commit is contained in:
parent
18f3190857
commit
3d57c14bb3
@ -67,6 +67,7 @@
|
||||
"controlNet": "ControlNet",
|
||||
"controlAdapter": "Control Adapter",
|
||||
"data": "Data",
|
||||
"delete": "Delete",
|
||||
"details": "Details",
|
||||
"ipAdapter": "IP Adapter",
|
||||
"t2iAdapter": "T2I Adapter",
|
||||
@ -103,6 +104,7 @@
|
||||
"langSpanish": "Español",
|
||||
"languagePickerLabel": "Language",
|
||||
"langUkranian": "Украї́нська",
|
||||
"lastUpdated": "Last updated: {{date}}",
|
||||
"lightMode": "Light Mode",
|
||||
"linear": "Linear",
|
||||
"load": "Load",
|
||||
@ -1312,7 +1314,10 @@
|
||||
"uploadFailedInvalidUploadDesc": "Must be single PNG or JPEG image",
|
||||
"uploadFailedUnableToLoadDesc": "Unable to load file",
|
||||
"upscalingFailed": "Upscaling Failed",
|
||||
"workflowLoaded": "Workflow Loaded"
|
||||
"workflowLoaded": "Workflow Loaded",
|
||||
"problemRetrievingWorkflow": "Problem Retrieving Workflow",
|
||||
"workflowDeleted": "Workflow Deleted",
|
||||
"problemDeletingWorkflow": "Problem Deleting Workflow"
|
||||
},
|
||||
"tooltip": {
|
||||
"feature": {
|
||||
@ -1607,6 +1612,7 @@
|
||||
"userCategory": "User",
|
||||
"systemCategory": "System",
|
||||
"loadWorkflow": "$t(nodes.loadWorkflow)",
|
||||
"deleteWorkflow": "Delete Workflow"
|
||||
"deleteWorkflow": "Delete Workflow",
|
||||
"unnamedWorkflow": "Unnamed Workflow"
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { useDisclosure } from '@chakra-ui/react';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { WorkflowLibraryContext } from 'features/nodes/components/flow/WorkflowLibrary/context';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaFolderOpen } from 'react-icons/fa';
|
||||
@ -7,18 +8,18 @@ import WorkflowLibraryModal from './WorkflowLibraryModal';
|
||||
|
||||
const WorkflowLibraryButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, onClose, onOpen } = useDisclosure();
|
||||
const disclosure = useDisclosure();
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkflowLibraryContext.Provider value={disclosure}>
|
||||
<IAIIconButton
|
||||
icon={<FaFolderOpen />}
|
||||
onClick={onOpen}
|
||||
onClick={disclosure.onOpen}
|
||||
tooltip={t('workflows.workflowLibrary')}
|
||||
aria-label={t('workflows.workflowLibrary')}
|
||||
/>
|
||||
<WorkflowLibraryModal isOpen={isOpen} onClose={onClose} />
|
||||
</>
|
||||
<WorkflowLibraryModal />
|
||||
</WorkflowLibraryContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,26 +1,28 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { WorkflowCategory } from './types';
|
||||
import { Dispatch, SetStateAction, memo } from 'react';
|
||||
import { paths } from 'services/api/schema';
|
||||
import { memo, useState } from 'react';
|
||||
import { useListWorkflowsQuery } from 'services/api/endpoints/workflows';
|
||||
import WorkflowLibraryCategories from './WorkflowLibraryCategories';
|
||||
import WorkflowLibraryPagination from './WorkflowLibraryPagination';
|
||||
import WorkflowLibraryList from './WorkflowLibraryList';
|
||||
import WorkflowLibraryPagination from './WorkflowLibraryPagination';
|
||||
import { WorkflowCategory } from './types';
|
||||
|
||||
type Props = {
|
||||
data: paths['/api/v1/workflows/']['get']['responses']['200']['content']['application/json'];
|
||||
category: WorkflowCategory;
|
||||
setCategory: Dispatch<SetStateAction<WorkflowCategory>>;
|
||||
page: number;
|
||||
setPage: Dispatch<SetStateAction<number>>;
|
||||
};
|
||||
const PER_PAGE = 10;
|
||||
|
||||
const WorkflowLibraryContent = () => {
|
||||
const [page, setPage] = useState(0);
|
||||
const [category, setCategory] = useState<WorkflowCategory>('user');
|
||||
const { data } = useListWorkflowsQuery(
|
||||
{
|
||||
page,
|
||||
per_page: PER_PAGE,
|
||||
},
|
||||
{ refetchOnMountOrArgChange: true }
|
||||
);
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const WorkflowLibraryContent = ({
|
||||
data,
|
||||
category,
|
||||
setCategory,
|
||||
page,
|
||||
setPage,
|
||||
}: Props) => {
|
||||
return (
|
||||
<Flex w="full" h="full" gap={2}>
|
||||
<WorkflowLibraryCategories
|
||||
|
@ -1,40 +1,20 @@
|
||||
import { Flex, Spacer, Text } from '@chakra-ui/react';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaFolderOpen, FaTrash } from 'react-icons/fa';
|
||||
import { paths } from 'services/api/schema';
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import WorkflowLibraryWorkflowItem from 'features/nodes/components/flow/WorkflowLibrary/WorkflowLibraryWorkflowItem';
|
||||
import ScrollableContent from 'features/nodes/components/sidePanel/ScrollableContent';
|
||||
import { memo } from 'react';
|
||||
import { paths } from 'services/api/schema';
|
||||
|
||||
type Props = {
|
||||
data: paths['/api/v1/workflows/']['get']['responses']['200']['content']['application/json'];
|
||||
};
|
||||
|
||||
const WorkflowLibraryList = ({ data }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Flex w="full" h="full" layerStyle="second" p={2} borderRadius="base">
|
||||
<ScrollableContent>
|
||||
<Flex w="full" h="full" gap={2} flexDir="column">
|
||||
{data.items.map((w) => (
|
||||
<Flex key={w.workflow_id} w="full">
|
||||
<Flex w="full" alignItems="center" gap={2}>
|
||||
<Text>{w.workflow_id}</Text>
|
||||
<Spacer />
|
||||
<IAIIconButton
|
||||
icon={<FaFolderOpen />}
|
||||
aria-label={t('workflows.loadWorkflow')}
|
||||
tooltip={t('workflows.loadWorkflow')}
|
||||
/>
|
||||
<IAIIconButton
|
||||
icon={<FaTrash />}
|
||||
colorScheme="error"
|
||||
aria-label={t('workflows.deleteWorkflow')}
|
||||
tooltip={t('workflows.deleteWorkflow')}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<WorkflowLibraryWorkflowItem key={w.workflow_id} workflowDTO={w} />
|
||||
))}
|
||||
</Flex>
|
||||
</ScrollableContent>
|
||||
|
@ -1,23 +1,20 @@
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
} from '@chakra-ui/react';
|
||||
import WorkflowLibraryContent from 'features/nodes/components/flow/WorkflowLibrary/WorkflowLibraryContent';
|
||||
import { useWorkflowLibraryContext } from 'features/nodes/components/flow/WorkflowLibrary/useWorkflowLibraryContext';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import WorkflowLibraryWrapper from './WorkflowLibraryWrapper';
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const WorkflowLibraryModal = ({ isOpen, onClose }: Props) => {
|
||||
const WorkflowLibraryModal = () => {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, onClose } = useWorkflowLibraryContext();
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} isCentered>
|
||||
<ModalOverlay />
|
||||
@ -32,7 +29,7 @@ const WorkflowLibraryModal = ({ isOpen, onClose }: Props) => {
|
||||
<ModalHeader>{t('workflows.workflowLibrary')}</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<WorkflowLibraryWrapper />
|
||||
<WorkflowLibraryContent />
|
||||
</ModalBody>
|
||||
<ModalFooter />
|
||||
</ModalContent>
|
||||
|
@ -0,0 +1,69 @@
|
||||
import { Flex, Heading, Spacer, Text } from '@chakra-ui/react';
|
||||
import IAIButton from 'common/components/IAIButton';
|
||||
import dateFormat from 'dateformat';
|
||||
import { useWorkflowLibraryContext } from 'features/nodes/components/flow/WorkflowLibrary/useWorkflowLibraryContext';
|
||||
import { useDeleteLibraryWorkflow } from 'features/nodes/hooks/useDeleteLibraryWorkflow';
|
||||
import { useGetAndLoadLibraryWorkflow } from 'features/nodes/hooks/useGetAndLoadLibraryWorkflow';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { paths } from 'services/api/schema';
|
||||
|
||||
type Props = {
|
||||
workflowDTO: paths['/api/v1/workflows/']['get']['responses']['200']['content']['application/json']['items'][number];
|
||||
};
|
||||
|
||||
const WorkflowLibraryList = ({ workflowDTO }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { onClose } = useWorkflowLibraryContext();
|
||||
const { deleteWorkflow, deleteWorkflowResult } = useDeleteLibraryWorkflow({});
|
||||
const { getAndLoadWorkflow, getAndLoadWorkflowResult } =
|
||||
useGetAndLoadLibraryWorkflow({ onSuccess: onClose });
|
||||
|
||||
const handleDeleteWorkflow = useCallback(() => {
|
||||
deleteWorkflow(workflowDTO.workflow_id);
|
||||
}, [deleteWorkflow, workflowDTO.workflow_id]);
|
||||
|
||||
const handleGetAndLoadWorkflow = useCallback(() => {
|
||||
getAndLoadWorkflow(workflowDTO.workflow_id);
|
||||
}, [getAndLoadWorkflow, workflowDTO.workflow_id]);
|
||||
|
||||
return (
|
||||
<Flex key={workflowDTO.workflow_id} w="full">
|
||||
<Flex w="full" alignItems="center" gap={2}>
|
||||
<Flex flexDir="column" flexGrow={1}>
|
||||
<Flex alignItems="center" w="full">
|
||||
<Heading size="sm">
|
||||
{workflowDTO.name || t('workflows.unnamedWorkflow')}
|
||||
</Heading>
|
||||
<Spacer />
|
||||
<Text fontSize="sm" fontStyle="italic" variant="subtext">
|
||||
{t('common.lastUpdated', {
|
||||
date: dateFormat(workflowDTO.updated_at),
|
||||
})}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Text fontSize="sm" noOfLines={1}>
|
||||
{workflowDTO.description}
|
||||
</Text>
|
||||
</Flex>
|
||||
<IAIButton
|
||||
onClick={handleGetAndLoadWorkflow}
|
||||
isLoading={getAndLoadWorkflowResult.isLoading}
|
||||
aria-label={t('workflows.loadWorkflow')}
|
||||
>
|
||||
{t('common.load')}
|
||||
</IAIButton>
|
||||
<IAIButton
|
||||
colorScheme="error"
|
||||
onClick={handleDeleteWorkflow}
|
||||
isLoading={deleteWorkflowResult.isLoading}
|
||||
aria-label={t('workflows.deleteWorkflow')}
|
||||
>
|
||||
{t('common.delete')}
|
||||
</IAIButton>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(WorkflowLibraryList);
|
@ -1,31 +0,0 @@
|
||||
import { memo, useState } from 'react';
|
||||
import { useListWorkflowsQuery } from 'services/api/endpoints/workflows';
|
||||
import WorkflowLibraryContent from './WorkflowLibraryContent';
|
||||
import { WorkflowCategory } from './types';
|
||||
|
||||
const PER_PAGE = 10;
|
||||
|
||||
const WorkflowLibraryWrapper = () => {
|
||||
const [page, setPage] = useState(0);
|
||||
const [category, setCategory] = useState<WorkflowCategory>('user');
|
||||
const { data } = useListWorkflowsQuery({
|
||||
page,
|
||||
per_page: PER_PAGE,
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<WorkflowLibraryContent
|
||||
data={data}
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
category={category}
|
||||
setCategory={setCategory}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(WorkflowLibraryWrapper);
|
@ -0,0 +1,6 @@
|
||||
import { UseDisclosureReturn } from '@chakra-ui/react';
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const WorkflowLibraryContext = createContext<UseDisclosureReturn | null>(
|
||||
null
|
||||
);
|
@ -0,0 +1,12 @@
|
||||
import { WorkflowLibraryContext } from 'features/nodes/components/flow/WorkflowLibrary/context';
|
||||
import { useContext } from 'react';
|
||||
|
||||
export const useWorkflowLibraryContext = () => {
|
||||
const context = useContext(WorkflowLibraryContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useWorkflowLibraryContext must be used within a WorkflowLibraryContext.Provider'
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
@ -1,9 +1,8 @@
|
||||
import { Flex } from '@chakra-ui/layout';
|
||||
import { memo } from 'react';
|
||||
import DownloadWorkflowButton from './DownloadWorkflowButton';
|
||||
import LoadWorkflowButton from './LoadWorkflowButton';
|
||||
import ResetWorkflowButton from './ResetWorkflowButton';
|
||||
import DownloadWorkflowButton from './DownloadWorkflowButton';
|
||||
import WorkflowLibraryButton from 'features/nodes/components/flow/WorkflowLibrary/WorkflowLibraryButton';
|
||||
|
||||
const TopCenterPanel = () => {
|
||||
return (
|
||||
@ -19,7 +18,6 @@ const TopCenterPanel = () => {
|
||||
<DownloadWorkflowButton />
|
||||
<LoadWorkflowButton />
|
||||
<ResetWorkflowButton />
|
||||
<WorkflowLibraryButton />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { Flex } from '@chakra-ui/layout';
|
||||
import { memo } from 'react';
|
||||
import WorkflowEditorSettings from './WorkflowEditorSettings';
|
||||
import WorkflowLibraryButton from 'features/nodes/components/flow/WorkflowLibrary/WorkflowLibraryButton';
|
||||
|
||||
const TopRightPanel = () => {
|
||||
return (
|
||||
<Flex sx={{ gap: 2, position: 'absolute', top: 2, insetInlineEnd: 2 }}>
|
||||
<WorkflowEditorSettings />
|
||||
<WorkflowLibraryButton />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,39 @@
|
||||
import { useAppToaster } from 'app/components/Toaster';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDeleteWorkflowMutation } from 'services/api/endpoints/workflows';
|
||||
|
||||
type UseDeleteLibraryWorkflowArg = {
|
||||
onSuccess?: () => void;
|
||||
onError?: () => void;
|
||||
};
|
||||
|
||||
export const useDeleteLibraryWorkflow = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
}: UseDeleteLibraryWorkflowArg) => {
|
||||
const toaster = useAppToaster();
|
||||
const { t } = useTranslation();
|
||||
const [_deleteWorkflow, deleteWorkflowResult] = useDeleteWorkflowMutation();
|
||||
|
||||
const deleteWorkflow = useCallback(
|
||||
async (workflow_id: string) => {
|
||||
try {
|
||||
await _deleteWorkflow(workflow_id).unwrap();
|
||||
toaster({
|
||||
title: t('toast.workflowDeleted'),
|
||||
});
|
||||
onSuccess && onSuccess();
|
||||
} catch {
|
||||
toaster({
|
||||
title: t('toast.problemDeletingWorkflow'),
|
||||
status: 'error',
|
||||
});
|
||||
onError && onError();
|
||||
}
|
||||
},
|
||||
[_deleteWorkflow, toaster, t, onSuccess, onError]
|
||||
);
|
||||
|
||||
return { deleteWorkflow, deleteWorkflowResult };
|
||||
};
|
@ -0,0 +1,21 @@
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { workflowLoadRequested } from 'features/nodes/store/actions';
|
||||
import { useCallback } from 'react';
|
||||
import { useLazyGetImageWorkflowQuery } from 'services/api/endpoints/images';
|
||||
|
||||
export const useGetAndLoadEmbeddedWorkflow = (
|
||||
image_name: string | undefined
|
||||
) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const [_trigger, result] = useLazyGetImageWorkflowQuery();
|
||||
const trigger = useCallback(() => {
|
||||
if (!image_name) {
|
||||
return;
|
||||
}
|
||||
_trigger(image_name).then((workflow) => {
|
||||
dispatch(workflowLoadRequested(workflow.data));
|
||||
});
|
||||
}, [dispatch, _trigger, image_name]);
|
||||
|
||||
return [trigger, result];
|
||||
};
|
@ -0,0 +1,41 @@
|
||||
import { useAppToaster } from 'app/components/Toaster';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { workflowLoadRequested } from 'features/nodes/store/actions';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLazyGetWorkflowQuery } from 'services/api/endpoints/workflows';
|
||||
|
||||
type UseGetAndLoadLibraryWorkflowArg = {
|
||||
onSuccess?: () => void;
|
||||
onError?: () => void;
|
||||
};
|
||||
|
||||
export const useGetAndLoadLibraryWorkflow = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
}: UseGetAndLoadLibraryWorkflowArg) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const toaster = useAppToaster();
|
||||
const { t } = useTranslation();
|
||||
const [_getAndLoadWorkflow, getAndLoadWorkflowResult] =
|
||||
useLazyGetWorkflowQuery();
|
||||
const getAndLoadWorkflow = useCallback(
|
||||
async (workflow_id: string) => {
|
||||
try {
|
||||
const data = await _getAndLoadWorkflow(workflow_id).unwrap();
|
||||
dispatch(workflowLoadRequested(data.workflow));
|
||||
// No toast - the listener for this action does that after the workflow is loaded
|
||||
onSuccess && onSuccess();
|
||||
} catch {
|
||||
toaster({
|
||||
title: t('toast.problemRetrievingWorkflow'),
|
||||
status: 'error',
|
||||
});
|
||||
onError && onError();
|
||||
}
|
||||
},
|
||||
[_getAndLoadWorkflow, dispatch, onSuccess, toaster, t, onError]
|
||||
);
|
||||
|
||||
return { getAndLoadWorkflow, getAndLoadWorkflowResult };
|
||||
};
|
@ -62,7 +62,7 @@ export const workflowsApi = api.injectEndpoints({
|
||||
});
|
||||
|
||||
export const {
|
||||
useGetWorkflowQuery,
|
||||
useLazyGetWorkflowQuery,
|
||||
useCreateWorkflowMutation,
|
||||
useDeleteWorkflowMutation,
|
||||
useUpdateWorkflowMutation,
|
||||
|
Loading…
Reference in New Issue
Block a user