fix(ui): improve node rendering performance

Previously the editor was using prop-drilling node data and templates to get values deep into nodes. This ended up causing very noticeable performance degradation. For example, any text entry fields were super laggy.

Refactor the whole thing to use memoized selectors via hooks. The hooks are mostly very narrow, returning only the data needed.

Data objects are never passed down, only node id and field name - sometimes the field kind ('input' or 'output').

The end result is a *much* smoother node editor with very minimal rerenders.
This commit is contained in:
psychedelicious 2023-08-16 22:18:48 +10:00
parent f7c92e1eff
commit f9b8b5cff2
42 changed files with 928 additions and 736 deletions

View File

@ -1,40 +1,34 @@
import { Flex } from '@chakra-ui/react';
import {
InvocationNodeData,
InvocationTemplate,
} from 'features/nodes/types/types';
import { map, some } from 'lodash-es';
import { memo, useMemo } from 'react';
import { NodeProps } from 'reactflow';
import { useFieldNames, useWithFooter } from 'features/nodes/hooks/useNodeData';
import { memo } from 'react';
import InputField from '../fields/InputField';
import OutputField from '../fields/OutputField';
import NodeFooter, { FOOTER_FIELDS } from './NodeFooter';
import NodeFooter from './NodeFooter';
import NodeHeader from './NodeHeader';
import NodeWrapper from './NodeWrapper';
type Props = {
nodeProps: NodeProps<InvocationNodeData>;
nodeTemplate: InvocationTemplate;
nodeId: string;
isOpen: boolean;
label: string;
type: string;
selected: boolean;
};
const InvocationNode = ({ nodeProps, nodeTemplate }: Props) => {
const { id: nodeId, data } = nodeProps;
const { inputs, outputs, isOpen } = data;
const inputFields = useMemo(
() => map(inputs).filter((i) => i.name !== 'is_intermediate'),
[inputs]
);
const outputFields = useMemo(() => map(outputs), [outputs]);
const withFooter = useMemo(
() => some(outputs, (output) => FOOTER_FIELDS.includes(output.type)),
[outputs]
);
const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
const inputFieldNames = useFieldNames(nodeId, 'input');
const outputFieldNames = useFieldNames(nodeId, 'output');
const withFooter = useWithFooter(nodeId);
return (
<NodeWrapper nodeProps={nodeProps}>
<NodeHeader nodeProps={nodeProps} nodeTemplate={nodeTemplate} />
<NodeWrapper nodeId={nodeId} selected={selected}>
<NodeHeader
nodeId={nodeId}
isOpen={isOpen}
label={label}
selected={selected}
type={type}
/>
{isOpen && (
<>
<Flex
@ -54,27 +48,23 @@ const InvocationNode = ({ nodeProps, nodeTemplate }: Props) => {
className="nopan"
sx={{ flexDir: 'column', px: 2, w: 'full', h: 'full' }}
>
{outputFields.map((field) => (
{outputFieldNames.map((fieldName) => (
<OutputField
key={`${nodeId}.${field.id}.input-field`}
nodeProps={nodeProps}
nodeTemplate={nodeTemplate}
field={field}
key={`${nodeId}.${fieldName}.output-field`}
nodeId={nodeId}
fieldName={fieldName}
/>
))}
{inputFields.map((field) => (
{inputFieldNames.map((fieldName) => (
<InputField
key={`${nodeId}.${field.id}.input-field`}
nodeProps={nodeProps}
nodeTemplate={nodeTemplate}
field={field}
key={`${nodeId}.${fieldName}.input-field`}
nodeId={nodeId}
fieldName={fieldName}
/>
))}
</Flex>
</Flex>
{withFooter && (
<NodeFooter nodeProps={nodeProps} nodeTemplate={nodeTemplate} />
)}
{withFooter && <NodeFooter nodeId={nodeId} />}
</>
)}
</NodeWrapper>

View File

@ -2,16 +2,15 @@ import { ChevronUpIcon } from '@chakra-ui/icons';
import { useAppDispatch } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
import { nodeIsOpenChanged } from 'features/nodes/store/nodesSlice';
import { NodeData } from 'features/nodes/types/types';
import { memo, useCallback } from 'react';
import { NodeProps, useUpdateNodeInternals } from 'reactflow';
import { useUpdateNodeInternals } from 'reactflow';
interface Props {
nodeProps: NodeProps<NodeData>;
nodeId: string;
isOpen: boolean;
}
const NodeCollapseButton = (props: Props) => {
const { id: nodeId, isOpen } = props.nodeProps.data;
const NodeCollapseButton = ({ nodeId, isOpen }: Props) => {
const dispatch = useAppDispatch();
const updateNodeInternals = useUpdateNodeInternals();

View File

@ -1,20 +1,17 @@
import { useColorModeValue } from '@chakra-ui/react';
import { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens';
import {
InvocationNodeData,
InvocationTemplate,
} from 'features/nodes/types/types';
import { useNodeData } from 'features/nodes/hooks/useNodeData';
import { isInvocationNodeData } from 'features/nodes/types/types';
import { map } from 'lodash-es';
import { CSSProperties, memo, useMemo } from 'react';
import { Handle, NodeProps, Position } from 'reactflow';
import { Handle, Position } from 'reactflow';
interface Props {
nodeProps: NodeProps<InvocationNodeData>;
nodeTemplate: InvocationTemplate;
nodeId: string;
}
const NodeCollapsedHandles = (props: Props) => {
const { data } = props.nodeProps;
const NodeCollapsedHandles = ({ nodeId }: Props) => {
const data = useNodeData(nodeId);
const { base400, base600 } = useChakraThemeTokens();
const backgroundColor = useColorModeValue(base400, base600);
@ -30,6 +27,10 @@ const NodeCollapsedHandles = (props: Props) => {
[backgroundColor]
);
if (!isInvocationNodeData(data)) {
return null;
}
return (
<>
<Handle
@ -44,7 +45,7 @@ const NodeCollapsedHandles = (props: Props) => {
key={`${data.id}-${input.name}-collapsed-input-handle`}
type="target"
id={input.name}
isValidConnection={() => false}
isConnectable={false}
position={Position.Left}
style={{ visibility: 'hidden' }}
/>
@ -52,7 +53,6 @@ const NodeCollapsedHandles = (props: Props) => {
<Handle
type="source"
id={`${data.id}-collapsed-source`}
isValidConnection={() => false}
isConnectable={false}
position={Position.Right}
style={{ ...dummyHandleStyles, right: '-0.5rem' }}
@ -62,7 +62,7 @@ const NodeCollapsedHandles = (props: Props) => {
key={`${data.id}-${output.name}-collapsed-output-handle`}
type="source"
id={output.name}
isValidConnection={() => false}
isConnectable={false}
position={Position.Right}
style={{ visibility: 'hidden' }}
/>

View File

@ -6,49 +6,22 @@ import {
Spacer,
} from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import {
useHasImageOutput,
useIsIntermediate,
} from 'features/nodes/hooks/useNodeData';
import { fieldBooleanValueChanged } from 'features/nodes/store/nodesSlice';
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
import {
InvocationNodeData,
InvocationTemplate,
} from 'features/nodes/types/types';
import { some } from 'lodash-es';
import { ChangeEvent, memo, useCallback, useMemo } from 'react';
import { NodeProps } from 'reactflow';
import { ChangeEvent, memo, useCallback } from 'react';
export const IMAGE_FIELDS = ['ImageField', 'ImageCollection'];
export const FOOTER_FIELDS = IMAGE_FIELDS;
type Props = {
nodeProps: NodeProps<InvocationNodeData>;
nodeTemplate: InvocationTemplate;
nodeId: string;
};
const NodeFooter = (props: Props) => {
const { nodeProps, nodeTemplate } = props;
const dispatch = useAppDispatch();
const hasImageOutput = useMemo(
() =>
some(nodeTemplate?.outputs, (output) =>
IMAGE_FIELDS.includes(output.type)
),
[nodeTemplate?.outputs]
);
const handleChangeIsIntermediate = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
dispatch(
fieldBooleanValueChanged({
nodeId: nodeProps.data.id,
fieldName: 'is_intermediate',
value: !e.target.checked,
})
);
},
[dispatch, nodeProps.data.id]
);
const NodeFooter = ({ nodeId }: Props) => {
return (
<Flex
className={DRAG_HANDLE_CLASSNAME}
@ -62,19 +35,45 @@ const NodeFooter = (props: Props) => {
}}
>
<Spacer />
{hasImageOutput && (
<FormControl as={Flex} sx={{ alignItems: 'center', gap: 2, w: 'auto' }}>
<FormLabel sx={{ fontSize: 'xs', mb: '1px' }}>Save Output</FormLabel>
<Checkbox
className="nopan"
size="sm"
onChange={handleChangeIsIntermediate}
isChecked={!nodeProps.data.inputs['is_intermediate']?.value}
/>
</FormControl>
)}
<SaveImageCheckbox nodeId={nodeId} />
</Flex>
);
};
export default memo(NodeFooter);
const SaveImageCheckbox = memo(({ nodeId }: { nodeId: string }) => {
const dispatch = useAppDispatch();
const hasImageOutput = useHasImageOutput(nodeId);
const is_intermediate = useIsIntermediate(nodeId);
const handleChangeIsIntermediate = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
dispatch(
fieldBooleanValueChanged({
nodeId,
fieldName: 'is_intermediate',
value: !e.target.checked,
})
);
},
[dispatch, nodeId]
);
if (!hasImageOutput) {
return null;
}
return (
<FormControl as={Flex} sx={{ alignItems: 'center', gap: 2, w: 'auto' }}>
<FormLabel sx={{ fontSize: 'xs', mb: '1px' }}>Save Output</FormLabel>
<Checkbox
className="nopan"
size="sm"
onChange={handleChangeIsIntermediate}
isChecked={!is_intermediate}
/>
</FormControl>
);
});
SaveImageCheckbox.displayName = 'SaveImageCheckbox';

View File

@ -1,10 +1,5 @@
import { Flex } from '@chakra-ui/react';
import {
InvocationNodeData,
InvocationTemplate,
} from 'features/nodes/types/types';
import { memo } from 'react';
import { NodeProps } from 'reactflow';
import NodeCollapseButton from '../Invocation/NodeCollapseButton';
import NodeCollapsedHandles from '../Invocation/NodeCollapsedHandles';
import NodeNotesEdit from '../Invocation/NodeNotesEdit';
@ -12,14 +7,14 @@ import NodeStatusIndicator from '../Invocation/NodeStatusIndicator';
import NodeTitle from '../Invocation/NodeTitle';
type Props = {
nodeProps: NodeProps<InvocationNodeData>;
nodeTemplate: InvocationTemplate;
nodeId: string;
isOpen: boolean;
label: string;
type: string;
selected: boolean;
};
const NodeHeader = (props: Props) => {
const { nodeProps, nodeTemplate } = props;
const { isOpen } = nodeProps.data;
const NodeHeader = ({ nodeId, isOpen, label, type, selected }: Props) => {
return (
<Flex
layerStyle="nodeHeader"
@ -35,18 +30,13 @@ const NodeHeader = (props: Props) => {
_dark: { color: 'base.200' },
}}
>
<NodeCollapseButton nodeProps={nodeProps} />
<NodeTitle nodeData={nodeProps.data} title={nodeTemplate.title} />
<NodeCollapseButton nodeId={nodeId} isOpen={isOpen} />
<NodeTitle nodeId={nodeId} />
<Flex alignItems="center">
<NodeStatusIndicator nodeProps={nodeProps} />
<NodeNotesEdit nodeProps={nodeProps} nodeTemplate={nodeTemplate} />
<NodeStatusIndicator nodeId={nodeId} />
<NodeNotesEdit nodeId={nodeId} />
</Flex>
{!isOpen && (
<NodeCollapsedHandles
nodeProps={nodeProps}
nodeTemplate={nodeTemplate}
/>
)}
{!isOpen && <NodeCollapsedHandles nodeId={nodeId} />}
</Flex>
);
};

View File

@ -16,41 +16,31 @@ import {
} from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import IAITextarea from 'common/components/IAITextarea';
import {
useNodeData,
useNodeLabel,
useNodeTemplate,
useNodeTemplateTitle,
} from 'features/nodes/hooks/useNodeData';
import { nodeNotesChanged } from 'features/nodes/store/nodesSlice';
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
import {
InvocationNodeData,
InvocationTemplate,
} from 'features/nodes/types/types';
import { isInvocationNodeData } from 'features/nodes/types/types';
import { ChangeEvent, memo, useCallback } from 'react';
import { FaInfoCircle } from 'react-icons/fa';
import { NodeProps } from 'reactflow';
interface Props {
nodeProps: NodeProps<InvocationNodeData>;
nodeTemplate: InvocationTemplate;
nodeId: string;
}
const NodeNotesEdit = (props: Props) => {
const { nodeProps, nodeTemplate } = props;
const { data } = nodeProps;
const NodeNotesEdit = ({ nodeId }: Props) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const dispatch = useAppDispatch();
const handleNotesChanged = useCallback(
(e: ChangeEvent<HTMLTextAreaElement>) => {
dispatch(nodeNotesChanged({ nodeId: data.id, notes: e.target.value }));
},
[data.id, dispatch]
);
const label = useNodeLabel(nodeId);
const title = useNodeTemplateTitle(nodeId);
return (
<>
<Tooltip
label={
nodeTemplate ? (
<TooltipContent nodeProps={nodeProps} nodeTemplate={nodeTemplate} />
) : undefined
}
label={<TooltipContent nodeId={nodeId} />}
placement="top"
shouldWrapChildren
>
@ -75,19 +65,10 @@ const NodeNotesEdit = (props: Props) => {
<Modal isOpen={isOpen} onClose={onClose} isCentered>
<ModalOverlay />
<ModalContent>
<ModalHeader>
{data.label || nodeTemplate?.title || 'Unknown Node'}
</ModalHeader>
<ModalHeader>{label || title || 'Unknown Node'}</ModalHeader>
<ModalCloseButton />
<ModalBody>
<FormControl>
<FormLabel>Notes</FormLabel>
<IAITextarea
value={data.notes}
onChange={handleNotesChanged}
rows={10}
/>
</FormControl>
<NotesTextarea nodeId={nodeId} />
</ModalBody>
<ModalFooter />
</ModalContent>
@ -98,16 +79,49 @@ const NodeNotesEdit = (props: Props) => {
export default memo(NodeNotesEdit);
type TooltipContentProps = Props;
const TooltipContent = memo(({ nodeId }: { nodeId: string }) => {
const data = useNodeData(nodeId);
const nodeTemplate = useNodeTemplate(nodeId);
if (!isInvocationNodeData(data)) {
return 'Unknown Node';
}
const TooltipContent = (props: TooltipContentProps) => {
return (
<Flex sx={{ flexDir: 'column' }}>
<Text sx={{ fontWeight: 600 }}>{props.nodeTemplate?.title}</Text>
<Text sx={{ fontWeight: 600 }}>{nodeTemplate?.title}</Text>
<Text sx={{ opacity: 0.7, fontStyle: 'oblique 5deg' }}>
{props.nodeTemplate?.description}
{nodeTemplate?.description}
</Text>
{props.nodeProps.data.notes && <Text>{props.nodeProps.data.notes}</Text>}
{data?.notes && <Text>{data.notes}</Text>}
</Flex>
);
};
});
TooltipContent.displayName = 'TooltipContent';
const NotesTextarea = memo(({ nodeId }: { nodeId: string }) => {
const dispatch = useAppDispatch();
const data = useNodeData(nodeId);
const handleNotesChanged = useCallback(
(e: ChangeEvent<HTMLTextAreaElement>) => {
dispatch(nodeNotesChanged({ nodeId, notes: e.target.value }));
},
[dispatch, nodeId]
);
if (!isInvocationNodeData(data)) {
return null;
}
return (
<FormControl>
<FormLabel>Notes</FormLabel>
<IAITextarea
value={data?.notes}
onChange={handleNotesChanged}
rows={10}
/>
</FormControl>
);
});
NotesTextarea.displayName = 'NodesTextarea';

View File

@ -11,17 +11,12 @@ import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
import {
InvocationNodeData,
NodeExecutionState,
NodeStatus,
} from 'features/nodes/types/types';
import { NodeExecutionState, NodeStatus } from 'features/nodes/types/types';
import { memo, useMemo } from 'react';
import { FaCheck, FaEllipsisH, FaExclamation } from 'react-icons/fa';
import { NodeProps } from 'reactflow';
type Props = {
nodeProps: NodeProps<InvocationNodeData>;
nodeId: string;
};
const iconBoxSize = 3;
@ -33,8 +28,7 @@ const circleStyles = {
'.chakra-progress__track': { stroke: 'transparent' },
};
const NodeStatusIndicator = (props: Props) => {
const nodeId = props.nodeProps.data.id;
const NodeStatusIndicator = ({ nodeId }: Props) => {
const selectNodeExecutionState = useMemo(
() =>
createSelector(
@ -76,7 +70,7 @@ type TooltipLabelProps = {
nodeExecutionState: NodeExecutionState;
};
const TooltipLabel = ({ nodeExecutionState }: TooltipLabelProps) => {
const TooltipLabel = memo(({ nodeExecutionState }: TooltipLabelProps) => {
const { status, progress, progressImage } = nodeExecutionState;
if (status === NodeStatus.PENDING) {
return <Text>Pending</Text>;
@ -118,13 +112,15 @@ const TooltipLabel = ({ nodeExecutionState }: TooltipLabelProps) => {
}
return null;
};
});
TooltipLabel.displayName = 'TooltipLabel';
type StatusIconProps = {
nodeExecutionState: NodeExecutionState;
};
const StatusIcon = (props: StatusIconProps) => {
const StatusIcon = memo((props: StatusIconProps) => {
const { progress, status } = props.nodeExecutionState;
if (status === NodeStatus.PENDING) {
return (
@ -182,4 +178,6 @@ const StatusIcon = (props: StatusIconProps) => {
);
}
return null;
};
});
StatusIcon.displayName = 'StatusIcon';

View File

@ -7,26 +7,29 @@ import {
useEditableControls,
} from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import {
useNodeLabel,
useNodeTemplateTitle,
} from 'features/nodes/hooks/useNodeData';
import { nodeLabelChanged } from 'features/nodes/store/nodesSlice';
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
import { NodeData } from 'features/nodes/types/types';
import { MouseEvent, memo, useCallback, useEffect, useState } from 'react';
type Props = {
nodeData: NodeData;
title: string;
nodeId: string;
title?: string;
};
const NodeTitle = (props: Props) => {
const { title } = props;
const { id: nodeId, label } = props.nodeData;
const NodeTitle = ({ nodeId, title }: Props) => {
const dispatch = useAppDispatch();
const [localTitle, setLocalTitle] = useState(label || title);
const label = useNodeLabel(nodeId);
const templateTitle = useNodeTemplateTitle(nodeId);
const [localTitle, setLocalTitle] = useState('');
const handleSubmit = useCallback(
async (newTitle: string) => {
dispatch(nodeLabelChanged({ nodeId, label: newTitle }));
setLocalTitle(newTitle || title);
setLocalTitle(newTitle || title || 'Problem Setting Title');
},
[nodeId, dispatch, title]
);
@ -37,8 +40,8 @@ const NodeTitle = (props: Props) => {
useEffect(() => {
// Another component may change the title; sync local title with global state
setLocalTitle(label || title);
}, [label, title]);
setLocalTitle(label || title || templateTitle || 'Problem Setting Title');
}, [label, templateTitle, title]);
return (
<Flex

View File

@ -6,10 +6,14 @@ import {
} from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { nodeClicked } from 'features/nodes/store/nodesSlice';
import { MouseEvent, PropsWithChildren, useCallback, useMemo } from 'react';
import {
MouseEvent,
PropsWithChildren,
memo,
useCallback,
useMemo,
} from 'react';
import { DRAG_HANDLE_CLASSNAME, NODE_WIDTH } from '../../types/constants';
import { NodeData } from 'features/nodes/types/types';
import { NodeProps } from 'reactflow';
const useNodeSelect = (nodeId: string) => {
const dispatch = useAppDispatch();
@ -25,14 +29,13 @@ const useNodeSelect = (nodeId: string) => {
};
type NodeWrapperProps = PropsWithChildren & {
nodeProps: NodeProps<NodeData>;
nodeId: string;
selected: boolean;
width?: NonNullable<ChakraProps['sx']>['w'];
};
const NodeWrapper = (props: NodeWrapperProps) => {
const { width, children, nodeProps } = props;
const { data, selected } = nodeProps;
const nodeId = data.id;
const { width, children, nodeId, selected } = props;
const [
nodeSelectedOutlineLight,
@ -93,4 +96,4 @@ const NodeWrapper = (props: NodeWrapperProps) => {
);
};
export default NodeWrapper;
export default memo(NodeWrapper);

View File

@ -1,20 +1,26 @@
import { Box, Flex, Text } from '@chakra-ui/react';
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
import { InvocationNodeData } from 'features/nodes/types/types';
import { memo } from 'react';
import { NodeProps } from 'reactflow';
import NodeCollapseButton from '../Invocation/NodeCollapseButton';
import NodeWrapper from '../Invocation/NodeWrapper';
type Props = {
nodeProps: NodeProps<InvocationNodeData>;
nodeId: string;
isOpen: boolean;
label: string;
type: string;
selected: boolean;
};
const UnknownNodeFallback = ({ nodeProps }: Props) => {
const { data } = nodeProps;
const { isOpen, label, type } = data;
const UnknownNodeFallback = ({
nodeId,
isOpen,
label,
type,
selected,
}: Props) => {
return (
<NodeWrapper nodeProps={nodeProps}>
<NodeWrapper nodeId={nodeId} selected={selected}>
<Flex
className={DRAG_HANDLE_CLASSNAME}
layerStyle="nodeHeader"
@ -27,7 +33,7 @@ const UnknownNodeFallback = ({ nodeProps }: Props) => {
fontSize: 'sm',
}}
>
<NodeCollapseButton nodeProps={nodeProps} />
<NodeCollapseButton nodeId={nodeId} isOpen={isOpen} />
<Text
sx={{
w: 'full',

View File

@ -1,19 +1,12 @@
import { Tooltip } from '@chakra-ui/react';
import { CSSProperties, memo, useMemo } from 'react';
import { Handle, HandleType, NodeProps, Position } from 'reactflow';
import { Handle, HandleType, Position } from 'reactflow';
import {
FIELDS,
HANDLE_TOOLTIP_OPEN_DELAY,
colorTokenToCssVar,
} from '../../types/constants';
import {
InputFieldTemplate,
InputFieldValue,
InvocationNodeData,
InvocationTemplate,
OutputFieldTemplate,
OutputFieldValue,
} from '../../types/types';
import { InputFieldTemplate, OutputFieldTemplate } from '../../types/types';
export const handleBaseStyles: CSSProperties = {
position: 'absolute',
@ -32,9 +25,6 @@ export const outputHandleStyles: CSSProperties = {
};
type FieldHandleProps = {
nodeProps: NodeProps<InvocationNodeData>;
nodeTemplate: InvocationTemplate;
field: InputFieldValue | OutputFieldValue;
fieldTemplate: InputFieldTemplate | OutputFieldTemplate;
handleType: HandleType;
isConnectionInProgress: boolean;

View File

@ -8,13 +8,11 @@ import {
import { useAppDispatch } from 'app/store/storeHooks';
import IAIDraggable from 'common/components/IAIDraggable';
import { NodeFieldDraggableData } from 'features/dnd/types';
import { fieldLabelChanged } from 'features/nodes/store/nodesSlice';
import {
InputFieldTemplate,
InputFieldValue,
InvocationNodeData,
InvocationTemplate,
} from 'features/nodes/types/types';
useFieldData,
useFieldTemplate,
} from 'features/nodes/hooks/useNodeData';
import { fieldLabelChanged } from 'features/nodes/store/nodesSlice';
import {
MouseEvent,
memo,
@ -25,41 +23,43 @@ import {
} from 'react';
interface Props {
nodeData: InvocationNodeData;
nodeTemplate: InvocationTemplate;
field: InputFieldValue;
fieldTemplate: InputFieldTemplate;
nodeId: string;
fieldName: string;
isDraggable?: boolean;
kind: 'input' | 'output';
}
const FieldTitle = (props: Props) => {
const { nodeData, field, fieldTemplate, isDraggable = false } = props;
const { label } = field;
const { title, input } = fieldTemplate;
const { id: nodeId } = nodeData;
const { nodeId, fieldName, isDraggable = false, kind } = props;
const fieldTemplate = useFieldTemplate(nodeId, fieldName, kind);
const field = useFieldData(nodeId, fieldName);
const dispatch = useAppDispatch();
const [localTitle, setLocalTitle] = useState(label || title);
const [localTitle, setLocalTitle] = useState(
field?.label || fieldTemplate?.title || 'Unknown Field'
);
const draggableData: NodeFieldDraggableData | undefined = useMemo(
() =>
input !== 'connection' && isDraggable
field &&
fieldTemplate?.fieldKind === 'input' &&
fieldTemplate?.input !== 'connection' &&
isDraggable
? {
id: `${nodeId}-${field.name}`,
id: `${nodeId}-${fieldName}`,
payloadType: 'NODE_FIELD',
payload: { nodeId, field, fieldTemplate },
}
: undefined,
[field, fieldTemplate, input, isDraggable, nodeId]
[field, fieldName, fieldTemplate, isDraggable, nodeId]
);
const handleSubmit = useCallback(
async (newTitle: string) => {
dispatch(
fieldLabelChanged({ nodeId, fieldName: field.name, label: newTitle })
);
setLocalTitle(newTitle || title);
dispatch(fieldLabelChanged({ nodeId, fieldName, label: newTitle }));
setLocalTitle(newTitle || fieldTemplate?.title || 'Unknown Field');
},
[dispatch, nodeId, field.name, title]
[dispatch, nodeId, fieldName, fieldTemplate?.title]
);
const handleChange = useCallback((newTitle: string) => {
@ -68,8 +68,8 @@ const FieldTitle = (props: Props) => {
useEffect(() => {
// Another component may change the title; sync local title with global state
setLocalTitle(label || title);
}, [label, title]);
setLocalTitle(field?.label || fieldTemplate?.title || 'Unknown Field');
}, [field?.label, fieldTemplate?.title]);
return (
<Flex
@ -120,7 +120,7 @@ type EditableControlsProps = {
draggableData?: NodeFieldDraggableData;
};
function EditableControls(props: EditableControlsProps) {
const EditableControls = memo((props: EditableControlsProps) => {
const { isEditing, getEditButtonProps } = useEditableControls();
const handleDoubleClick = useCallback(
(e: MouseEvent<HTMLDivElement>) => {
@ -158,4 +158,6 @@ function EditableControls(props: EditableControlsProps) {
cursor="text"
/>
);
}
});
EditableControls.displayName = 'EditableControls';

View File

@ -1,38 +1,53 @@
import { Flex, Text } from '@chakra-ui/react';
import {
useFieldData,
useFieldTemplate,
} from 'features/nodes/hooks/useNodeData';
import { FIELDS } from 'features/nodes/types/constants';
import {
InputFieldTemplate,
InputFieldValue,
InvocationNodeData,
InvocationTemplate,
OutputFieldTemplate,
OutputFieldValue,
isInputFieldTemplate,
isInputFieldValue,
} from 'features/nodes/types/types';
import { startCase } from 'lodash-es';
import { useMemo } from 'react';
interface Props {
nodeData: InvocationNodeData;
nodeTemplate: InvocationTemplate;
field: InputFieldValue | OutputFieldValue;
fieldTemplate: InputFieldTemplate | OutputFieldTemplate;
nodeId: string;
fieldName: string;
kind: 'input' | 'output';
}
const FieldTooltipContent = ({ field, fieldTemplate }: Props) => {
const FieldTooltipContent = ({ nodeId, fieldName, kind }: Props) => {
const field = useFieldData(nodeId, fieldName);
const fieldTemplate = useFieldTemplate(nodeId, fieldName, kind);
const isInputTemplate = isInputFieldTemplate(fieldTemplate);
const fieldTitle = useMemo(() => {
if (isInputFieldValue(field)) {
if (field.label && fieldTemplate) {
return `${field.label} (${fieldTemplate.title})`;
}
if (field.label && !fieldTemplate) {
return field.label;
}
if (!field.label && fieldTemplate) {
return fieldTemplate.title;
}
return 'Unknown Field';
}
}, [field, fieldTemplate]);
return (
<Flex sx={{ flexDir: 'column' }}>
<Text sx={{ fontWeight: 600 }}>
{isInputFieldValue(field) && field.label
? `${field.label} (${fieldTemplate.title})`
: fieldTemplate.title}
</Text>
<Text sx={{ opacity: 0.7, fontStyle: 'oblique 5deg' }}>
{fieldTemplate.description}
</Text>
<Text>Type: {FIELDS[fieldTemplate.type].title}</Text>
<Text sx={{ fontWeight: 600 }}>{fieldTitle}</Text>
{fieldTemplate && (
<Text sx={{ opacity: 0.7, fontStyle: 'oblique 5deg' }}>
{fieldTemplate.description}
</Text>
)}
{fieldTemplate && <Text>Type: {FIELDS[fieldTemplate.type].title}</Text>}
{isInputTemplate && <Text>Input: {startCase(fieldTemplate.input)}</Text>}
</Flex>
);

View File

@ -1,27 +1,24 @@
import { Flex, FormControl, FormLabel, Tooltip } from '@chakra-ui/react';
import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
import {
InputFieldValue,
InvocationNodeData,
InvocationTemplate,
} from 'features/nodes/types/types';
import { PropsWithChildren, useMemo } from 'react';
import { NodeProps } from 'reactflow';
useDoesInputHaveValue,
useFieldTemplate,
} from 'features/nodes/hooks/useNodeData';
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
import { PropsWithChildren, memo, useMemo } from 'react';
import FieldHandle from './FieldHandle';
import FieldTitle from './FieldTitle';
import FieldTooltipContent from './FieldTooltipContent';
import InputFieldRenderer from './InputFieldRenderer';
interface Props {
nodeProps: NodeProps<InvocationNodeData>;
nodeTemplate: InvocationTemplate;
field: InputFieldValue;
nodeId: string;
fieldName: string;
}
const InputField = (props: Props) => {
const { nodeProps, nodeTemplate, field } = props;
const { id: nodeId } = nodeProps.data;
const InputField = ({ nodeId, fieldName }: Props) => {
const fieldTemplate = useFieldTemplate(nodeId, fieldName, 'input');
const doesFieldHaveValue = useDoesInputHaveValue(nodeId, fieldName);
const {
isConnected,
@ -29,15 +26,10 @@ const InputField = (props: Props) => {
isConnectionStartField,
connectionError,
shouldDim,
} = useConnectionState({ nodeId, field, kind: 'input' });
const fieldTemplate = useMemo(
() => nodeTemplate.inputs[field.name],
[field.name, nodeTemplate.inputs]
);
} = useConnectionState({ nodeId, fieldName, kind: 'input' });
const isMissingInput = useMemo(() => {
if (!fieldTemplate) {
if (fieldTemplate?.fieldKind !== 'input') {
return false;
}
@ -49,18 +41,18 @@ const InputField = (props: Props) => {
return true;
}
if (!field.value && !isConnected && fieldTemplate.input === 'any') {
if (!doesFieldHaveValue && !isConnected && fieldTemplate.input === 'any') {
return true;
}
}, [fieldTemplate, isConnected, field.value]);
}, [fieldTemplate, isConnected, doesFieldHaveValue]);
if (!fieldTemplate) {
if (fieldTemplate?.fieldKind !== 'input') {
return (
<InputFieldWrapper shouldDim={shouldDim}>
<FormControl
sx={{ color: 'error.400', textAlign: 'left', fontSize: 'sm' }}
>
Unknown input: {field.name}
Unknown input: {fieldName}
</FormControl>
</InputFieldWrapper>
);
@ -82,10 +74,9 @@ const InputField = (props: Props) => {
<Tooltip
label={
<FieldTooltipContent
nodeData={nodeProps.data}
nodeTemplate={nodeTemplate}
field={field}
fieldTemplate={fieldTemplate}
nodeId={nodeId}
fieldName={fieldName}
kind="input"
/>
}
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
@ -95,27 +86,18 @@ const InputField = (props: Props) => {
>
<FormLabel sx={{ mb: 0 }}>
<FieldTitle
nodeData={nodeProps.data}
nodeTemplate={nodeTemplate}
field={field}
fieldTemplate={fieldTemplate}
nodeId={nodeId}
fieldName={fieldName}
kind="input"
isDraggable
/>
</FormLabel>
</Tooltip>
<InputFieldRenderer
nodeData={nodeProps.data}
nodeTemplate={nodeTemplate}
field={field}
fieldTemplate={fieldTemplate}
/>
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
</FormControl>
{fieldTemplate.input !== 'direct' && (
<FieldHandle
nodeProps={nodeProps}
nodeTemplate={nodeTemplate}
field={field}
fieldTemplate={fieldTemplate}
handleType="target"
isConnectionInProgress={isConnectionInProgress}
@ -133,21 +115,25 @@ type InputFieldWrapperProps = PropsWithChildren<{
shouldDim: boolean;
}>;
const InputFieldWrapper = ({ shouldDim, children }: InputFieldWrapperProps) => (
<Flex
className="nopan"
sx={{
position: 'relative',
minH: 8,
py: 0.5,
alignItems: 'center',
opacity: shouldDim ? 0.5 : 1,
transitionProperty: 'opacity',
transitionDuration: '0.1s',
w: 'full',
h: 'full',
}}
>
{children}
</Flex>
const InputFieldWrapper = memo(
({ shouldDim, children }: InputFieldWrapperProps) => (
<Flex
className="nopan"
sx={{
position: 'relative',
minH: 8,
py: 0.5,
alignItems: 'center',
opacity: shouldDim ? 0.5 : 1,
transitionProperty: 'opacity',
transitionDuration: '0.1s',
w: 'full',
h: 'full',
}}
>
{children}
</Flex>
)
);
InputFieldWrapper.displayName = 'InputFieldWrapper';

View File

@ -1,11 +1,9 @@
import { Box } from '@chakra-ui/react';
import { memo } from 'react';
import {
InputFieldTemplate,
InputFieldValue,
InvocationNodeData,
InvocationTemplate,
} from '../../types/types';
useFieldData,
useFieldTemplate,
} from 'features/nodes/hooks/useNodeData';
import { memo } from 'react';
import BooleanInputField from './fieldTypes/BooleanInputField';
import ClipInputField from './fieldTypes/ClipInputField';
import CollectionInputField from './fieldTypes/CollectionInputField';
@ -29,33 +27,33 @@ import VaeInputField from './fieldTypes/VaeInputField';
import VaeModelInputField from './fieldTypes/VaeModelInputField';
type InputFieldProps = {
nodeData: InvocationNodeData;
nodeTemplate: InvocationTemplate;
field: InputFieldValue;
fieldTemplate: InputFieldTemplate;
nodeId: string;
fieldName: string;
};
// build an individual input element based on the schema
const InputFieldRenderer = (props: InputFieldProps) => {
const { nodeData, nodeTemplate, field, fieldTemplate } = props;
const { type } = field;
const InputFieldRenderer = ({ nodeId, fieldName }: InputFieldProps) => {
const field = useFieldData(nodeId, fieldName);
const fieldTemplate = useFieldTemplate(nodeId, fieldName, 'input');
if (type === 'string' && fieldTemplate.type === 'string') {
if (fieldTemplate?.fieldKind === 'output') {
return <Box p={2}>Output field in input: {field?.type}</Box>;
}
if (field?.type === 'string' && fieldTemplate?.type === 'string') {
return (
<StringInputField
nodeData={nodeData}
nodeTemplate={nodeTemplate}
nodeId={nodeId}
field={field}
fieldTemplate={fieldTemplate}
/>
);
}
if (type === 'boolean' && fieldTemplate.type === 'boolean') {
if (field?.type === 'boolean' && fieldTemplate?.type === 'boolean') {
return (
<BooleanInputField
nodeData={nodeData}
nodeTemplate={nodeTemplate}
nodeId={nodeId}
field={field}
fieldTemplate={fieldTemplate}
/>
@ -63,46 +61,32 @@ const InputFieldRenderer = (props: InputFieldProps) => {
}
if (
(type === 'integer' && fieldTemplate.type === 'integer') ||
(type === 'float' && fieldTemplate.type === 'float')
(field?.type === 'integer' && fieldTemplate?.type === 'integer') ||
(field?.type === 'float' && fieldTemplate?.type === 'float')
) {
return (
<NumberInputField
nodeData={nodeData}
nodeTemplate={nodeTemplate}
nodeId={nodeId}
field={field}
fieldTemplate={fieldTemplate}
/>
);
}
if (type === 'enum' && fieldTemplate.type === 'enum') {
if (field?.type === 'enum' && fieldTemplate?.type === 'enum') {
return (
<EnumInputField
nodeData={nodeData}
nodeTemplate={nodeTemplate}
nodeId={nodeId}
field={field}
fieldTemplate={fieldTemplate}
/>
);
}
if (type === 'ImageField' && fieldTemplate.type === 'ImageField') {
if (field?.type === 'ImageField' && fieldTemplate?.type === 'ImageField') {
return (
<ImageInputField
nodeData={nodeData}
nodeTemplate={nodeTemplate}
field={field}
fieldTemplate={fieldTemplate}
/>
);
}
if (type === 'LatentsField' && fieldTemplate.type === 'LatentsField') {
return (
<LatentsInputField
nodeData={nodeData}
nodeTemplate={nodeTemplate}
nodeId={nodeId}
field={field}
fieldTemplate={fieldTemplate}
/>
@ -110,68 +94,55 @@ const InputFieldRenderer = (props: InputFieldProps) => {
}
if (
type === 'ConditioningField' &&
fieldTemplate.type === 'ConditioningField'
field?.type === 'LatentsField' &&
fieldTemplate?.type === 'LatentsField'
) {
return (
<LatentsInputField
nodeId={nodeId}
field={field}
fieldTemplate={fieldTemplate}
/>
);
}
if (
field?.type === 'ConditioningField' &&
fieldTemplate?.type === 'ConditioningField'
) {
return (
<ConditioningInputField
nodeData={nodeData}
nodeTemplate={nodeTemplate}
nodeId={nodeId}
field={field}
fieldTemplate={fieldTemplate}
/>
);
}
if (type === 'UNetField' && fieldTemplate.type === 'UNetField') {
if (field?.type === 'UNetField' && fieldTemplate?.type === 'UNetField') {
return (
<UnetInputField
nodeData={nodeData}
nodeTemplate={nodeTemplate}
nodeId={nodeId}
field={field}
fieldTemplate={fieldTemplate}
/>
);
}
if (type === 'ClipField' && fieldTemplate.type === 'ClipField') {
if (field?.type === 'ClipField' && fieldTemplate?.type === 'ClipField') {
return (
<ClipInputField
nodeData={nodeData}
nodeTemplate={nodeTemplate}
nodeId={nodeId}
field={field}
fieldTemplate={fieldTemplate}
/>
);
}
if (type === 'VaeField' && fieldTemplate.type === 'VaeField') {
if (field?.type === 'VaeField' && fieldTemplate?.type === 'VaeField') {
return (
<VaeInputField
nodeData={nodeData}
nodeTemplate={nodeTemplate}
field={field}
fieldTemplate={fieldTemplate}
/>
);
}
if (type === 'ControlField' && fieldTemplate.type === 'ControlField') {
return (
<ControlInputField
nodeData={nodeData}
nodeTemplate={nodeTemplate}
field={field}
fieldTemplate={fieldTemplate}
/>
);
}
if (type === 'MainModelField' && fieldTemplate.type === 'MainModelField') {
return (
<MainModelInputField
nodeData={nodeData}
nodeTemplate={nodeTemplate}
nodeId={nodeId}
field={field}
fieldTemplate={fieldTemplate}
/>
@ -179,35 +150,38 @@ const InputFieldRenderer = (props: InputFieldProps) => {
}
if (
type === 'SDXLRefinerModelField' &&
fieldTemplate.type === 'SDXLRefinerModelField'
field?.type === 'ControlField' &&
fieldTemplate?.type === 'ControlField'
) {
return (
<ControlInputField
nodeId={nodeId}
field={field}
fieldTemplate={fieldTemplate}
/>
);
}
if (
field?.type === 'MainModelField' &&
fieldTemplate?.type === 'MainModelField'
) {
return (
<MainModelInputField
nodeId={nodeId}
field={field}
fieldTemplate={fieldTemplate}
/>
);
}
if (
field?.type === 'SDXLRefinerModelField' &&
fieldTemplate?.type === 'SDXLRefinerModelField'
) {
return (
<RefinerModelInputField
nodeData={nodeData}
nodeTemplate={nodeTemplate}
field={field}
fieldTemplate={fieldTemplate}
/>
);
}
if (type === 'VaeModelField' && fieldTemplate.type === 'VaeModelField') {
return (
<VaeModelInputField
nodeData={nodeData}
nodeTemplate={nodeTemplate}
field={field}
fieldTemplate={fieldTemplate}
/>
);
}
if (type === 'LoRAModelField' && fieldTemplate.type === 'LoRAModelField') {
return (
<LoRAModelInputField
nodeData={nodeData}
nodeTemplate={nodeTemplate}
nodeId={nodeId}
field={field}
fieldTemplate={fieldTemplate}
/>
@ -215,57 +189,48 @@ const InputFieldRenderer = (props: InputFieldProps) => {
}
if (
type === 'ControlNetModelField' &&
fieldTemplate.type === 'ControlNetModelField'
field?.type === 'VaeModelField' &&
fieldTemplate?.type === 'VaeModelField'
) {
return (
<VaeModelInputField
nodeId={nodeId}
field={field}
fieldTemplate={fieldTemplate}
/>
);
}
if (
field?.type === 'LoRAModelField' &&
fieldTemplate?.type === 'LoRAModelField'
) {
return (
<LoRAModelInputField
nodeId={nodeId}
field={field}
fieldTemplate={fieldTemplate}
/>
);
}
if (
field?.type === 'ControlNetModelField' &&
fieldTemplate?.type === 'ControlNetModelField'
) {
return (
<ControlNetModelInputField
nodeData={nodeData}
nodeTemplate={nodeTemplate}
nodeId={nodeId}
field={field}
fieldTemplate={fieldTemplate}
/>
);
}
if (type === 'Collection' && fieldTemplate.type === 'Collection') {
if (field?.type === 'Collection' && fieldTemplate?.type === 'Collection') {
return (
<CollectionInputField
nodeData={nodeData}
nodeTemplate={nodeTemplate}
field={field}
fieldTemplate={fieldTemplate}
/>
);
}
if (type === 'CollectionItem' && fieldTemplate.type === 'CollectionItem') {
return (
<CollectionItemInputField
nodeData={nodeData}
nodeTemplate={nodeTemplate}
field={field}
fieldTemplate={fieldTemplate}
/>
);
}
if (type === 'ColorField' && fieldTemplate.type === 'ColorField') {
return (
<ColorInputField
nodeData={nodeData}
nodeTemplate={nodeTemplate}
field={field}
fieldTemplate={fieldTemplate}
/>
);
}
if (type === 'ImageCollection' && fieldTemplate.type === 'ImageCollection') {
return (
<ImageCollectionInputField
nodeData={nodeData}
nodeTemplate={nodeTemplate}
nodeId={nodeId}
field={field}
fieldTemplate={fieldTemplate}
/>
@ -273,20 +238,55 @@ const InputFieldRenderer = (props: InputFieldProps) => {
}
if (
type === 'SDXLMainModelField' &&
fieldTemplate.type === 'SDXLMainModelField'
field?.type === 'CollectionItem' &&
fieldTemplate?.type === 'CollectionItem'
) {
return (
<SDXLMainModelInputField
nodeData={nodeData}
nodeTemplate={nodeTemplate}
<CollectionItemInputField
nodeId={nodeId}
field={field}
fieldTemplate={fieldTemplate}
/>
);
}
return <Box p={2}>Unknown field type: {type}</Box>;
if (field?.type === 'ColorField' && fieldTemplate?.type === 'ColorField') {
return (
<ColorInputField
nodeId={nodeId}
field={field}
fieldTemplate={fieldTemplate}
/>
);
}
if (
field?.type === 'ImageCollection' &&
fieldTemplate?.type === 'ImageCollection'
) {
return (
<ImageCollectionInputField
nodeId={nodeId}
field={field}
fieldTemplate={fieldTemplate}
/>
);
}
if (
field?.type === 'SDXLMainModelField' &&
fieldTemplate?.type === 'SDXLMainModelField'
) {
return (
<SDXLMainModelInputField
nodeId={nodeId}
field={field}
fieldTemplate={fieldTemplate}
/>
);
}
return <Box p={2}>Unknown field type: {field?.type}</Box>;
};
export default memo(InputFieldRenderer);

View File

@ -1,39 +1,16 @@
import { Flex, FormControl, FormLabel, Tooltip } from '@chakra-ui/react';
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
import {
InputFieldTemplate,
InputFieldValue,
InvocationNodeData,
InvocationTemplate,
} from 'features/nodes/types/types';
import { memo } from 'react';
import FieldTitle from './FieldTitle';
import FieldTooltipContent from './FieldTooltipContent';
import InputFieldRenderer from './InputFieldRenderer';
type Props = {
nodeData: InvocationNodeData;
nodeTemplate: InvocationTemplate;
field: InputFieldValue;
fieldTemplate: InputFieldTemplate;
nodeId: string;
fieldName: string;
};
const LinearViewField = ({
nodeData,
nodeTemplate,
field,
fieldTemplate,
}: Props) => {
// const dispatch = useAppDispatch();
// const handleRemoveField = useCallback(() => {
// dispatch(
// workflowExposedFieldRemoved({
// nodeId: nodeData.id,
// fieldName: field.name,
// })
// );
// }, [dispatch, field.name, nodeData.id]);
const LinearViewField = ({ nodeId, fieldName }: Props) => {
return (
<Flex
layerStyle="second"
@ -48,10 +25,9 @@ const LinearViewField = ({
<Tooltip
label={
<FieldTooltipContent
nodeData={nodeData}
nodeTemplate={nodeTemplate}
field={field}
fieldTemplate={fieldTemplate}
nodeId={nodeId}
fieldName={fieldName}
kind="input"
/>
}
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
@ -66,20 +42,10 @@ const LinearViewField = ({
mb: 0,
}}
>
<FieldTitle
nodeData={nodeData}
nodeTemplate={nodeTemplate}
field={field}
fieldTemplate={fieldTemplate}
/>
<FieldTitle nodeId={nodeId} fieldName={fieldName} kind="input" />
</FormLabel>
</Tooltip>
<InputFieldRenderer
nodeData={nodeData}
nodeTemplate={nodeTemplate}
field={field}
fieldTemplate={fieldTemplate}
/>
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
</FormControl>
</Flex>
);

View File

@ -6,25 +6,19 @@ import {
Tooltip,
} from '@chakra-ui/react';
import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
import { useFieldTemplate } from 'features/nodes/hooks/useNodeData';
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
import {
InvocationNodeData,
InvocationTemplate,
OutputFieldValue,
} from 'features/nodes/types/types';
import { PropsWithChildren, useMemo } from 'react';
import { NodeProps } from 'reactflow';
import { PropsWithChildren, memo } from 'react';
import FieldHandle from './FieldHandle';
import FieldTooltipContent from './FieldTooltipContent';
interface Props {
nodeProps: NodeProps<InvocationNodeData>;
nodeTemplate: InvocationTemplate;
field: OutputFieldValue;
nodeId: string;
fieldName: string;
}
const OutputField = (props: Props) => {
const { nodeTemplate, nodeProps, field } = props;
const OutputField = ({ nodeId, fieldName }: Props) => {
const fieldTemplate = useFieldTemplate(nodeId, fieldName, 'output');
const {
isConnected,
@ -32,20 +26,15 @@ const OutputField = (props: Props) => {
isConnectionStartField,
connectionError,
shouldDim,
} = useConnectionState({ nodeId: nodeProps.data.id, field, kind: 'output' });
} = useConnectionState({ nodeId, fieldName, kind: 'output' });
const fieldTemplate = useMemo(
() => nodeTemplate.outputs[field.name],
[field.name, nodeTemplate]
);
if (!fieldTemplate) {
if (fieldTemplate?.fieldKind !== 'output') {
return (
<OutputFieldWrapper shouldDim={shouldDim}>
<FormControl
sx={{ color: 'error.400', textAlign: 'right', fontSize: 'sm' }}
>
Unknown output: {field.name}
Unknown output: {fieldName}
</FormControl>
</OutputFieldWrapper>
);
@ -57,10 +46,9 @@ const OutputField = (props: Props) => {
<Tooltip
label={
<FieldTooltipContent
nodeData={nodeProps.data}
nodeTemplate={nodeTemplate}
field={field}
fieldTemplate={fieldTemplate}
nodeId={nodeId}
fieldName={fieldName}
kind="output"
/>
}
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
@ -75,9 +63,6 @@ const OutputField = (props: Props) => {
</FormControl>
</Tooltip>
<FieldHandle
nodeProps={nodeProps}
nodeTemplate={nodeTemplate}
field={field}
fieldTemplate={fieldTemplate}
handleType="source"
isConnectionInProgress={isConnectionInProgress}
@ -88,27 +73,28 @@ const OutputField = (props: Props) => {
);
};
export default OutputField;
export default memo(OutputField);
type OutputFieldWrapperProps = PropsWithChildren<{
shouldDim: boolean;
}>;
const OutputFieldWrapper = ({
shouldDim,
children,
}: OutputFieldWrapperProps) => (
<Flex
sx={{
position: 'relative',
minH: 8,
py: 0.5,
alignItems: 'center',
opacity: shouldDim ? 0.5 : 1,
transitionProperty: 'opacity',
transitionDuration: '0.1s',
}}
>
{children}
</Flex>
const OutputFieldWrapper = memo(
({ shouldDim, children }: OutputFieldWrapperProps) => (
<Flex
sx={{
position: 'relative',
minH: 8,
py: 0.5,
alignItems: 'center',
opacity: shouldDim ? 0.5 : 1,
transitionProperty: 'opacity',
transitionDuration: '0.1s',
}}
>
{children}
</Flex>
)
);
OutputFieldWrapper.displayName = 'OutputFieldWrapper';

View File

@ -11,8 +11,7 @@ import { FieldComponentProps } from './types';
const BooleanInputFieldComponent = (
props: FieldComponentProps<BooleanInputFieldValue, BooleanInputFieldTemplate>
) => {
const { nodeData, field } = props;
const nodeId = nodeData.id;
const { nodeId, field } = props;
const dispatch = useAppDispatch();

View File

@ -11,8 +11,7 @@ import { FieldComponentProps } from './types';
const ColorInputFieldComponent = (
props: FieldComponentProps<ColorInputFieldValue, ColorInputFieldTemplate>
) => {
const { nodeData, field } = props;
const nodeId = nodeData.id;
const { nodeId, field } = props;
const dispatch = useAppDispatch();

View File

@ -19,8 +19,7 @@ const ControlNetModelInputFieldComponent = (
ControlNetModelInputFieldTemplate
>
) => {
const { nodeData, field } = props;
const nodeId = nodeData.id;
const { nodeId, field } = props;
const controlNetModel = field.value;
const dispatch = useAppDispatch();

View File

@ -11,8 +11,7 @@ import { FieldComponentProps } from './types';
const EnumInputFieldComponent = (
props: FieldComponentProps<EnumInputFieldValue, EnumInputFieldTemplate>
) => {
const { nodeData, field, fieldTemplate } = props;
const nodeId = nodeData.id;
const { nodeId, field, fieldTemplate } = props;
const dispatch = useAppDispatch();

View File

@ -19,8 +19,7 @@ const ImageCollectionInputFieldComponent = (
ImageCollectionInputFieldTemplate
>
) => {
const { nodeData, field } = props;
const nodeId = nodeData.id;
const { nodeId, field } = props;
// const dispatch = useAppDispatch();

View File

@ -21,8 +21,7 @@ import { FieldComponentProps } from './types';
const ImageInputFieldComponent = (
props: FieldComponentProps<ImageInputFieldValue, ImageInputFieldTemplate>
) => {
const { nodeData, field } = props;
const nodeId = nodeData.id;
const { nodeId, field } = props;
const dispatch = useAppDispatch();
const { currentData: imageDTO } = useGetImageDTOQuery(

View File

@ -21,8 +21,7 @@ const LoRAModelInputFieldComponent = (
LoRAModelInputFieldTemplate
>
) => {
const { nodeData, field } = props;
const nodeId = nodeData.id;
const { nodeId, field } = props;
const lora = field.value;
const dispatch = useAppDispatch();
const { data: loraModels } = useGetLoRAModelsQuery();

View File

@ -26,8 +26,7 @@ const MainModelInputFieldComponent = (
MainModelInputFieldTemplate
>
) => {
const { nodeData, field } = props;
const nodeId = nodeData.id;
const { nodeId, field } = props;
const dispatch = useAppDispatch();
const isSyncModelEnabled = useFeatureStatus('syncModels').isFeatureEnabled;

View File

@ -23,8 +23,7 @@ const NumberInputFieldComponent = (
IntegerInputFieldTemplate | FloatInputFieldTemplate
>
) => {
const { nodeData, field, fieldTemplate } = props;
const nodeId = nodeData.id;
const { nodeId, field, fieldTemplate } = props;
const dispatch = useAppDispatch();
const [valueAsString, setValueAsString] = useState<string>(
String(field.value)

View File

@ -24,8 +24,7 @@ const RefinerModelInputFieldComponent = (
SDXLRefinerModelInputFieldTemplate
>
) => {
const { nodeData, field } = props;
const nodeId = nodeData.id;
const { nodeId, field } = props;
const dispatch = useAppDispatch();
const { t } = useTranslation();
const isSyncModelEnabled = useFeatureStatus('syncModels').isFeatureEnabled;

View File

@ -27,8 +27,7 @@ const ModelInputFieldComponent = (
SDXLMainModelInputFieldTemplate
>
) => {
const { nodeData, field } = props;
const nodeId = nodeData.id;
const { nodeId, field } = props;
const dispatch = useAppDispatch();
const { t } = useTranslation();
const isSyncModelEnabled = useFeatureStatus('syncModels').isFeatureEnabled;

View File

@ -12,8 +12,7 @@ import { FieldComponentProps } from './types';
const StringInputFieldComponent = (
props: FieldComponentProps<StringInputFieldValue, StringInputFieldTemplate>
) => {
const { nodeData, field, fieldTemplate } = props;
const nodeId = nodeData.id;
const { nodeId, field, fieldTemplate } = props;
const dispatch = useAppDispatch();
const handleValueChanged = useCallback(

View File

@ -20,8 +20,7 @@ const VaeModelInputFieldComponent = (
VaeModelInputFieldTemplate
>
) => {
const { nodeData, field } = props;
const nodeId = nodeData.id;
const { nodeId, field } = props;
const vae = field.value;
const dispatch = useAppDispatch();
const { data: vaeModels } = useGetVaeModelsQuery();

View File

@ -1,16 +1,13 @@
import {
InputFieldTemplate,
InputFieldValue,
InvocationNodeData,
InvocationTemplate,
} from 'features/nodes/types/types';
export type FieldComponentProps<
V extends InputFieldValue,
T extends InputFieldTemplate
> = {
nodeData: InvocationNodeData;
nodeTemplate: InvocationTemplate;
nodeId: string;
field: V;
fieldTemplate: T;
};

View File

@ -55,7 +55,11 @@ const CurrentImageNode = (props: NodeProps) => {
export default memo(CurrentImageNode);
const Wrapper = (props: PropsWithChildren<{ nodeProps: NodeProps }>) => (
<NodeWrapper nodeProps={props.nodeProps} width={384}>
<NodeWrapper
nodeId={props.nodeProps.data.id}
selected={props.nodeProps.selected}
width={384}
>
<Flex
className={DRAG_HANDLE_CLASSNAME}
sx={{

View File

@ -1,5 +1,6 @@
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { makeTemplateSelector } from 'features/nodes/store/util/makeTemplateSelector';
import { InvocationNodeData } from 'features/nodes/types/types';
import { memo, useMemo } from 'react';
import { NodeProps } from 'reactflow';
@ -7,18 +8,40 @@ import InvocationNode from '../Invocation/InvocationNode';
import UnknownNodeFallback from '../Invocation/UnknownNodeFallback';
const InvocationNodeWrapper = (props: NodeProps<InvocationNodeData>) => {
const { data } = props;
const { type } = data;
const { data, selected } = props;
const { id: nodeId, type, isOpen, label } = data;
const templateSelector = useMemo(() => makeTemplateSelector(type), [type]);
const hasTemplateSelector = useMemo(
() =>
createSelector(stateSelector, ({ nodes }) =>
Boolean(nodes.nodeTemplates[type])
),
[type]
);
const nodeTemplate = useAppSelector(templateSelector);
const nodeTemplate = useAppSelector(hasTemplateSelector);
if (!nodeTemplate) {
return <UnknownNodeFallback nodeProps={props} />;
return (
<UnknownNodeFallback
nodeId={nodeId}
isOpen={isOpen}
label={label}
type={type}
selected={selected}
/>
);
}
return <InvocationNode nodeProps={props} nodeTemplate={nodeTemplate} />;
return (
<InvocationNode
nodeId={nodeId}
isOpen={isOpen}
label={label}
type={type}
selected={selected}
/>
);
};
export default memo(InvocationNodeWrapper);

View File

@ -10,7 +10,7 @@ import NodeTitle from '../Invocation/NodeTitle';
import NodeWrapper from '../Invocation/NodeWrapper';
const NotesNode = (props: NodeProps<NotesNodeData>) => {
const { id: nodeId, data } = props;
const { id: nodeId, data, selected } = props;
const { notes, isOpen } = data;
const dispatch = useAppDispatch();
const handleChange = useCallback(
@ -21,7 +21,7 @@ const NotesNode = (props: NodeProps<NotesNodeData>) => {
);
return (
<NodeWrapper nodeProps={props}>
<NodeWrapper nodeId={nodeId} selected={selected}>
<Flex
layerStyle="nodeHeader"
sx={{
@ -32,8 +32,8 @@ const NotesNode = (props: NodeProps<NotesNodeData>) => {
h: 8,
}}
>
<NodeCollapseButton nodeProps={props} />
<NodeTitle nodeData={props.data} title="Notes" />
<NodeCollapseButton nodeId={nodeId} isOpen={isOpen} />
<NodeTitle nodeId={nodeId} title="Notes" />
<Box minW={8} />
</Flex>
{isOpen && (

View File

@ -6,39 +6,11 @@ import {
TabPanels,
Tabs,
} 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 { IAINoContentFallback } from 'common/components/IAIImageFallback';
import ImageMetadataJSON from 'features/gallery/components/ImageMetadataViewer/ImageMetadataJSON';
import { memo } from 'react';
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
);
import NodeDataInspector from './NodeDataInspector';
import NodeTemplateInspector from './NodeTemplateInspector';
const InspectorPanel = () => {
const { node, template } = useAppSelector(selector);
return (
<Flex
layerStyle="first"
@ -60,37 +32,10 @@ const InspectorPanel = () => {
<TabPanels>
<TabPanel>
{template ? (
<Flex
sx={{
flexDir: 'column',
alignItems: 'flex-start',
gap: 2,
h: 'full',
}}
>
<ImageMetadataJSON
jsonObject={template}
label="Node Template"
/>
</Flex>
) : (
<IAINoContentFallback
label={
node
? 'No template found for selected node'
: 'No node selected'
}
icon={null}
/>
)}
<NodeTemplateInspector />
</TabPanel>
<TabPanel>
{node ? (
<ImageMetadataJSON jsonObject={node.data} label="Node Data" />
) : (
<IAINoContentFallback label="No node selected" icon={null} />
)}
<NodeDataInspector />
</TabPanel>
</TabPanels>
</Tabs>

View File

@ -17,20 +17,20 @@ const selector = createSelector(
);
return {
node: lastSelectedNode,
data: lastSelectedNode?.data,
};
},
defaultSelectorOptions
);
const NodeDataInspector = () => {
const { node } = useAppSelector(selector);
const { data } = useAppSelector(selector);
return node ? (
<ImageMetadataJSON jsonObject={node.data} label="Node Data" />
) : (
<IAINoContentFallback label="No node data" icon={null} />
);
if (!data) {
return <IAINoContentFallback label="No node selected" icon={null} />;
}
return <ImageMetadataJSON jsonObject={data} label="Node Data" />;
};
export default memo(NodeDataInspector);

View File

@ -0,0 +1,40 @@
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 { IAINoContentFallback } from 'common/components/IAIImageFallback';
import ImageMetadataJSON from 'features/gallery/components/ImageMetadataViewer/ImageMetadataJSON';
import { memo } from 'react';
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 {
template: lastSelectedNodeTemplate,
};
},
defaultSelectorOptions
);
const NodeTemplateInspector = () => {
const { template } = useAppSelector(selector);
if (!template) {
return <IAINoContentFallback label="No node selected" icon={null} />;
}
return <ImageMetadataJSON jsonObject={template} label="Node Template" />;
};
export default memo(NodeTemplateInspector);

View File

@ -6,14 +6,6 @@ import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIDroppable from 'common/components/IAIDroppable';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { AddFieldToLinearViewDropData } from 'features/dnd/types';
import {
InputFieldTemplate,
InputFieldValue,
InvocationNodeData,
InvocationTemplate,
isInvocationNode,
} from 'features/nodes/types/types';
import { forEach } from 'lodash-es';
import { memo } from 'react';
import LinearViewField from '../../fields/LinearViewField';
import ScrollableContent from '../ScrollableContent';
@ -21,41 +13,8 @@ import ScrollableContent from '../ScrollableContent';
const selector = createSelector(
stateSelector,
({ nodes }) => {
const fields: {
nodeData: InvocationNodeData;
nodeTemplate: InvocationTemplate;
field: InputFieldValue;
fieldTemplate: InputFieldTemplate;
}[] = [];
const { exposedFields } = nodes.workflow;
nodes.nodes.filter(isInvocationNode).forEach((node) => {
const nodeTemplate = nodes.nodeTemplates[node.data.type];
if (!nodeTemplate) {
return;
}
forEach(node.data.inputs, (field) => {
if (
!exposedFields.some(
(f) => f.nodeId === node.id && f.fieldName === field.name
)
) {
return;
}
const fieldTemplate = nodeTemplate.inputs[field.name];
if (!fieldTemplate) {
return;
}
fields.push({
nodeData: node.data,
nodeTemplate,
field,
fieldTemplate,
});
});
});
return {
fields,
fields: nodes.workflow.exposedFields,
};
},
defaultSelectorOptions
@ -89,13 +48,11 @@ const LinearTabContent = () => {
}}
>
{fields.length ? (
fields.map(({ nodeData, nodeTemplate, field, fieldTemplate }) => (
fields.map(({ nodeId, fieldName }) => (
<LinearViewField
key={field.id}
nodeData={nodeData}
nodeTemplate={nodeTemplate}
field={field}
fieldTemplate={fieldTemplate}
key={`${nodeId}-${fieldName}`}
nodeId={nodeId}
fieldName={fieldName}
/>
))
) : (

View File

@ -2,8 +2,8 @@ import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { makeConnectionErrorSelector } from 'features/nodes/store/util/makeIsConnectionValidSelector';
import { InputFieldValue, OutputFieldValue } from 'features/nodes/types/types';
import { useMemo } from 'react';
import { useFieldType } from './useNodeData';
const selectIsConnectionInProgress = createSelector(
stateSelector,
@ -12,23 +12,19 @@ const selectIsConnectionInProgress = createSelector(
nodes.connectionStartParams !== null
);
export type UseConnectionStateProps =
| {
nodeId: string;
field: InputFieldValue;
kind: 'input';
}
| {
nodeId: string;
field: OutputFieldValue;
kind: 'output';
};
export type UseConnectionStateProps = {
nodeId: string;
fieldName: string;
kind: 'input' | 'output';
};
export const useConnectionState = ({
nodeId,
field,
fieldName,
kind,
}: UseConnectionStateProps) => {
const fieldType = useFieldType(nodeId, fieldName, kind);
const selectIsConnected = useMemo(
() =>
createSelector(stateSelector, ({ nodes }) =>
@ -37,23 +33,23 @@ export const useConnectionState = ({
return (
(kind === 'input' ? edge.target : edge.source) === nodeId &&
(kind === 'input' ? edge.targetHandle : edge.sourceHandle) ===
field.name
fieldName
);
}).length
)
),
[field.name, kind, nodeId]
[fieldName, kind, nodeId]
);
const selectConnectionError = useMemo(
() =>
makeConnectionErrorSelector(
nodeId,
field.name,
fieldName,
kind === 'input' ? 'target' : 'source',
field.type
fieldType
),
[nodeId, field.name, field.type, kind]
[nodeId, fieldName, kind, fieldType]
);
const selectIsConnectionStartField = useMemo(
@ -61,12 +57,12 @@ export const useConnectionState = ({
createSelector(stateSelector, ({ nodes }) =>
Boolean(
nodes.connectionStartParams?.nodeId === nodeId &&
nodes.connectionStartParams?.handleId === field.name &&
nodes.connectionStartParams?.handleId === fieldName &&
nodes.connectionStartParams?.handleType ===
{ input: 'target', output: 'source' }[kind]
)
),
[field.name, kind, nodeId]
[fieldName, kind, nodeId]
);
const isConnected = useAppSelector(selectIsConnected);

View File

@ -0,0 +1,289 @@
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 { map, some } from 'lodash-es';
import { useMemo } from 'react';
import {
FOOTER_FIELDS,
IMAGE_FIELDS,
} from '../components/Invocation/NodeFooter';
import { isInvocationNode } from '../types/types';
const KIND_MAP = {
input: 'inputs' as const,
output: 'outputs' as const,
};
export const useNodeTemplate = (nodeId: string) => {
const selector = useMemo(
() =>
createSelector(
stateSelector,
({ nodes }) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
const nodeTemplate = nodes.nodeTemplates[node?.data.type ?? ''];
return nodeTemplate;
},
defaultSelectorOptions
),
[nodeId]
);
const nodeTemplate = useAppSelector(selector);
return nodeTemplate;
};
export const useNodeData = (nodeId: string) => {
const selector = useMemo(
() =>
createSelector(
stateSelector,
({ nodes }) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
return node?.data;
},
defaultSelectorOptions
),
[nodeId]
);
const nodeData = useAppSelector(selector);
return nodeData;
};
export const useFieldData = (nodeId: string, fieldName: string) => {
const selector = useMemo(
() =>
createSelector(
stateSelector,
({ nodes }) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return;
}
return node?.data.inputs[fieldName];
},
defaultSelectorOptions
),
[fieldName, nodeId]
);
const fieldData = useAppSelector(selector);
return fieldData;
};
export const useFieldType = (
nodeId: string,
fieldName: string,
kind: 'input' | 'output'
) => {
const selector = useMemo(
() =>
createSelector(
stateSelector,
({ nodes }) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return;
}
return node?.data[KIND_MAP[kind]][fieldName]?.type;
},
defaultSelectorOptions
),
[fieldName, kind, nodeId]
);
const fieldType = useAppSelector(selector);
return fieldType;
};
export const useFieldNames = (nodeId: string, kind: 'input' | 'output') => {
const selector = useMemo(
() =>
createSelector(
stateSelector,
({ nodes }) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return [];
}
return map(node.data[KIND_MAP[kind]], (field) => field.name).filter(
(fieldName) => fieldName !== 'is_intermediate'
);
},
defaultSelectorOptions
),
[kind, nodeId]
);
const fieldNames = useAppSelector(selector);
return fieldNames;
};
export const useWithFooter = (nodeId: string) => {
const selector = useMemo(
() =>
createSelector(
stateSelector,
({ nodes }) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return false;
}
return some(node.data.outputs, (output) =>
FOOTER_FIELDS.includes(output.type)
);
},
defaultSelectorOptions
),
[nodeId]
);
const withFooter = useAppSelector(selector);
return withFooter;
};
export const useHasImageOutput = (nodeId: string) => {
const selector = useMemo(
() =>
createSelector(
stateSelector,
({ nodes }) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return false;
}
return some(node.data.outputs, (output) =>
IMAGE_FIELDS.includes(output.type)
);
},
defaultSelectorOptions
),
[nodeId]
);
const hasImageOutput = useAppSelector(selector);
return hasImageOutput;
};
export const useIsIntermediate = (nodeId: string) => {
const selector = useMemo(
() =>
createSelector(
stateSelector,
({ nodes }) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return false;
}
return Boolean(node.data.inputs.is_intermediate?.value);
},
defaultSelectorOptions
),
[nodeId]
);
const is_intermediate = useAppSelector(selector);
return is_intermediate;
};
export const useNodeLabel = (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.label;
},
defaultSelectorOptions
),
[nodeId]
);
const label = useAppSelector(selector);
return label;
};
export const useNodeTemplateTitle = (nodeId: string) => {
const selector = useMemo(
() =>
createSelector(
stateSelector,
({ nodes }) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return false;
}
const nodeTemplate = node
? nodes.nodeTemplates[node.data.type]
: undefined;
return nodeTemplate?.title;
},
defaultSelectorOptions
),
[nodeId]
);
const title = useAppSelector(selector);
return title;
};
export const useFieldTemplate = (
nodeId: string,
fieldName: string,
kind: 'input' | 'output'
) => {
const selector = useMemo(
() =>
createSelector(
stateSelector,
({ nodes }) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return;
}
const nodeTemplate = nodes.nodeTemplates[node?.data.type ?? ''];
return nodeTemplate?.[KIND_MAP[kind]][fieldName];
},
defaultSelectorOptions
),
[fieldName, kind, nodeId]
);
const fieldTemplate = useAppSelector(selector);
return fieldTemplate;
};
export const useDoesInputHaveValue = (nodeId: string, fieldName: string) => {
const selector = useMemo(
() =>
createSelector(
stateSelector,
({ nodes }) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return;
}
return Boolean(node?.data.inputs[fieldName]?.value);
},
defaultSelectorOptions
),
[fieldName, nodeId]
);
const doesFieldHaveValue = useAppSelector(selector);
return doesFieldHaveValue;
};

View File

@ -9,9 +9,13 @@ export const makeConnectionErrorSelector = (
nodeId: string,
fieldName: string,
handleType: HandleType,
fieldType: FieldType
fieldType?: FieldType
) =>
createSelector(stateSelector, (state) => {
if (!fieldType) {
return 'No field type';
}
const { currentConnectionFieldType, connectionStartParams, nodes, edges } =
state.nodes;

View File

@ -457,12 +457,13 @@ export type ColorInputFieldTemplate = InputFieldTemplateBase & {
};
export const isInputFieldValue = (
field: InputFieldValue | OutputFieldValue
): field is InputFieldValue => field.fieldKind === 'input';
field?: InputFieldValue | OutputFieldValue
): field is InputFieldValue => Boolean(field && field.fieldKind === 'input');
export const isInputFieldTemplate = (
fieldTemplate: InputFieldTemplate | OutputFieldTemplate
): fieldTemplate is InputFieldTemplate => fieldTemplate.fieldKind === 'input';
fieldTemplate?: InputFieldTemplate | OutputFieldTemplate
): fieldTemplate is InputFieldTemplate =>
Boolean(fieldTemplate && fieldTemplate.fieldKind === 'input');
/**
* JANKY CUSTOMISATION OF OpenAPI SCHEMA TYPES
@ -632,20 +633,22 @@ export type NodeData =
export const isInvocationNode = (
node?: Node<NodeData>
): node is Node<InvocationNodeData> => node?.type === 'invocation';
): node is Node<InvocationNodeData> =>
Boolean(node && node.type === 'invocation');
export const isInvocationNodeData = (
node?: NodeData
): node is InvocationNodeData =>
!['notes', 'current_image'].includes(node?.type ?? '');
Boolean(node && !['notes', 'current_image'].includes(node.type));
export const isNotesNode = (
node?: Node<NodeData>
): node is Node<NotesNodeData> => node?.type === 'notes';
): node is Node<NotesNodeData> => Boolean(node && node.type === 'notes');
export const isProgressImageNode = (
node?: Node<NodeData>
): node is Node<CurrentImageNodeData> => node?.type === 'current_image';
): node is Node<CurrentImageNodeData> =>
Boolean(node && node.type === 'current_image');
export enum NodeStatus {
PENDING,