mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): add update node functionality
A workflow's nodes may update itself, if its major version matches the template's major version. If the major versions do not match, the user will need to delete and re-add the node (current behaviour). The update functionality is not automatic (for now). The logic to update the node is pretty simple, but I want to ensure it works well first before doing it automatically when a workflow is loaded. - New `Details` tab on Workflow Inspector, displays node title, type, version, and notes - Button to update the node is displayed on the `Details` tab - Add hook to determine if a node needs an update, may be updated (i.e. major versions match), and the callback to update the node in state - Remove the notes modal from the little info icon - Modularize the node building logic
This commit is contained in:
@ -3,7 +3,7 @@ import { memo } from 'react';
|
|||||||
import NodeCollapseButton from '../common/NodeCollapseButton';
|
import NodeCollapseButton from '../common/NodeCollapseButton';
|
||||||
import NodeTitle from '../common/NodeTitle';
|
import NodeTitle from '../common/NodeTitle';
|
||||||
import InvocationNodeCollapsedHandles from './InvocationNodeCollapsedHandles';
|
import InvocationNodeCollapsedHandles from './InvocationNodeCollapsedHandles';
|
||||||
import InvocationNodeNotes from './InvocationNodeNotes';
|
import InvocationNodeInfoIcon from './InvocationNodeInfoIcon';
|
||||||
import InvocationNodeStatusIndicator from './InvocationNodeStatusIndicator';
|
import InvocationNodeStatusIndicator from './InvocationNodeStatusIndicator';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -34,7 +34,7 @@ const InvocationNodeHeader = ({ nodeId, isOpen }: Props) => {
|
|||||||
<NodeTitle nodeId={nodeId} />
|
<NodeTitle nodeId={nodeId} />
|
||||||
<Flex alignItems="center">
|
<Flex alignItems="center">
|
||||||
<InvocationNodeStatusIndicator nodeId={nodeId} />
|
<InvocationNodeStatusIndicator nodeId={nodeId} />
|
||||||
<InvocationNodeNotes nodeId={nodeId} />
|
<InvocationNodeInfoIcon nodeId={nodeId} />
|
||||||
</Flex>
|
</Flex>
|
||||||
{!isOpen && <InvocationNodeCollapsedHandles nodeId={nodeId} />}
|
{!isOpen && <InvocationNodeCollapsedHandles nodeId={nodeId} />}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -1,85 +1,39 @@
|
|||||||
import {
|
import { Flex, Icon, Text, Tooltip } from '@chakra-ui/react';
|
||||||
Flex,
|
|
||||||
Icon,
|
|
||||||
Modal,
|
|
||||||
ModalBody,
|
|
||||||
ModalCloseButton,
|
|
||||||
ModalContent,
|
|
||||||
ModalFooter,
|
|
||||||
ModalHeader,
|
|
||||||
ModalOverlay,
|
|
||||||
Text,
|
|
||||||
Tooltip,
|
|
||||||
useDisclosure,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import { compare } from 'compare-versions';
|
import { compare } from 'compare-versions';
|
||||||
import { useNodeData } from 'features/nodes/hooks/useNodeData';
|
import { useNodeData } from 'features/nodes/hooks/useNodeData';
|
||||||
import { useNodeLabel } from 'features/nodes/hooks/useNodeLabel';
|
|
||||||
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
|
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
|
||||||
import { useNodeTemplateTitle } from 'features/nodes/hooks/useNodeTemplateTitle';
|
import { useNodeVersion } from 'features/nodes/hooks/useNodeVersion';
|
||||||
import { isInvocationNodeData } from 'features/nodes/types/types';
|
import { isInvocationNodeData } from 'features/nodes/types/types';
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { FaInfoCircle } from 'react-icons/fa';
|
|
||||||
import NotesTextarea from './NotesTextarea';
|
|
||||||
import { useDoNodeVersionsMatch } from 'features/nodes/hooks/useDoNodeVersionsMatch';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { FaInfoCircle } from 'react-icons/fa';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const InvocationNodeNotes = ({ nodeId }: Props) => {
|
const InvocationNodeInfoIcon = ({ nodeId }: Props) => {
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { needsUpdate } = useNodeVersion(nodeId);
|
||||||
const label = useNodeLabel(nodeId);
|
|
||||||
const title = useNodeTemplateTitle(nodeId);
|
|
||||||
const doVersionsMatch = useDoNodeVersionsMatch(nodeId);
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
label={<TooltipContent nodeId={nodeId} />}
|
label={<TooltipContent nodeId={nodeId} />}
|
||||||
placement="top"
|
placement="top"
|
||||||
shouldWrapChildren
|
shouldWrapChildren
|
||||||
>
|
|
||||||
<Flex
|
|
||||||
className="nodrag"
|
|
||||||
onClick={onOpen}
|
|
||||||
sx={{
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
w: 8,
|
|
||||||
h: 8,
|
|
||||||
cursor: 'pointer',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
as={FaInfoCircle}
|
as={FaInfoCircle}
|
||||||
sx={{
|
sx={{
|
||||||
boxSize: 4,
|
boxSize: 4,
|
||||||
w: 8,
|
w: 8,
|
||||||
color: doVersionsMatch ? 'base.400' : 'error.400',
|
color: needsUpdate ? 'error.400' : 'base.400',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Modal isOpen={isOpen} onClose={onClose} isCentered>
|
|
||||||
<ModalOverlay />
|
|
||||||
<ModalContent>
|
|
||||||
<ModalHeader>{label || title || t('nodes.unknownNode')}</ModalHeader>
|
|
||||||
<ModalCloseButton />
|
|
||||||
<ModalBody>
|
|
||||||
<NotesTextarea nodeId={nodeId} />
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter />
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(InvocationNodeNotes);
|
export default memo(InvocationNodeInfoIcon);
|
||||||
|
|
||||||
const TooltipContent = memo(({ nodeId }: { nodeId: string }) => {
|
const TooltipContent = memo(({ nodeId }: { nodeId: string }) => {
|
||||||
const data = useNodeData(nodeId);
|
const data = useNodeData(nodeId);
|
@ -0,0 +1,125 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
HStack,
|
||||||
|
Text,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
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 IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||||
|
import { useNodeVersion } from 'features/nodes/hooks/useNodeVersion';
|
||||||
|
import {
|
||||||
|
InvocationNodeData,
|
||||||
|
InvocationTemplate,
|
||||||
|
isInvocationNode,
|
||||||
|
} from 'features/nodes/types/types';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { FaSync } from 'react-icons/fa';
|
||||||
|
import { Node } from 'reactflow';
|
||||||
|
import NotesTextarea from '../../flow/nodes/Invocation/NotesTextarea';
|
||||||
|
import ScrollableContent from '../ScrollableContent';
|
||||||
|
import EditableNodeTitle from './details/EditableNodeTitle';
|
||||||
|
|
||||||
|
const selector = createSelector(
|
||||||
|
stateSelector,
|
||||||
|
({ nodes }) => {
|
||||||
|
const lastSelectedNodeId =
|
||||||
|
nodes.selectedNodes[nodes.selectedNodes.length - 1];
|
||||||
|
|
||||||
|
const lastSelectedNode = nodes.nodes.find(
|
||||||
|
(node) => node.id === lastSelectedNodeId
|
||||||
|
);
|
||||||
|
|
||||||
|
const lastSelectedNodeTemplate = lastSelectedNode
|
||||||
|
? nodes.nodeTemplates[lastSelectedNode.data.type]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
node: lastSelectedNode,
|
||||||
|
template: lastSelectedNodeTemplate,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
defaultSelectorOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
const InspectorDetailsTab = () => {
|
||||||
|
const { node, template } = useAppSelector(selector);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
if (!template || !isInvocationNode(node)) {
|
||||||
|
return (
|
||||||
|
<IAINoContentFallback label={t('nodes.noNodeSelected')} icon={null} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Content node={node} template={template} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(InspectorDetailsTab);
|
||||||
|
|
||||||
|
const Content = (props: {
|
||||||
|
node: Node<InvocationNodeData>;
|
||||||
|
template: InvocationTemplate;
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { needsUpdate, mayUpdate, updateNode } = useNodeVersion(props.node.id);
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ScrollableContent>
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
flexDir: 'column',
|
||||||
|
position: 'relative',
|
||||||
|
p: 1,
|
||||||
|
gap: 2,
|
||||||
|
w: 'full',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EditableNodeTitle nodeId={props.node.data.id} />
|
||||||
|
<HStack>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Node Type</FormLabel>
|
||||||
|
<Text fontSize="sm" fontWeight={600}>
|
||||||
|
{props.template.title}
|
||||||
|
</Text>
|
||||||
|
</FormControl>
|
||||||
|
<Flex
|
||||||
|
flexDir="row"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="space-between"
|
||||||
|
w="full"
|
||||||
|
>
|
||||||
|
<FormControl isInvalid={needsUpdate}>
|
||||||
|
<FormLabel>Node Version</FormLabel>
|
||||||
|
<Text fontSize="sm" fontWeight={600}>
|
||||||
|
{props.node.data.version}
|
||||||
|
</Text>
|
||||||
|
</FormControl>
|
||||||
|
{mayUpdate && (
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label={t('nodes.updateNode')}
|
||||||
|
tooltip={t('nodes.updateNode')}
|
||||||
|
icon={<FaSync />}
|
||||||
|
onClick={updateNode}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</HStack>
|
||||||
|
<NotesTextarea nodeId={props.node.data.id} />
|
||||||
|
</Flex>
|
||||||
|
</ScrollableContent>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
@ -10,7 +10,7 @@ import { memo } from 'react';
|
|||||||
import InspectorDataTab from './InspectorDataTab';
|
import InspectorDataTab from './InspectorDataTab';
|
||||||
import InspectorOutputsTab from './InspectorOutputsTab';
|
import InspectorOutputsTab from './InspectorOutputsTab';
|
||||||
import InspectorTemplateTab from './InspectorTemplateTab';
|
import InspectorTemplateTab from './InspectorTemplateTab';
|
||||||
// import InspectorDetailsTab from './InspectorDetailsTab';
|
import InspectorDetailsTab from './InspectorDetailsTab';
|
||||||
|
|
||||||
const InspectorPanel = () => {
|
const InspectorPanel = () => {
|
||||||
return (
|
return (
|
||||||
@ -30,16 +30,16 @@ const InspectorPanel = () => {
|
|||||||
sx={{ display: 'flex', flexDir: 'column', w: 'full', h: 'full' }}
|
sx={{ display: 'flex', flexDir: 'column', w: 'full', h: 'full' }}
|
||||||
>
|
>
|
||||||
<TabList>
|
<TabList>
|
||||||
{/* <Tab>Details</Tab> */}
|
<Tab>Details</Tab>
|
||||||
<Tab>Outputs</Tab>
|
<Tab>Outputs</Tab>
|
||||||
<Tab>Data</Tab>
|
<Tab>Data</Tab>
|
||||||
<Tab>Template</Tab>
|
<Tab>Template</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
|
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
{/* <TabPanel>
|
<TabPanel>
|
||||||
<InspectorDetailsTab />
|
<InspectorDetailsTab />
|
||||||
</TabPanel> */}
|
</TabPanel>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<InspectorOutputsTab />
|
<InspectorOutputsTab />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
@ -0,0 +1,74 @@
|
|||||||
|
import {
|
||||||
|
Editable,
|
||||||
|
EditableInput,
|
||||||
|
EditablePreview,
|
||||||
|
Flex,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import { useNodeLabel } from 'features/nodes/hooks/useNodeLabel';
|
||||||
|
import { useNodeTemplateTitle } from 'features/nodes/hooks/useNodeTemplateTitle';
|
||||||
|
import { nodeLabelChanged } from 'features/nodes/store/nodesSlice';
|
||||||
|
import { memo, useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
nodeId: string;
|
||||||
|
title?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EditableNodeTitle = ({ nodeId, title }: Props) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const label = useNodeLabel(nodeId);
|
||||||
|
const templateTitle = useNodeTemplateTitle(nodeId);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [localTitle, setLocalTitle] = useState('');
|
||||||
|
const handleSubmit = useCallback(
|
||||||
|
async (newTitle: string) => {
|
||||||
|
dispatch(nodeLabelChanged({ nodeId, label: newTitle }));
|
||||||
|
setLocalTitle(
|
||||||
|
label || title || templateTitle || t('nodes.problemSettingTitle')
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[dispatch, nodeId, title, templateTitle, label, t]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange = useCallback((newTitle: string) => {
|
||||||
|
setLocalTitle(newTitle);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Another component may change the title; sync local title with global state
|
||||||
|
setLocalTitle(
|
||||||
|
label || title || templateTitle || t('nodes.problemSettingTitle')
|
||||||
|
);
|
||||||
|
}, [label, templateTitle, title, t]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Editable
|
||||||
|
as={Flex}
|
||||||
|
value={localTitle}
|
||||||
|
onChange={handleChange}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
w="full"
|
||||||
|
fontWeight={600}
|
||||||
|
>
|
||||||
|
<EditablePreview noOfLines={1} />
|
||||||
|
<EditableInput
|
||||||
|
className="nodrag"
|
||||||
|
_focusVisible={{ boxShadow: 'none' }}
|
||||||
|
/>
|
||||||
|
</Editable>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(EditableNodeTitle);
|
@ -1,19 +1,10 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { RootState } from 'app/store/store';
|
import { RootState } from 'app/store/store';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { reduce } from 'lodash-es';
|
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { Node, useReactFlow } from 'reactflow';
|
import { Node, useReactFlow } from 'reactflow';
|
||||||
import { AnyInvocationType } from 'services/events/types';
|
import { AnyInvocationType } from 'services/events/types';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { buildNodeData } from '../store/util/buildNodeData';
|
||||||
import {
|
|
||||||
CurrentImageNodeData,
|
|
||||||
InputFieldValue,
|
|
||||||
InvocationNodeData,
|
|
||||||
NotesNodeData,
|
|
||||||
OutputFieldValue,
|
|
||||||
} from '../types/types';
|
|
||||||
import { buildInputFieldValue } from '../util/fieldValueBuilders';
|
|
||||||
import { DRAG_HANDLE_CLASSNAME, NODE_WIDTH } from '../types/constants';
|
import { DRAG_HANDLE_CLASSNAME, NODE_WIDTH } from '../types/constants';
|
||||||
|
|
||||||
const templatesSelector = createSelector(
|
const templatesSelector = createSelector(
|
||||||
@ -26,14 +17,12 @@ export const SHARED_NODE_PROPERTIES: Partial<Node> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useBuildNodeData = () => {
|
export const useBuildNodeData = () => {
|
||||||
const invocationTemplates = useAppSelector(templatesSelector);
|
const nodeTemplates = useAppSelector(templatesSelector);
|
||||||
|
|
||||||
const flow = useReactFlow();
|
const flow = useReactFlow();
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
(type: AnyInvocationType | 'current_image' | 'notes') => {
|
(type: AnyInvocationType | 'current_image' | 'notes') => {
|
||||||
const nodeId = uuidv4();
|
|
||||||
|
|
||||||
let _x = window.innerWidth / 2;
|
let _x = window.innerWidth / 2;
|
||||||
let _y = window.innerHeight / 2;
|
let _y = window.innerHeight / 2;
|
||||||
|
|
||||||
@ -47,111 +36,15 @@ export const useBuildNodeData = () => {
|
|||||||
_y = rect.height / 2 - NODE_WIDTH / 2;
|
_y = rect.height / 2 - NODE_WIDTH / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { x, y } = flow.project({
|
const position = flow.project({
|
||||||
x: _x,
|
x: _x,
|
||||||
y: _y,
|
y: _y,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (type === 'current_image') {
|
const template = nodeTemplates[type];
|
||||||
const node: Node<CurrentImageNodeData> = {
|
|
||||||
...SHARED_NODE_PROPERTIES,
|
return buildNodeData(type, position, template);
|
||||||
id: nodeId,
|
|
||||||
type: 'current_image',
|
|
||||||
position: { x: x, y: y },
|
|
||||||
data: {
|
|
||||||
id: nodeId,
|
|
||||||
type: 'current_image',
|
|
||||||
isOpen: true,
|
|
||||||
label: 'Current Image',
|
|
||||||
},
|
},
|
||||||
};
|
[nodeTemplates, flow]
|
||||||
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === 'notes') {
|
|
||||||
const node: Node<NotesNodeData> = {
|
|
||||||
...SHARED_NODE_PROPERTIES,
|
|
||||||
id: nodeId,
|
|
||||||
type: 'notes',
|
|
||||||
position: { x: x, y: y },
|
|
||||||
data: {
|
|
||||||
id: nodeId,
|
|
||||||
isOpen: true,
|
|
||||||
label: 'Notes',
|
|
||||||
notes: '',
|
|
||||||
type: 'notes',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
const template = invocationTemplates[type];
|
|
||||||
|
|
||||||
if (template === undefined) {
|
|
||||||
console.error(`Unable to find template ${type}.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputs = reduce(
|
|
||||||
template.inputs,
|
|
||||||
(inputsAccumulator, inputTemplate, inputName) => {
|
|
||||||
const fieldId = uuidv4();
|
|
||||||
|
|
||||||
const inputFieldValue: InputFieldValue = buildInputFieldValue(
|
|
||||||
fieldId,
|
|
||||||
inputTemplate
|
|
||||||
);
|
|
||||||
|
|
||||||
inputsAccumulator[inputName] = inputFieldValue;
|
|
||||||
|
|
||||||
return inputsAccumulator;
|
|
||||||
},
|
|
||||||
{} as Record<string, InputFieldValue>
|
|
||||||
);
|
|
||||||
|
|
||||||
const outputs = reduce(
|
|
||||||
template.outputs,
|
|
||||||
(outputsAccumulator, outputTemplate, outputName) => {
|
|
||||||
const fieldId = uuidv4();
|
|
||||||
|
|
||||||
const outputFieldValue: OutputFieldValue = {
|
|
||||||
id: fieldId,
|
|
||||||
name: outputName,
|
|
||||||
type: outputTemplate.type,
|
|
||||||
fieldKind: 'output',
|
|
||||||
};
|
|
||||||
|
|
||||||
outputsAccumulator[outputName] = outputFieldValue;
|
|
||||||
|
|
||||||
return outputsAccumulator;
|
|
||||||
},
|
|
||||||
{} as Record<string, OutputFieldValue>
|
|
||||||
);
|
|
||||||
|
|
||||||
const invocation: Node<InvocationNodeData> = {
|
|
||||||
...SHARED_NODE_PROPERTIES,
|
|
||||||
id: nodeId,
|
|
||||||
type: 'invocation',
|
|
||||||
position: { x: x, y: y },
|
|
||||||
data: {
|
|
||||||
id: nodeId,
|
|
||||||
type,
|
|
||||||
version: template.version,
|
|
||||||
label: '',
|
|
||||||
notes: '',
|
|
||||||
isOpen: true,
|
|
||||||
embedWorkflow: false,
|
|
||||||
isIntermediate: type === 'save_image' ? false : true,
|
|
||||||
inputs,
|
|
||||||
outputs,
|
|
||||||
useCache: template.useCache,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return invocation;
|
|
||||||
},
|
|
||||||
[invocationTemplates, flow]
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
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 { AnyInvocationType } from 'services/events/types';
|
||||||
|
|
||||||
|
export const useNodeTemplateByType = (
|
||||||
|
type: AnyInvocationType | 'current_image' | 'notes'
|
||||||
|
) => {
|
||||||
|
const selector = useMemo(
|
||||||
|
() =>
|
||||||
|
createSelector(
|
||||||
|
stateSelector,
|
||||||
|
({ nodes }) => {
|
||||||
|
const nodeTemplate = nodes.nodeTemplates[type];
|
||||||
|
return nodeTemplate;
|
||||||
|
},
|
||||||
|
defaultSelectorOptions
|
||||||
|
),
|
||||||
|
[type]
|
||||||
|
);
|
||||||
|
|
||||||
|
const nodeTemplate = useAppSelector(selector);
|
||||||
|
|
||||||
|
return nodeTemplate;
|
||||||
|
};
|
@ -0,0 +1,80 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { stateSelector } from 'app/store/store';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
|
import { satisfies } from 'compare-versions';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
InvocationNodeData,
|
||||||
|
isInvocationNode,
|
||||||
|
zParsedSemver,
|
||||||
|
} from '../types/types';
|
||||||
|
import { cloneDeep, defaultsDeep } from 'lodash-es';
|
||||||
|
import { buildNodeData } from '../store/util/buildNodeData';
|
||||||
|
import { AnyInvocationType } from 'services/events/types';
|
||||||
|
import { Node } from 'reactflow';
|
||||||
|
import { nodeReplaced } from '../store/nodesSlice';
|
||||||
|
|
||||||
|
export const useNodeVersion = (nodeId: string) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const selector = useMemo(
|
||||||
|
() =>
|
||||||
|
createSelector(
|
||||||
|
stateSelector,
|
||||||
|
({ nodes }) => {
|
||||||
|
const node = nodes.nodes.find((node) => node.id === nodeId);
|
||||||
|
const nodeTemplate = nodes.nodeTemplates[node?.data.type ?? ''];
|
||||||
|
return { node, nodeTemplate };
|
||||||
|
},
|
||||||
|
defaultSelectorOptions
|
||||||
|
),
|
||||||
|
[nodeId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { node, nodeTemplate } = useAppSelector(selector);
|
||||||
|
|
||||||
|
const needsUpdate = useMemo(() => {
|
||||||
|
if (!isInvocationNode(node) || !nodeTemplate) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return node.data.version !== nodeTemplate.version;
|
||||||
|
}, [node, nodeTemplate]);
|
||||||
|
|
||||||
|
const mayUpdate = useMemo(() => {
|
||||||
|
if (
|
||||||
|
!needsUpdate ||
|
||||||
|
!isInvocationNode(node) ||
|
||||||
|
!nodeTemplate ||
|
||||||
|
!node.data.version
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const templateMajor = zParsedSemver.parse(nodeTemplate.version).major;
|
||||||
|
|
||||||
|
return satisfies(node.data.version, `^${templateMajor}`);
|
||||||
|
}, [needsUpdate, node, nodeTemplate]);
|
||||||
|
|
||||||
|
const updateNode = useCallback(() => {
|
||||||
|
if (
|
||||||
|
!mayUpdate ||
|
||||||
|
!isInvocationNode(node) ||
|
||||||
|
!nodeTemplate ||
|
||||||
|
!node.data.version
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaults = buildNodeData(
|
||||||
|
node.data.type as AnyInvocationType,
|
||||||
|
node.position,
|
||||||
|
nodeTemplate
|
||||||
|
) as Node<InvocationNodeData>;
|
||||||
|
|
||||||
|
const clone = cloneDeep(node);
|
||||||
|
clone.data.version = nodeTemplate.version;
|
||||||
|
defaultsDeep(clone, defaults);
|
||||||
|
dispatch(nodeReplaced({ nodeId: clone.id, node: clone }));
|
||||||
|
}, [dispatch, mayUpdate, node, nodeTemplate]);
|
||||||
|
|
||||||
|
return { needsUpdate, mayUpdate, updateNode };
|
||||||
|
};
|
@ -149,6 +149,18 @@ const nodesSlice = createSlice({
|
|||||||
nodesChanged: (state, action: PayloadAction<NodeChange[]>) => {
|
nodesChanged: (state, action: PayloadAction<NodeChange[]>) => {
|
||||||
state.nodes = applyNodeChanges(action.payload, state.nodes);
|
state.nodes = applyNodeChanges(action.payload, state.nodes);
|
||||||
},
|
},
|
||||||
|
nodeReplaced: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{ nodeId: string; node: Node }>
|
||||||
|
) => {
|
||||||
|
const nodeIndex = state.nodes.findIndex(
|
||||||
|
(n) => n.id === action.payload.nodeId
|
||||||
|
);
|
||||||
|
if (nodeIndex < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.nodes[nodeIndex] = action.payload.node;
|
||||||
|
},
|
||||||
nodeAdded: (
|
nodeAdded: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<
|
action: PayloadAction<
|
||||||
@ -1029,6 +1041,7 @@ export const {
|
|||||||
mouseOverFieldChanged,
|
mouseOverFieldChanged,
|
||||||
mouseOverNodeChanged,
|
mouseOverNodeChanged,
|
||||||
nodeAdded,
|
nodeAdded,
|
||||||
|
nodeReplaced,
|
||||||
nodeEditorReset,
|
nodeEditorReset,
|
||||||
nodeEmbedWorkflowChanged,
|
nodeEmbedWorkflowChanged,
|
||||||
nodeExclusivelySelected,
|
nodeExclusivelySelected,
|
||||||
|
@ -0,0 +1,127 @@
|
|||||||
|
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
|
||||||
|
import {
|
||||||
|
CurrentImageNodeData,
|
||||||
|
InputFieldValue,
|
||||||
|
InvocationNodeData,
|
||||||
|
InvocationTemplate,
|
||||||
|
NotesNodeData,
|
||||||
|
OutputFieldValue,
|
||||||
|
} from 'features/nodes/types/types';
|
||||||
|
import { buildInputFieldValue } from 'features/nodes/util/fieldValueBuilders';
|
||||||
|
import { reduce } from 'lodash-es';
|
||||||
|
import { Node, XYPosition } from 'reactflow';
|
||||||
|
import { AnyInvocationType } from 'services/events/types';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
export const SHARED_NODE_PROPERTIES: Partial<Node> = {
|
||||||
|
dragHandle: `.${DRAG_HANDLE_CLASSNAME}`,
|
||||||
|
};
|
||||||
|
export const buildNodeData = (
|
||||||
|
type: AnyInvocationType | 'current_image' | 'notes',
|
||||||
|
position: XYPosition,
|
||||||
|
template?: InvocationTemplate
|
||||||
|
):
|
||||||
|
| Node<CurrentImageNodeData>
|
||||||
|
| Node<NotesNodeData>
|
||||||
|
| Node<InvocationNodeData>
|
||||||
|
| undefined => {
|
||||||
|
const nodeId = uuidv4();
|
||||||
|
|
||||||
|
if (type === 'current_image') {
|
||||||
|
const node: Node<CurrentImageNodeData> = {
|
||||||
|
...SHARED_NODE_PROPERTIES,
|
||||||
|
id: nodeId,
|
||||||
|
type: 'current_image',
|
||||||
|
position,
|
||||||
|
data: {
|
||||||
|
id: nodeId,
|
||||||
|
type: 'current_image',
|
||||||
|
isOpen: true,
|
||||||
|
label: 'Current Image',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'notes') {
|
||||||
|
const node: Node<NotesNodeData> = {
|
||||||
|
...SHARED_NODE_PROPERTIES,
|
||||||
|
id: nodeId,
|
||||||
|
type: 'notes',
|
||||||
|
position,
|
||||||
|
data: {
|
||||||
|
id: nodeId,
|
||||||
|
isOpen: true,
|
||||||
|
label: 'Notes',
|
||||||
|
notes: '',
|
||||||
|
type: 'notes',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (template === undefined) {
|
||||||
|
console.error(`Unable to find template ${type}.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputs = reduce(
|
||||||
|
template.inputs,
|
||||||
|
(inputsAccumulator, inputTemplate, inputName) => {
|
||||||
|
const fieldId = uuidv4();
|
||||||
|
|
||||||
|
const inputFieldValue: InputFieldValue = buildInputFieldValue(
|
||||||
|
fieldId,
|
||||||
|
inputTemplate
|
||||||
|
);
|
||||||
|
|
||||||
|
inputsAccumulator[inputName] = inputFieldValue;
|
||||||
|
|
||||||
|
return inputsAccumulator;
|
||||||
|
},
|
||||||
|
{} as Record<string, InputFieldValue>
|
||||||
|
);
|
||||||
|
|
||||||
|
const outputs = reduce(
|
||||||
|
template.outputs,
|
||||||
|
(outputsAccumulator, outputTemplate, outputName) => {
|
||||||
|
const fieldId = uuidv4();
|
||||||
|
|
||||||
|
const outputFieldValue: OutputFieldValue = {
|
||||||
|
id: fieldId,
|
||||||
|
name: outputName,
|
||||||
|
type: outputTemplate.type,
|
||||||
|
fieldKind: 'output',
|
||||||
|
};
|
||||||
|
|
||||||
|
outputsAccumulator[outputName] = outputFieldValue;
|
||||||
|
|
||||||
|
return outputsAccumulator;
|
||||||
|
},
|
||||||
|
{} as Record<string, OutputFieldValue>
|
||||||
|
);
|
||||||
|
|
||||||
|
const invocation: Node<InvocationNodeData> = {
|
||||||
|
...SHARED_NODE_PROPERTIES,
|
||||||
|
id: nodeId,
|
||||||
|
type: 'invocation',
|
||||||
|
position,
|
||||||
|
data: {
|
||||||
|
id: nodeId,
|
||||||
|
type,
|
||||||
|
version: template.version,
|
||||||
|
label: '',
|
||||||
|
notes: '',
|
||||||
|
isOpen: true,
|
||||||
|
embedWorkflow: false,
|
||||||
|
isIntermediate: type === 'save_image' ? false : true,
|
||||||
|
inputs,
|
||||||
|
outputs,
|
||||||
|
useCache: template.useCache,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return invocation;
|
||||||
|
};
|
Reference in New Issue
Block a user