mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): updated workflow handling (WIP)
Clientside updates for the backend workflow changes. Includes roughed-out workflow library UI.
This commit is contained in:
parent
a514c9e28b
commit
0b079df4ae
@ -161,7 +161,9 @@
|
||||
"txt2img": "Text To Image",
|
||||
"unifiedCanvas": "Unified Canvas",
|
||||
"unknown": "Unknown",
|
||||
"upload": "Upload"
|
||||
"upload": "Upload",
|
||||
"prevPage": "Previous Page",
|
||||
"nextPage": "Next Page"
|
||||
},
|
||||
"controlnet": {
|
||||
"controlAdapter_one": "Control Adapter",
|
||||
@ -1599,5 +1601,12 @@
|
||||
"showIntermediates": "Show Intermediates",
|
||||
"snapToGrid": "Snap to Grid",
|
||||
"undo": "Undo"
|
||||
},
|
||||
"workflows": {
|
||||
"workflowLibrary": "Workflow Library",
|
||||
"userCategory": "User",
|
||||
"systemCategory": "System",
|
||||
"loadWorkflow": "$t(nodes.loadWorkflow)",
|
||||
"deleteWorkflow": "Delete Workflow"
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { buildNodesGraph } from 'features/nodes/util/graph/buildNodesGraph';
|
||||
import { queueApi } from 'services/api/endpoints/queue';
|
||||
import { BatchConfig } from 'services/api/types';
|
||||
import { startAppListening } from '..';
|
||||
import { buildWorkflow } from 'features/nodes/util/workflow/buildWorkflow';
|
||||
|
||||
export const addEnqueueRequestedNodes = () => {
|
||||
startAppListening({
|
||||
@ -11,9 +12,11 @@ export const addEnqueueRequestedNodes = () => {
|
||||
effect: async (action, { getState, dispatch }) => {
|
||||
const state = getState();
|
||||
const graph = buildNodesGraph(state.nodes);
|
||||
const workflow = buildWorkflow(state.nodes);
|
||||
const batchConfig: BatchConfig = {
|
||||
batch: {
|
||||
graph,
|
||||
workflow,
|
||||
runs: state.generation.iterations,
|
||||
},
|
||||
prepend: action.payload.prepend,
|
||||
|
@ -99,7 +99,6 @@ export const addWorkflowLoadRequestedListener = () => {
|
||||
);
|
||||
} else {
|
||||
// Some other error occurred
|
||||
console.log(e);
|
||||
log.error(
|
||||
{ error: parseify(e) },
|
||||
t('nodes.unknownErrorValidatingWorkflow')
|
||||
|
@ -16,6 +16,8 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton';
|
||||
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
||||
import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMenu/SingleSelectionMenuItems';
|
||||
import { sentImageToImg2Img } from 'features/gallery/store/actions';
|
||||
import { workflowLoadRequested } from 'features/nodes/store/actions';
|
||||
import ParamUpscalePopover from 'features/parameters/components/Parameters/Upscale/ParamUpscaleSettings';
|
||||
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
||||
@ -39,12 +41,12 @@ import {
|
||||
FaSeedling,
|
||||
} from 'react-icons/fa';
|
||||
import { FaCircleNodes, FaEllipsis } from 'react-icons/fa6';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
import {
|
||||
useGetImageDTOQuery,
|
||||
useLazyGetImageWorkflowQuery,
|
||||
} from 'services/api/endpoints/images';
|
||||
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
|
||||
import { useDebouncedWorkflow } from 'services/api/hooks/useDebouncedWorkflow';
|
||||
import { menuListMotionProps } from 'theme/components/menu';
|
||||
import { sentImageToImg2Img } from 'features/gallery/store/actions';
|
||||
import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMenu/SingleSelectionMenuItems';
|
||||
|
||||
const currentImageButtonsSelector = createSelector(
|
||||
[stateSelector, activeTabNameSelector],
|
||||
@ -111,18 +113,17 @@ const CurrentImageButtons = () => {
|
||||
lastSelectedImage?.image_name
|
||||
);
|
||||
|
||||
const { workflow, isLoading: isLoadingWorkflow } = useDebouncedWorkflow(
|
||||
lastSelectedImage?.workflow_id
|
||||
);
|
||||
|
||||
const [getWorkflow, getWorkflowResult] = useLazyGetImageWorkflowQuery();
|
||||
const handleLoadWorkflow = useCallback(() => {
|
||||
if (!workflow) {
|
||||
if (!lastSelectedImage) {
|
||||
return;
|
||||
}
|
||||
dispatch(workflowLoadRequested(workflow));
|
||||
}, [dispatch, workflow]);
|
||||
getWorkflow(lastSelectedImage?.image_name).then((workflow) => {
|
||||
dispatch(workflowLoadRequested(workflow.data));
|
||||
});
|
||||
}, [dispatch, getWorkflow, lastSelectedImage]);
|
||||
|
||||
useHotkeys('w', handleLoadWorkflow, [workflow]);
|
||||
useHotkeys('w', handleLoadWorkflow, [lastSelectedImage]);
|
||||
|
||||
const handleClickUseAllParameters = useCallback(() => {
|
||||
recallAllParameters(metadata);
|
||||
@ -255,12 +256,12 @@ const CurrentImageButtons = () => {
|
||||
|
||||
<ButtonGroup isAttached={true} isDisabled={shouldDisableToolbarButtons}>
|
||||
<IAIIconButton
|
||||
isLoading={isLoadingWorkflow}
|
||||
icon={<FaCircleNodes />}
|
||||
tooltip={`${t('nodes.loadWorkflow')} (W)`}
|
||||
aria-label={`${t('nodes.loadWorkflow')} (W)`}
|
||||
isDisabled={!workflow}
|
||||
isDisabled={!imageDTO?.has_workflow}
|
||||
onClick={handleLoadWorkflow}
|
||||
isLoading={getWorkflowResult.isLoading}
|
||||
/>
|
||||
<IAIIconButton
|
||||
isLoading={isLoadingMetadata}
|
||||
|
@ -3,17 +3,21 @@ import { useStore } from '@nanostores/react';
|
||||
import { useAppToaster } from 'app/components/Toaster';
|
||||
import { $customStarUI } from 'app/store/nanostores/customStarUI';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useCopyImageToClipboard } from 'common/hooks/useCopyImageToClipboard';
|
||||
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
||||
import {
|
||||
imagesToChangeSelected,
|
||||
isModalOpenChanged,
|
||||
} from 'features/changeBoardModal/store/slice';
|
||||
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
||||
import {
|
||||
sentImageToCanvas,
|
||||
sentImageToImg2Img,
|
||||
} from 'features/gallery/store/actions';
|
||||
import { workflowLoadRequested } from 'features/nodes/store/actions';
|
||||
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
||||
import { initialImageSelected } from 'features/parameters/store/actions';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { useCopyImageToClipboard } from 'common/hooks/useCopyImageToClipboard';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
@ -32,16 +36,12 @@ import {
|
||||
import { FaCircleNodes } from 'react-icons/fa6';
|
||||
import { MdStar, MdStarBorder } from 'react-icons/md';
|
||||
import {
|
||||
useLazyGetImageWorkflowQuery,
|
||||
useStarImagesMutation,
|
||||
useUnstarImagesMutation,
|
||||
} from 'services/api/endpoints/images';
|
||||
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
|
||||
import { useDebouncedWorkflow } from 'services/api/hooks/useDebouncedWorkflow';
|
||||
import { ImageDTO } from 'services/api/types';
|
||||
import {
|
||||
sentImageToCanvas,
|
||||
sentImageToImg2Img,
|
||||
} from 'features/gallery/store/actions';
|
||||
|
||||
type SingleSelectionMenuItemsProps = {
|
||||
imageDTO: ImageDTO;
|
||||
@ -61,9 +61,13 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
||||
const { metadata, isLoading: isLoadingMetadata } = useDebouncedMetadata(
|
||||
imageDTO?.image_name
|
||||
);
|
||||
const { workflow, isLoading: isLoadingWorkflow } = useDebouncedWorkflow(
|
||||
imageDTO?.workflow_id
|
||||
);
|
||||
|
||||
const [getWorkflow, getWorkflowResult] = useLazyGetImageWorkflowQuery();
|
||||
const handleLoadWorkflow = useCallback(() => {
|
||||
getWorkflow(imageDTO.image_name).then((workflow) => {
|
||||
dispatch(workflowLoadRequested(workflow.data));
|
||||
});
|
||||
}, [dispatch, getWorkflow, imageDTO]);
|
||||
|
||||
const [starImages] = useStarImagesMutation();
|
||||
const [unstarImages] = useUnstarImagesMutation();
|
||||
@ -101,13 +105,6 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
||||
recallSeed(metadata?.seed);
|
||||
}, [metadata?.seed, recallSeed]);
|
||||
|
||||
const handleLoadWorkflow = useCallback(() => {
|
||||
if (!workflow) {
|
||||
return;
|
||||
}
|
||||
dispatch(workflowLoadRequested(workflow));
|
||||
}, [dispatch, workflow]);
|
||||
|
||||
const handleSendToImageToImage = useCallback(() => {
|
||||
dispatch(sentImageToImg2Img());
|
||||
dispatch(initialImageSelected(imageDTO));
|
||||
@ -179,9 +176,9 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
||||
{t('parameters.downloadImage')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={isLoadingWorkflow ? <SpinnerIcon /> : <FaCircleNodes />}
|
||||
icon={getWorkflowResult.isLoading ? <SpinnerIcon /> : <FaCircleNodes />}
|
||||
onClickCapture={handleLoadWorkflow}
|
||||
isDisabled={isLoadingWorkflow || !workflow}
|
||||
isDisabled={!imageDTO.has_workflow}
|
||||
>
|
||||
{t('nodes.loadWorkflow')}
|
||||
</MenuItem>
|
||||
|
@ -14,10 +14,10 @@ import ScrollableContent from 'features/nodes/components/sidePanel/ScrollableCon
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
|
||||
import { useDebouncedWorkflow } from 'services/api/hooks/useDebouncedWorkflow';
|
||||
import { ImageDTO } from 'services/api/types';
|
||||
import DataViewer from './DataViewer';
|
||||
import ImageMetadataActions from './ImageMetadataActions';
|
||||
import ImageMetadataWorkflowTabContent from './ImageMetadataWorkflowTabContent';
|
||||
|
||||
type ImageMetadataViewerProps = {
|
||||
image: ImageDTO;
|
||||
@ -32,7 +32,6 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { metadata } = useDebouncedMetadata(image.image_name);
|
||||
const { workflow } = useDebouncedWorkflow(image.workflow_id);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@ -67,9 +66,9 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
|
||||
>
|
||||
<TabList>
|
||||
<Tab>{t('metadata.recallParameters')}</Tab>
|
||||
<Tab>{t('metadata.metadata')}</Tab>
|
||||
<Tab isDisabled={!metadata}>{t('metadata.metadata')}</Tab>
|
||||
<Tab>{t('metadata.imageDetails')}</Tab>
|
||||
<Tab>{t('metadata.workflow')}</Tab>
|
||||
<Tab isDisabled={!image.has_workflow}>{t('metadata.workflow')}</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
@ -97,11 +96,7 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
|
||||
)}
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
{workflow ? (
|
||||
<DataViewer data={workflow} label={t('metadata.workflow')} />
|
||||
) : (
|
||||
<IAINoContentFallback label={t('nodes.noWorkflow')} />
|
||||
)}
|
||||
<ImageMetadataWorkflowTabContent image={image} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
|
@ -0,0 +1,23 @@
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useGetImageWorkflowQuery } from 'services/api/endpoints/images';
|
||||
import { ImageDTO } from 'services/api/types';
|
||||
import DataViewer from './DataViewer';
|
||||
|
||||
type Props = {
|
||||
image: ImageDTO;
|
||||
};
|
||||
|
||||
const ImageMetadataWorkflowTabContent = ({ image }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { currentData: workflow } = useGetImageWorkflowQuery(image.image_name);
|
||||
|
||||
if (!workflow) {
|
||||
return <IAINoContentFallback label={t('nodes.noWorkflow')} />;
|
||||
}
|
||||
|
||||
return <DataViewer data={workflow} label={t('metadata.workflow')} />;
|
||||
};
|
||||
|
||||
export default memo(ImageMetadataWorkflowTabContent);
|
@ -0,0 +1,25 @@
|
||||
import { useDisclosure } from '@chakra-ui/react';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaFolderOpen } from 'react-icons/fa';
|
||||
import WorkflowLibraryModal from './WorkflowLibraryModal';
|
||||
|
||||
const WorkflowLibraryButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, onClose, onOpen } = useDisclosure();
|
||||
|
||||
return (
|
||||
<>
|
||||
<IAIIconButton
|
||||
icon={<FaFolderOpen />}
|
||||
onClick={onOpen}
|
||||
tooltip={t('workflows.workflowLibrary')}
|
||||
aria-label={t('workflows.workflowLibrary')}
|
||||
/>
|
||||
<WorkflowLibraryModal isOpen={isOpen} onClose={onClose} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(WorkflowLibraryButton);
|
@ -0,0 +1,40 @@
|
||||
import { ButtonGroup, Flex } from '@chakra-ui/react';
|
||||
import IAIButton from 'common/components/IAIButton';
|
||||
import { WorkflowCategory } from './types';
|
||||
import { Dispatch, SetStateAction, memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type Props = {
|
||||
category: WorkflowCategory;
|
||||
setCategory: Dispatch<SetStateAction<WorkflowCategory>>;
|
||||
};
|
||||
|
||||
const WorkflowLibraryCategories = ({ category, setCategory }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const handleClickUser = useCallback(() => {
|
||||
setCategory('user');
|
||||
}, [setCategory]);
|
||||
const handleClickSystem = useCallback(() => {
|
||||
setCategory('system');
|
||||
}, [setCategory]);
|
||||
return (
|
||||
<Flex layerStyle="second" p={2} borderRadius="base">
|
||||
<ButtonGroup orientation="vertical">
|
||||
<IAIButton
|
||||
onClick={handleClickUser}
|
||||
variant={category === 'user' ? 'invokeAI' : 'ghost'}
|
||||
>
|
||||
{t('workflows.userCategory')}
|
||||
</IAIButton>
|
||||
<IAIButton
|
||||
onClick={handleClickSystem}
|
||||
variant={category === 'system' ? 'invokeAI' : 'ghost'}
|
||||
>
|
||||
{t('workflows.systemCategory')}
|
||||
</IAIButton>
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(WorkflowLibraryCategories);
|
@ -0,0 +1,38 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { WorkflowCategory } from './types';
|
||||
import { Dispatch, SetStateAction, memo } from 'react';
|
||||
import { paths } from 'services/api/schema';
|
||||
import WorkflowLibraryCategories from './WorkflowLibraryCategories';
|
||||
import WorkflowLibraryPagination from './WorkflowLibraryPagination';
|
||||
import WorkflowLibraryList from './WorkflowLibraryList';
|
||||
|
||||
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 WorkflowLibraryContent = ({
|
||||
data,
|
||||
category,
|
||||
setCategory,
|
||||
page,
|
||||
setPage,
|
||||
}: Props) => {
|
||||
return (
|
||||
<Flex w="full" h="full" gap={2}>
|
||||
<WorkflowLibraryCategories
|
||||
category={category}
|
||||
setCategory={setCategory}
|
||||
/>
|
||||
<Flex h="full" w="full" gap={2} flexDir="column">
|
||||
<WorkflowLibraryList data={data} />
|
||||
<WorkflowLibraryPagination data={data} page={page} setPage={setPage} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(WorkflowLibraryContent);
|
@ -0,0 +1,45 @@
|
||||
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 ScrollableContent from 'features/nodes/components/sidePanel/ScrollableContent';
|
||||
|
||||
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>
|
||||
))}
|
||||
</Flex>
|
||||
</ScrollableContent>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(WorkflowLibraryList);
|
@ -0,0 +1,43 @@
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
} from '@chakra-ui/react';
|
||||
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 { t } = useTranslation();
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} isCentered>
|
||||
<ModalOverlay />
|
||||
<ModalContent
|
||||
w="80%"
|
||||
h="80%"
|
||||
minW="unset"
|
||||
minH="unset"
|
||||
maxW="unset"
|
||||
maxH="unset"
|
||||
>
|
||||
<ModalHeader>{t('workflows.workflowLibrary')}</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<WorkflowLibraryWrapper />
|
||||
</ModalBody>
|
||||
<ModalFooter />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(WorkflowLibraryModal);
|
@ -0,0 +1,88 @@
|
||||
import { ButtonGroup } from '@chakra-ui/react';
|
||||
import IAIButton from 'common/components/IAIButton';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { Dispatch, SetStateAction, memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa';
|
||||
import { paths } from 'services/api/schema';
|
||||
|
||||
const PAGES_TO_DISPLAY = 7;
|
||||
|
||||
type PageData = {
|
||||
page: number;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
page: number;
|
||||
setPage: Dispatch<SetStateAction<number>>;
|
||||
data: paths['/api/v1/workflows/']['get']['responses']['200']['content']['application/json'];
|
||||
};
|
||||
|
||||
const WorkflowLibraryPagination = ({ page, setPage, data }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handlePrevPage = useCallback(() => {
|
||||
setPage((p) => Math.max(p - 1, 0));
|
||||
}, [setPage]);
|
||||
|
||||
const handleNextPage = useCallback(() => {
|
||||
setPage((p) => Math.min(p + 1, data.pages - 1));
|
||||
}, [data.pages, setPage]);
|
||||
|
||||
const pages: PageData[] = useMemo(() => {
|
||||
const pages = [];
|
||||
let first =
|
||||
data.pages > PAGES_TO_DISPLAY
|
||||
? Math.max(0, page - Math.floor(PAGES_TO_DISPLAY / 2))
|
||||
: 0;
|
||||
const last =
|
||||
data.pages > PAGES_TO_DISPLAY
|
||||
? Math.min(data.pages, first + PAGES_TO_DISPLAY)
|
||||
: data.pages;
|
||||
if (last - first < PAGES_TO_DISPLAY && data.pages > PAGES_TO_DISPLAY) {
|
||||
first = last - PAGES_TO_DISPLAY;
|
||||
}
|
||||
for (let i = first; i < last; i++) {
|
||||
pages.push({
|
||||
page: i,
|
||||
onClick: () => setPage(i),
|
||||
});
|
||||
}
|
||||
return pages;
|
||||
}, [data.pages, page, setPage]);
|
||||
|
||||
if (data.items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<IAIIconButton
|
||||
onClick={handlePrevPage}
|
||||
isDisabled={page === 0}
|
||||
aria-label={t('common.prevPage')}
|
||||
icon={<FaChevronLeft />}
|
||||
/>
|
||||
{pages.map((p) => (
|
||||
<IAIButton
|
||||
w={10}
|
||||
onClick={p.page === page ? undefined : p.onClick}
|
||||
variant={p.page === page ? 'invokeAI' : 'ghost'}
|
||||
key={p.page}
|
||||
transitionDuration="0s" // the delay in animation looks jank
|
||||
>
|
||||
{p.page + 1}
|
||||
</IAIButton>
|
||||
))}
|
||||
<IAIIconButton
|
||||
onClick={handleNextPage}
|
||||
isDisabled={page === data.pages - 1}
|
||||
aria-label={t('common.nextPage')}
|
||||
icon={<FaChevronRight />}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(WorkflowLibraryPagination);
|
@ -0,0 +1,31 @@
|
||||
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 @@
|
||||
export type WorkflowCategory = 'user' | 'system';
|
@ -1,45 +0,0 @@
|
||||
import { Checkbox, Flex, FormControl, FormLabel } from '@chakra-ui/react';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useEmbedWorkflow } from 'features/nodes/hooks/useEmbedWorkflow';
|
||||
import { useWithWorkflow } from 'features/nodes/hooks/useWithWorkflow';
|
||||
import { nodeEmbedWorkflowChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { ChangeEvent, memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const EmbedWorkflowCheckbox = ({ nodeId }: { nodeId: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const withWorkflow = useWithWorkflow(nodeId);
|
||||
const embedWorkflow = useEmbedWorkflow(nodeId);
|
||||
const handleChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch(
|
||||
nodeEmbedWorkflowChanged({
|
||||
nodeId,
|
||||
embedWorkflow: e.target.checked,
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch, nodeId]
|
||||
);
|
||||
|
||||
if (!withWorkflow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<FormControl as={Flex} sx={{ alignItems: 'center', gap: 2, w: 'auto' }}>
|
||||
<FormLabel sx={{ fontSize: 'xs', mb: '1px' }}>
|
||||
{t('metadata.workflow')}
|
||||
</FormLabel>
|
||||
<Checkbox
|
||||
className="nopan"
|
||||
size="sm"
|
||||
onChange={handleChange}
|
||||
isChecked={embedWorkflow}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(EmbedWorkflowCheckbox);
|
@ -1,9 +1,8 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { useHasImageOutput } from 'features/nodes/hooks/useHasImageOutput';
|
||||
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
|
||||
import { memo } from 'react';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import EmbedWorkflowCheckbox from './EmbedWorkflowCheckbox';
|
||||
import { memo } from 'react';
|
||||
import SaveToGalleryCheckbox from './SaveToGalleryCheckbox';
|
||||
import UseCacheCheckbox from './UseCacheCheckbox';
|
||||
|
||||
@ -28,7 +27,6 @@ const InvocationNodeFooter = ({ nodeId }: Props) => {
|
||||
}}
|
||||
>
|
||||
{isCacheEnabled && <UseCacheCheckbox nodeId={nodeId} />}
|
||||
{hasImageOutput && <EmbedWorkflowCheckbox nodeId={nodeId} />}
|
||||
{hasImageOutput && <SaveToGalleryCheckbox nodeId={nodeId} />}
|
||||
</Flex>
|
||||
);
|
||||
|
@ -3,6 +3,7 @@ import { memo } from 'react';
|
||||
import LoadWorkflowButton from './LoadWorkflowButton';
|
||||
import ResetWorkflowButton from './ResetWorkflowButton';
|
||||
import DownloadWorkflowButton from './DownloadWorkflowButton';
|
||||
import WorkflowLibraryButton from 'features/nodes/components/flow/WorkflowLibrary/WorkflowLibraryButton';
|
||||
|
||||
const TopCenterPanel = () => {
|
||||
return (
|
||||
@ -18,6 +19,7 @@ const TopCenterPanel = () => {
|
||||
<DownloadWorkflowButton />
|
||||
<LoadWorkflowButton />
|
||||
<ResetWorkflowButton />
|
||||
<WorkflowLibraryButton />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
@ -11,17 +11,16 @@ import { stateSelector } from 'app/store/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import { getNeedsUpdate } from 'features/nodes/util/node/nodeUpdate';
|
||||
import NotesTextarea from 'features/nodes/components/flow/nodes/Invocation/NotesTextarea';
|
||||
import ScrollableContent from 'features/nodes/components/sidePanel/ScrollableContent';
|
||||
import {
|
||||
InvocationNodeData,
|
||||
InvocationNode,
|
||||
InvocationTemplate,
|
||||
isInvocationNode,
|
||||
} from 'features/nodes/types/invocation';
|
||||
import { getNeedsUpdate } from 'features/nodes/util/node/nodeUpdate';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Node } from 'reactflow';
|
||||
import NotesTextarea from 'features/nodes/components/flow/nodes/Invocation/NotesTextarea';
|
||||
import ScrollableContent from 'features/nodes/components/sidePanel/ScrollableContent';
|
||||
import EditableNodeTitle from './details/EditableNodeTitle';
|
||||
|
||||
const selector = createSelector(
|
||||
@ -62,7 +61,7 @@ const InspectorDetailsTab = () => {
|
||||
export default memo(InspectorDetailsTab);
|
||||
|
||||
type ContentProps = {
|
||||
node: Node<InvocationNodeData>;
|
||||
node: InvocationNode;
|
||||
template: InvocationTemplate;
|
||||
};
|
||||
|
||||
|
@ -1,27 +0,0 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { useMemo } from 'react';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
|
||||
export const useEmbedWorkflow = (nodeId: string) => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(
|
||||
stateSelector,
|
||||
({ nodes }) => {
|
||||
const node = nodes.nodes.find((node) => node.id === nodeId);
|
||||
if (!isInvocationNode(node)) {
|
||||
return false;
|
||||
}
|
||||
return node.data.embedWorkflow;
|
||||
},
|
||||
defaultSelectorOptions
|
||||
),
|
||||
[nodeId]
|
||||
);
|
||||
|
||||
const embedWorkflow = useAppSelector(selector);
|
||||
return embedWorkflow;
|
||||
};
|
@ -1,31 +0,0 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { useMemo } from 'react';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
|
||||
export const useWithWorkflow = (nodeId: string) => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(
|
||||
stateSelector,
|
||||
({ nodes }) => {
|
||||
const node = nodes.nodes.find((node) => node.id === nodeId);
|
||||
if (!isInvocationNode(node)) {
|
||||
return false;
|
||||
}
|
||||
const nodeTemplate = nodes.nodeTemplates[node?.data.type ?? ''];
|
||||
if (!nodeTemplate) {
|
||||
return false;
|
||||
}
|
||||
return nodeTemplate.withWorkflow;
|
||||
},
|
||||
defaultSelectorOptions
|
||||
),
|
||||
[nodeId]
|
||||
);
|
||||
|
||||
const withWorkflow = useAppSelector(selector);
|
||||
return withWorkflow;
|
||||
};
|
@ -344,20 +344,6 @@ const nodesSlice = createSlice({
|
||||
}
|
||||
field.label = label;
|
||||
},
|
||||
nodeEmbedWorkflowChanged: (
|
||||
state,
|
||||
action: PayloadAction<{ nodeId: string; embedWorkflow: boolean }>
|
||||
) => {
|
||||
const { nodeId, embedWorkflow } = action.payload;
|
||||
const nodeIndex = state.nodes.findIndex((n) => n.id === nodeId);
|
||||
|
||||
const node = state.nodes?.[nodeIndex];
|
||||
|
||||
if (!isInvocationNode(node)) {
|
||||
return;
|
||||
}
|
||||
node.data.embedWorkflow = embedWorkflow;
|
||||
},
|
||||
nodeUseCacheChanged: (
|
||||
state,
|
||||
action: PayloadAction<{ nodeId: string; useCache: boolean }>
|
||||
@ -984,7 +970,6 @@ export const {
|
||||
nodeAdded,
|
||||
nodeReplaced,
|
||||
nodeEditorReset,
|
||||
nodeEmbedWorkflowChanged,
|
||||
nodeExclusivelySelected,
|
||||
nodeIsIntermediateChanged,
|
||||
nodeIsOpenChanged,
|
||||
|
@ -18,7 +18,6 @@ export const zInvocationTemplate = z.object({
|
||||
inputs: z.record(zFieldInputTemplate),
|
||||
outputs: z.record(zFieldOutputTemplate),
|
||||
outputType: z.string().min(1),
|
||||
withWorkflow: z.boolean(),
|
||||
version: zSemVer,
|
||||
useCache: z.boolean(),
|
||||
nodePack: z.string().min(1).nullish(),
|
||||
@ -33,7 +32,6 @@ export const zInvocationNodeData = z.object({
|
||||
label: z.string(),
|
||||
isOpen: z.boolean(),
|
||||
notes: z.string(),
|
||||
embedWorkflow: z.boolean(),
|
||||
isIntermediate: z.boolean(),
|
||||
useCache: z.boolean(),
|
||||
version: zSemVer,
|
||||
|
@ -73,6 +73,7 @@ export type WorkflowEdge = z.infer<typeof zWorkflowEdge>;
|
||||
|
||||
// #region Workflow
|
||||
export const zWorkflowV2 = z.object({
|
||||
id: z.string().min(1).optional(),
|
||||
name: z.string(),
|
||||
author: z.string(),
|
||||
description: z.string(),
|
||||
|
@ -1,14 +1,13 @@
|
||||
import { NodesState } from 'features/nodes/store/types';
|
||||
import {
|
||||
FieldInputInstance,
|
||||
isColorFieldInputInstance,
|
||||
} from 'features/nodes/types/field';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { cloneDeep, omit, reduce } from 'lodash-es';
|
||||
import { Graph } from 'services/api/types';
|
||||
import { AnyInvocation } from 'services/events/types';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { buildWorkflow } from 'features/nodes/util/workflow/buildWorkflow';
|
||||
import {
|
||||
FieldInputInstance,
|
||||
isColorFieldInputInstance,
|
||||
} from 'features/nodes/types/field';
|
||||
|
||||
/**
|
||||
* We need to do special handling for some fields
|
||||
@ -44,7 +43,7 @@ export const buildNodesGraph = (nodesState: NodesState): Graph => {
|
||||
const parsedNodes = filteredNodes.reduce<NonNullable<Graph['nodes']>>(
|
||||
(nodesAccumulator, node) => {
|
||||
const { id, data } = node;
|
||||
const { type, inputs, isIntermediate, embedWorkflow } = data;
|
||||
const { type, inputs, isIntermediate } = data;
|
||||
|
||||
// Transform each node's inputs to simple key-value pairs
|
||||
const transformedInputs = reduce(
|
||||
@ -69,11 +68,6 @@ export const buildNodesGraph = (nodesState: NodesState): Graph => {
|
||||
is_intermediate: isIntermediate,
|
||||
};
|
||||
|
||||
if (embedWorkflow) {
|
||||
// add the workflow to the node
|
||||
Object.assign(graphNode, { workflow: buildWorkflow(nodesState) });
|
||||
}
|
||||
|
||||
// Add it to the nodes object
|
||||
Object.assign(nodesAccumulator, {
|
||||
[id]: graphNode,
|
||||
|
@ -67,7 +67,6 @@ export const buildInvocationNode = (
|
||||
label: '',
|
||||
notes: '',
|
||||
isOpen: true,
|
||||
embedWorkflow: false,
|
||||
isIntermediate: type === 'save_image' ? false : true,
|
||||
useCache: template.useCache,
|
||||
inputs,
|
||||
|
@ -1,16 +1,15 @@
|
||||
import { satisfies } from 'compare-versions';
|
||||
import { NodeUpdateError } from 'features/nodes/types/error';
|
||||
import {
|
||||
InvocationNodeData,
|
||||
InvocationNode,
|
||||
InvocationTemplate,
|
||||
} from 'features/nodes/types/invocation';
|
||||
import { zParsedSemver } from 'features/nodes/types/semver';
|
||||
import { cloneDeep, defaultsDeep } from 'lodash-es';
|
||||
import { Node } from 'reactflow';
|
||||
import { cloneDeep, keys, defaultsDeep, pick } from 'lodash-es';
|
||||
import { buildInvocationNode } from './buildInvocationNode';
|
||||
|
||||
export const getNeedsUpdate = (
|
||||
node: Node<InvocationNodeData>,
|
||||
node: InvocationNode,
|
||||
template: InvocationTemplate
|
||||
): boolean => {
|
||||
if (node.data.type !== template.type) {
|
||||
@ -24,7 +23,7 @@ export const getNeedsUpdate = (
|
||||
*/
|
||||
|
||||
export const getMayUpdateNode = (
|
||||
node: Node<InvocationNodeData>,
|
||||
node: InvocationNode,
|
||||
template: InvocationTemplate
|
||||
): boolean => {
|
||||
const needsUpdate = getNeedsUpdate(node, template);
|
||||
@ -45,9 +44,9 @@ export const getMayUpdateNode = (
|
||||
*/
|
||||
|
||||
export const updateNode = (
|
||||
node: Node<InvocationNodeData>,
|
||||
node: InvocationNode,
|
||||
template: InvocationTemplate
|
||||
): Node<InvocationNodeData> => {
|
||||
): InvocationNode => {
|
||||
const mayUpdate = getMayUpdateNode(node, template);
|
||||
|
||||
if (!mayUpdate || node.data.type !== template.type) {
|
||||
@ -64,5 +63,8 @@ export const updateNode = (
|
||||
clone.data.version = template.version;
|
||||
defaultsDeep(clone, defaults); // mutates!
|
||||
|
||||
// Remove any fields that are not in the template
|
||||
clone.data.inputs = pick(clone.data.inputs, keys(defaults.data.inputs));
|
||||
clone.data.outputs = pick(clone.data.outputs, keys(defaults.data.outputs));
|
||||
return clone;
|
||||
};
|
||||
|
@ -86,7 +86,6 @@ export const parseSchema = (
|
||||
const description = schema.description ?? '';
|
||||
const version = schema.version;
|
||||
const nodePack = schema.node_pack;
|
||||
let withWorkflow = false;
|
||||
|
||||
const inputs = reduce(
|
||||
schema.properties,
|
||||
@ -114,12 +113,6 @@ export const parseSchema = (
|
||||
try {
|
||||
const fieldType = parseFieldType(property);
|
||||
|
||||
if (fieldType.name === 'WorkflowField') {
|
||||
// This supports workflows, set the flag and skip to next field
|
||||
withWorkflow = true;
|
||||
return inputsAccumulator;
|
||||
}
|
||||
|
||||
if (isReservedFieldType(fieldType.name)) {
|
||||
// Skip processing this reserved field
|
||||
return inputsAccumulator;
|
||||
@ -260,7 +253,6 @@ export const parseSchema = (
|
||||
inputs,
|
||||
outputs,
|
||||
useCache,
|
||||
withWorkflow,
|
||||
nodePack,
|
||||
};
|
||||
|
||||
|
@ -1,18 +1,18 @@
|
||||
import { $store } from 'app/store/nanostores/store';
|
||||
import { RootState } from 'app/store/store';
|
||||
import { FieldType } from 'features/nodes/types/field';
|
||||
import { InvocationNodeData } from 'features/nodes/types/invocation';
|
||||
import { t } from 'i18next';
|
||||
import { forEach } from 'lodash-es';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
WorkflowMigrationError,
|
||||
WorkflowVersionError,
|
||||
} from 'features/nodes/types/error';
|
||||
import { FieldType } from 'features/nodes/types/field';
|
||||
import { InvocationNodeData } from 'features/nodes/types/invocation';
|
||||
import { zSemVer } from 'features/nodes/types/semver';
|
||||
import { FIELD_TYPE_V1_TO_FIELD_TYPE_V2_MAPPING } from 'features/nodes/types/v1/fieldTypeMap';
|
||||
import { WorkflowV1, zWorkflowV1 } from 'features/nodes/types/v1/workflowV1';
|
||||
import { WorkflowV2, zWorkflowV2 } from 'features/nodes/types/workflow';
|
||||
import { t } from 'i18next';
|
||||
import { forEach } from 'lodash-es';
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Helper schema to extract the version from a workflow.
|
||||
@ -25,6 +25,11 @@ const zWorkflowMetaVersion = z.object({
|
||||
|
||||
/**
|
||||
* Migrates a workflow from V1 to V2.
|
||||
*
|
||||
* Changes include:
|
||||
* - Field types are now structured
|
||||
* - Invocation node pack is now saved in the node data
|
||||
* - Workflow schema version bumped to 2.0.0
|
||||
*/
|
||||
const migrateV1toV2 = (workflowToMigrate: WorkflowV1): WorkflowV2 => {
|
||||
const invocationTemplates = ($store.get()?.getState() as RootState).nodes
|
||||
@ -39,7 +44,6 @@ const migrateV1toV2 = (workflowToMigrate: WorkflowV1): WorkflowV2 => {
|
||||
t('nodes.unknownFieldType', { type: input.type })
|
||||
);
|
||||
}
|
||||
// Cast as the V2 type
|
||||
(input.type as unknown as FieldType) = newFieldType;
|
||||
});
|
||||
forEach(node.data.outputs, (output) => {
|
||||
@ -50,19 +54,19 @@ const migrateV1toV2 = (workflowToMigrate: WorkflowV1): WorkflowV2 => {
|
||||
t('nodes.unknownFieldType', { type: output.type })
|
||||
);
|
||||
}
|
||||
// Cast as the V2 type
|
||||
(output.type as unknown as FieldType) = newFieldType;
|
||||
});
|
||||
// Migrate nodePack
|
||||
// Add node pack
|
||||
const invocationTemplate = invocationTemplates[node.data.type];
|
||||
const nodePack = invocationTemplate
|
||||
? invocationTemplate.nodePack
|
||||
: t('common.unknown');
|
||||
// Cast as the V2 type
|
||||
(node.data as unknown as InvocationNodeData).nodePack = nodePack;
|
||||
}
|
||||
});
|
||||
(workflowToMigrate.meta.version as WorkflowV2['meta']['version']) = '2.0.0';
|
||||
// Bump version
|
||||
(workflowToMigrate as unknown as WorkflowV2).meta.version = '2.0.0';
|
||||
// Parsing strips out any extra properties not in the latest version
|
||||
return zWorkflowV2.parse(workflowToMigrate);
|
||||
};
|
||||
|
||||
@ -73,7 +77,6 @@ export const parseAndMigrateWorkflow = (data: unknown): WorkflowV2 => {
|
||||
const workflowVersionResult = zWorkflowMetaVersion.safeParse(data);
|
||||
|
||||
if (!workflowVersionResult.success) {
|
||||
console.log(data);
|
||||
throw new WorkflowVersionError(t('nodes.unableToGetWorkflowVersion'));
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,6 @@ import {
|
||||
} from 'features/gallery/store/types';
|
||||
import { CoreMetadata, zCoreMetadata } from 'features/nodes/types/metadata';
|
||||
import { keyBy } from 'lodash-es';
|
||||
import { ApiTagDescription, LIST_TAG, api } from '..';
|
||||
import { components, paths } from 'services/api/schema';
|
||||
import {
|
||||
DeleteBoardResult,
|
||||
@ -26,6 +25,7 @@ import {
|
||||
imagesAdapter,
|
||||
imagesSelectors,
|
||||
} from 'services/api/util';
|
||||
import { ApiTagDescription, LIST_TAG, api } from '..';
|
||||
import { boardsApi } from './boards';
|
||||
|
||||
export const imagesApi = api.injectEndpoints({
|
||||
@ -128,6 +128,16 @@ export const imagesApi = api.injectEndpoints({
|
||||
},
|
||||
keepUnusedDataFor: 86400, // 24 hours
|
||||
}),
|
||||
getImageWorkflow: build.query<
|
||||
paths['/api/v1/images/i/{image_name}/workflow']['get']['responses']['200']['content']['application/json'],
|
||||
string
|
||||
>({
|
||||
query: (image_name) => ({ url: `images/i/${image_name}/workflow` }),
|
||||
providesTags: (result, error, image_name) => [
|
||||
{ type: 'ImageWorkflow', id: image_name },
|
||||
],
|
||||
keepUnusedDataFor: 86400, // 24 hours
|
||||
}),
|
||||
deleteImage: build.mutation<void, ImageDTO>({
|
||||
query: ({ image_name }) => ({
|
||||
url: `images/i/${image_name}`,
|
||||
@ -1560,6 +1570,8 @@ export const {
|
||||
useLazyListImagesQuery,
|
||||
useGetImageDTOQuery,
|
||||
useGetImageMetadataQuery,
|
||||
useGetImageWorkflowQuery,
|
||||
useLazyGetImageWorkflowQuery,
|
||||
useDeleteImageMutation,
|
||||
useDeleteImagesMutation,
|
||||
useUploadImageMutation,
|
||||
|
@ -1,30 +1,70 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { WorkflowV2, zWorkflowV2 } from 'features/nodes/types/workflow';
|
||||
import { api } from '..';
|
||||
import { WorkflowV2 } from 'features/nodes/types/workflow';
|
||||
import { paths } from 'services/api/schema';
|
||||
import { LIST_TAG, api } from '..';
|
||||
|
||||
export const workflowsApi = api.injectEndpoints({
|
||||
endpoints: (build) => ({
|
||||
getWorkflow: build.query<WorkflowV2 | undefined, string>({
|
||||
getWorkflow: build.query<
|
||||
paths['/api/v1/workflows/i/{workflow_id}']['get']['responses']['200']['content']['application/json'],
|
||||
string
|
||||
>({
|
||||
query: (workflow_id) => `workflows/i/${workflow_id}`,
|
||||
providesTags: (result, error, workflow_id) => [
|
||||
{ type: 'Workflow', id: workflow_id },
|
||||
],
|
||||
transformResponse: (
|
||||
response: paths['/api/v1/workflows/i/{workflow_id}']['get']['responses']['200']['content']['application/json']
|
||||
) => {
|
||||
if (response) {
|
||||
const result = zWorkflowV2.safeParse(response);
|
||||
if (result.success) {
|
||||
return result.data;
|
||||
} else {
|
||||
logger('images').warn('Problem parsing workflow');
|
||||
}
|
||||
}
|
||||
return;
|
||||
},
|
||||
}),
|
||||
deleteWorkflow: build.mutation<void, string>({
|
||||
query: (workflow_id) => ({
|
||||
url: `workflows/i/${workflow_id}`,
|
||||
method: 'DELETE',
|
||||
}),
|
||||
invalidatesTags: [{ type: 'Workflow', id: LIST_TAG }],
|
||||
}),
|
||||
createWorkflow: build.mutation<
|
||||
paths['/api/v1/workflows/']['post']['responses']['200']['content']['application/json'],
|
||||
WorkflowV2
|
||||
>({
|
||||
query: (workflow) => ({
|
||||
url: 'workflows',
|
||||
method: 'POST',
|
||||
body: workflow,
|
||||
}),
|
||||
invalidatesTags: [{ type: 'Workflow', id: LIST_TAG }],
|
||||
}),
|
||||
updateWorkflow: build.mutation<
|
||||
paths['/api/v1/workflows/i/{workflow_id}']['patch']['responses']['200']['content']['application/json'],
|
||||
WorkflowV2
|
||||
>({
|
||||
query: (workflow) => ({
|
||||
url: `workflows/i/${workflow.id}`,
|
||||
method: 'PATCH',
|
||||
body: workflow,
|
||||
}),
|
||||
invalidatesTags: (response, error, arg) => [
|
||||
{ type: 'Workflow', id: LIST_TAG },
|
||||
{ type: 'Workflow', id: arg.id },
|
||||
],
|
||||
}),
|
||||
listWorkflows: build.query<
|
||||
paths['/api/v1/workflows/']['get']['responses']['200']['content']['application/json'],
|
||||
NonNullable<paths['/api/v1/workflows/']['get']['parameters']['query']>
|
||||
>({
|
||||
query: (params) => ({
|
||||
url: 'workflows/',
|
||||
params,
|
||||
}),
|
||||
providesTags: (result, error, params) => [
|
||||
{ type: 'Workflow', id: LIST_TAG },
|
||||
{ type: 'Workflow', id: params?.page },
|
||||
],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const { useGetWorkflowQuery } = workflowsApi;
|
||||
export const {
|
||||
useGetWorkflowQuery,
|
||||
useCreateWorkflowMutation,
|
||||
useDeleteWorkflowMutation,
|
||||
useUpdateWorkflowMutation,
|
||||
useListWorkflowsQuery,
|
||||
} = workflowsApi;
|
||||
|
@ -20,6 +20,7 @@ export const tagTypes = [
|
||||
'ImageNameList',
|
||||
'ImageList',
|
||||
'ImageMetadata',
|
||||
'ImageWorkflow',
|
||||
'ImageMetadataFromFile',
|
||||
'IntermediatesCount',
|
||||
'SessionQueueItem',
|
||||
|
544
invokeai/frontend/web/src/services/api/schema.d.ts
vendored
544
invokeai/frontend/web/src/services/api/schema.d.ts
vendored
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user