feat(ui): add workflow loading, deleting to workflow library UI

This commit is contained in:
psychedelicious 2023-11-29 23:39:10 +11:00
parent 18f3190857
commit 3d57c14bb3
15 changed files with 240 additions and 97 deletions

View File

@ -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"
}
}

View File

@ -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>
);
};

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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);

View File

@ -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);

View File

@ -0,0 +1,6 @@
import { UseDisclosureReturn } from '@chakra-ui/react';
import { createContext } from 'react';
export const WorkflowLibraryContext = createContext<UseDisclosureReturn | null>(
null
);

View File

@ -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;
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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 };
};

View File

@ -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];
};

View File

@ -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 };
};

View File

@ -62,7 +62,7 @@ export const workflowsApi = api.injectEndpoints({
});
export const {
useGetWorkflowQuery,
useLazyGetWorkflowQuery,
useCreateWorkflowMutation,
useDeleteWorkflowMutation,
useUpdateWorkflowMutation,