mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
fix(ui): crash when using notes nodes or missing node/field templates (#6412)
## Summary Notes nodes used some overly-strict redux selectors. The selectors are now more chill. Also fixed an issue where you couldn't edit a notes node title. Found another class of error related to the overly strict reducers that caused errors when loading a workflow that had missing templates. Fixed this with fallback wrapper component, works like an error boundary when a template isn't found. ## Related Issues / Discussions https://discord.com/channels/1020123559063990373/1149506274971631688/1242256425527545949 ## QA Instructions - Add a notes node to a workflow. Edit the notes title. - Load a workflow that has nodes that aren't installed. Should get a fallback UI for each missing node. - Load a workflow that references a node with different inputs than are in the template - like an old version of a node. Should get a fallback field warning for both missing templates, or missing inputs. ## Merge Plan n/a ## Checklist - [x] _The PR has a short but descriptive title, suitable for a changelog_ - [ ] _Tests added / updated (if applicable)_ - [ ] _Documentation added / updated (if applicable)_
This commit is contained in:
commit
aa0c59bb51
@ -1,7 +1,7 @@
|
|||||||
import { Flex, Grid, GridItem } from '@invoke-ai/ui-library';
|
import { Flex, Grid, GridItem } from '@invoke-ai/ui-library';
|
||||||
import NodeWrapper from 'features/nodes/components/flow/nodes/common/NodeWrapper';
|
import NodeWrapper from 'features/nodes/components/flow/nodes/common/NodeWrapper';
|
||||||
import { useAnyOrDirectInputFieldNames } from 'features/nodes/hooks/useAnyOrDirectInputFieldNames';
|
import { InvocationInputFieldCheck } from 'features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck';
|
||||||
import { useConnectionInputFieldNames } from 'features/nodes/hooks/useConnectionInputFieldNames';
|
import { useFieldNames } from 'features/nodes/hooks/useFieldNames';
|
||||||
import { useOutputFieldNames } from 'features/nodes/hooks/useOutputFieldNames';
|
import { useOutputFieldNames } from 'features/nodes/hooks/useOutputFieldNames';
|
||||||
import { useWithFooter } from 'features/nodes/hooks/useWithFooter';
|
import { useWithFooter } from 'features/nodes/hooks/useWithFooter';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
@ -20,8 +20,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
|
const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
|
||||||
const inputConnectionFieldNames = useConnectionInputFieldNames(nodeId);
|
const fieldNames = useFieldNames(nodeId);
|
||||||
const inputAnyOrDirectFieldNames = useAnyOrDirectInputFieldNames(nodeId);
|
|
||||||
const withFooter = useWithFooter(nodeId);
|
const withFooter = useWithFooter(nodeId);
|
||||||
const outputFieldNames = useOutputFieldNames(nodeId);
|
const outputFieldNames = useOutputFieldNames(nodeId);
|
||||||
|
|
||||||
@ -41,9 +40,11 @@ const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
|
|||||||
>
|
>
|
||||||
<Flex flexDir="column" px={2} w="full" h="full">
|
<Flex flexDir="column" px={2} w="full" h="full">
|
||||||
<Grid gridTemplateColumns="1fr auto" gridAutoRows="1fr">
|
<Grid gridTemplateColumns="1fr auto" gridAutoRows="1fr">
|
||||||
{inputConnectionFieldNames.map((fieldName, i) => (
|
{fieldNames.connectionFields.map((fieldName, i) => (
|
||||||
<GridItem gridColumnStart={1} gridRowStart={i + 1} key={`${nodeId}.${fieldName}.input-field`}>
|
<GridItem gridColumnStart={1} gridRowStart={i + 1} key={`${nodeId}.${fieldName}.input-field`}>
|
||||||
<InputField nodeId={nodeId} fieldName={fieldName} />
|
<InvocationInputFieldCheck nodeId={nodeId} fieldName={fieldName}>
|
||||||
|
<InputField nodeId={nodeId} fieldName={fieldName} />
|
||||||
|
</InvocationInputFieldCheck>
|
||||||
</GridItem>
|
</GridItem>
|
||||||
))}
|
))}
|
||||||
{outputFieldNames.map((fieldName, i) => (
|
{outputFieldNames.map((fieldName, i) => (
|
||||||
@ -52,8 +53,23 @@ const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
|
|||||||
</GridItem>
|
</GridItem>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
{inputAnyOrDirectFieldNames.map((fieldName) => (
|
{fieldNames.anyOrDirectFields.map((fieldName) => (
|
||||||
<InputField key={`${nodeId}.${fieldName}.input-field`} nodeId={nodeId} fieldName={fieldName} />
|
<InvocationInputFieldCheck
|
||||||
|
key={`${nodeId}.${fieldName}.input-field`}
|
||||||
|
nodeId={nodeId}
|
||||||
|
fieldName={fieldName}
|
||||||
|
>
|
||||||
|
<InputField nodeId={nodeId} fieldName={fieldName} />
|
||||||
|
</InvocationInputFieldCheck>
|
||||||
|
))}
|
||||||
|
{fieldNames.missingFields.map((fieldName) => (
|
||||||
|
<InvocationInputFieldCheck
|
||||||
|
key={`${nodeId}.${fieldName}.input-field`}
|
||||||
|
nodeId={nodeId}
|
||||||
|
fieldName={fieldName}
|
||||||
|
>
|
||||||
|
<InputField nodeId={nodeId} fieldName={fieldName} />
|
||||||
|
</InvocationInputFieldCheck>
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
import { useDoesFieldExist } from 'features/nodes/hooks/useDoesFieldExist';
|
|
||||||
import type { PropsWithChildren } from 'react';
|
|
||||||
import { memo } from 'react';
|
|
||||||
|
|
||||||
type Props = PropsWithChildren<{
|
|
||||||
nodeId: string;
|
|
||||||
fieldName?: string;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export const MissingFallback = memo((props: Props) => {
|
|
||||||
// We must be careful here to avoid race conditions where a deleted node is still referenced as an exposed field
|
|
||||||
const exists = useDoesFieldExist(props.nodeId, props.fieldName);
|
|
||||||
if (!exists) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return props.children;
|
|
||||||
});
|
|
||||||
|
|
||||||
MissingFallback.displayName = 'MissingFallback';
|
|
@ -1,16 +1,14 @@
|
|||||||
import { Flex, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
import { Flex, FormControl } from '@invoke-ai/ui-library';
|
||||||
import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
|
import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
|
||||||
import { useDoesInputHaveValue } from 'features/nodes/hooks/useDoesInputHaveValue';
|
import { useDoesInputHaveValue } from 'features/nodes/hooks/useDoesInputHaveValue';
|
||||||
import { useFieldInputInstance } from 'features/nodes/hooks/useFieldInputInstance';
|
|
||||||
import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate';
|
import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate';
|
||||||
import type { PropsWithChildren } from 'react';
|
|
||||||
import { memo, useCallback, useMemo, useState } from 'react';
|
import { memo, useCallback, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
import EditableFieldTitle from './EditableFieldTitle';
|
import EditableFieldTitle from './EditableFieldTitle';
|
||||||
import FieldHandle from './FieldHandle';
|
import FieldHandle from './FieldHandle';
|
||||||
import FieldLinearViewToggle from './FieldLinearViewToggle';
|
import FieldLinearViewToggle from './FieldLinearViewToggle';
|
||||||
import InputFieldRenderer from './InputFieldRenderer';
|
import InputFieldRenderer from './InputFieldRenderer';
|
||||||
|
import { InputFieldWrapper } from './InputFieldWrapper';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
@ -18,9 +16,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const InputField = ({ nodeId, fieldName }: Props) => {
|
const InputField = ({ nodeId, fieldName }: Props) => {
|
||||||
const { t } = useTranslation();
|
|
||||||
const fieldTemplate = useFieldInputTemplate(nodeId, fieldName);
|
const fieldTemplate = useFieldInputTemplate(nodeId, fieldName);
|
||||||
const fieldInstance = useFieldInputInstance(nodeId, fieldName);
|
|
||||||
const doesFieldHaveValue = useDoesInputHaveValue(nodeId, fieldName);
|
const doesFieldHaveValue = useDoesInputHaveValue(nodeId, fieldName);
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
@ -55,20 +51,6 @@ const InputField = ({ nodeId, fieldName }: Props) => {
|
|||||||
setIsHovered(false);
|
setIsHovered(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!fieldTemplate || !fieldInstance) {
|
|
||||||
return (
|
|
||||||
<InputFieldWrapper shouldDim={shouldDim}>
|
|
||||||
<FormControl alignItems="stretch" justifyContent="space-between" flexDir="column" gap={2} h="full" w="full">
|
|
||||||
<FormLabel display="flex" alignItems="center" mb={0} px={1} gap={2} h="full">
|
|
||||||
{t('nodes.unknownInput', {
|
|
||||||
name: fieldInstance?.label ?? fieldTemplate?.title ?? fieldName,
|
|
||||||
})}
|
|
||||||
</FormLabel>
|
|
||||||
</FormControl>
|
|
||||||
</InputFieldWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fieldTemplate.input === 'connection' || isConnected) {
|
if (fieldTemplate.input === 'connection' || isConnected) {
|
||||||
return (
|
return (
|
||||||
<InputFieldWrapper shouldDim={shouldDim}>
|
<InputFieldWrapper shouldDim={shouldDim}>
|
||||||
@ -134,27 +116,3 @@ const InputField = ({ nodeId, fieldName }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default memo(InputField);
|
export default memo(InputField);
|
||||||
|
|
||||||
type InputFieldWrapperProps = PropsWithChildren<{
|
|
||||||
shouldDim: boolean;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
const InputFieldWrapper = memo(({ shouldDim, children }: InputFieldWrapperProps) => {
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
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';
|
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
|
import type { PropsWithChildren } from 'react';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
type InputFieldWrapperProps = PropsWithChildren<{
|
||||||
|
shouldDim: boolean;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export const InputFieldWrapper = memo(({ shouldDim, children }: InputFieldWrapperProps) => {
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
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';
|
@ -0,0 +1,59 @@
|
|||||||
|
import { Flex, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||||
|
import { useStore } from '@nanostores/react';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
||||||
|
import { selectInvocationNode } from 'features/nodes/store/selectors';
|
||||||
|
import type { PropsWithChildren } from 'react';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
type Props = PropsWithChildren<{
|
||||||
|
nodeId: string;
|
||||||
|
fieldName: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export const InvocationInputFieldCheck = memo(({ nodeId, fieldName, children }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const templates = useStore($templates);
|
||||||
|
const selector = useMemo(
|
||||||
|
() =>
|
||||||
|
createSelector(selectNodesSlice, (nodesSlice) => {
|
||||||
|
const node = selectInvocationNode(nodesSlice, nodeId);
|
||||||
|
const instance = node.data.inputs[fieldName];
|
||||||
|
const template = templates[node.data.type];
|
||||||
|
const fieldTemplate = template?.inputs[fieldName];
|
||||||
|
return {
|
||||||
|
name: instance?.label || fieldTemplate?.title || fieldName,
|
||||||
|
hasInstance: Boolean(instance),
|
||||||
|
hasTemplate: Boolean(fieldTemplate),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
[fieldName, nodeId, templates]
|
||||||
|
);
|
||||||
|
const { hasInstance, hasTemplate, name } = useAppSelector(selector);
|
||||||
|
|
||||||
|
if (!hasTemplate || !hasInstance) {
|
||||||
|
return (
|
||||||
|
<Flex position="relative" minH={8} py={0.5} alignItems="center" w="full" h="full">
|
||||||
|
<FormControl
|
||||||
|
isInvalid={true}
|
||||||
|
alignItems="stretch"
|
||||||
|
justifyContent="center"
|
||||||
|
flexDir="column"
|
||||||
|
gap={2}
|
||||||
|
h="full"
|
||||||
|
w="full"
|
||||||
|
>
|
||||||
|
<FormLabel display="flex" mb={0} px={1} py={2} gap={2}>
|
||||||
|
{t('nodes.unknownInput', { name })}
|
||||||
|
</FormLabel>
|
||||||
|
</FormControl>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
});
|
||||||
|
|
||||||
|
InvocationInputFieldCheck.displayName = 'InvocationInputFieldCheck';
|
@ -3,7 +3,7 @@ import { CSS } from '@dnd-kit/utilities';
|
|||||||
import { Flex, Icon, IconButton, Spacer, Tooltip } from '@invoke-ai/ui-library';
|
import { Flex, Icon, IconButton, Spacer, Tooltip } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import NodeSelectionOverlay from 'common/components/NodeSelectionOverlay';
|
import NodeSelectionOverlay from 'common/components/NodeSelectionOverlay';
|
||||||
import { MissingFallback } from 'features/nodes/components/flow/nodes/Invocation/MissingFallback';
|
import { InvocationInputFieldCheck } from 'features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck';
|
||||||
import { useFieldOriginalValue } from 'features/nodes/hooks/useFieldOriginalValue';
|
import { useFieldOriginalValue } from 'features/nodes/hooks/useFieldOriginalValue';
|
||||||
import { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
|
import { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
|
||||||
import { workflowExposedFieldRemoved } from 'features/nodes/store/workflowSlice';
|
import { workflowExposedFieldRemoved } from 'features/nodes/store/workflowSlice';
|
||||||
@ -102,9 +102,9 @@ const LinearViewFieldInternal = ({ nodeId, fieldName }: Props) => {
|
|||||||
|
|
||||||
const LinearViewField = ({ nodeId, fieldName }: Props) => {
|
const LinearViewField = ({ nodeId, fieldName }: Props) => {
|
||||||
return (
|
return (
|
||||||
<MissingFallback nodeId={nodeId} fieldName={fieldName}>
|
<InvocationInputFieldCheck nodeId={nodeId} fieldName={fieldName}>
|
||||||
<LinearViewFieldInternal nodeId={nodeId} fieldName={fieldName} />
|
<LinearViewFieldInternal nodeId={nodeId} fieldName={fieldName} />
|
||||||
</MissingFallback>
|
</InvocationInputFieldCheck>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ const NotesNode = (props: NodeProps<NotesNodeData>) => {
|
|||||||
gap={1}
|
gap={1}
|
||||||
>
|
>
|
||||||
<Flex className="nopan" w="full" h="full" flexDir="column">
|
<Flex className="nopan" w="full" h="full" flexDir="column">
|
||||||
<Textarea value={notes} onChange={handleChange} rows={8} resize="none" fontSize="sm" />
|
<Textarea className="nodrag" value={notes} onChange={handleChange} rows={8} resize="none" fontSize="sm" />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Flex, FormLabel, Icon, IconButton, Spacer, Tooltip } from '@invoke-ai/ui-library';
|
import { Flex, FormLabel, Icon, IconButton, Spacer, Tooltip } from '@invoke-ai/ui-library';
|
||||||
import FieldTooltipContent from 'features/nodes/components/flow/nodes/Invocation/fields/FieldTooltipContent';
|
import FieldTooltipContent from 'features/nodes/components/flow/nodes/Invocation/fields/FieldTooltipContent';
|
||||||
import InputFieldRenderer from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer';
|
import InputFieldRenderer from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer';
|
||||||
import { MissingFallback } from 'features/nodes/components/flow/nodes/Invocation/MissingFallback';
|
import { InvocationInputFieldCheck } from 'features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck';
|
||||||
import { useFieldLabel } from 'features/nodes/hooks/useFieldLabel';
|
import { useFieldLabel } from 'features/nodes/hooks/useFieldLabel';
|
||||||
import { useFieldOriginalValue } from 'features/nodes/hooks/useFieldOriginalValue';
|
import { useFieldOriginalValue } from 'features/nodes/hooks/useFieldOriginalValue';
|
||||||
import { useFieldTemplateTitle } from 'features/nodes/hooks/useFieldTemplateTitle';
|
import { useFieldTemplateTitle } from 'features/nodes/hooks/useFieldTemplateTitle';
|
||||||
@ -53,9 +53,9 @@ const WorkflowFieldInternal = ({ nodeId, fieldName }: Props) => {
|
|||||||
|
|
||||||
const WorkflowField = ({ nodeId, fieldName }: Props) => {
|
const WorkflowField = ({ nodeId, fieldName }: Props) => {
|
||||||
return (
|
return (
|
||||||
<MissingFallback nodeId={nodeId} fieldName={fieldName}>
|
<InvocationInputFieldCheck nodeId={nodeId} fieldName={fieldName}>
|
||||||
<WorkflowFieldInternal nodeId={nodeId} fieldName={fieldName} />
|
<WorkflowFieldInternal nodeId={nodeId} fieldName={fieldName} />
|
||||||
</MissingFallback>
|
</InvocationInputFieldCheck>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
import { EMPTY_ARRAY } from 'app/store/constants';
|
|
||||||
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
|
|
||||||
import { isSingleOrCollection } from 'features/nodes/types/field';
|
|
||||||
import { getSortedFilteredFieldNames } from 'features/nodes/util/node/getSortedFilteredFieldNames';
|
|
||||||
import { TEMPLATE_BUILDER_MAP } from 'features/nodes/util/schema/buildFieldInputTemplate';
|
|
||||||
import { keys, map } from 'lodash-es';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
export const useAnyOrDirectInputFieldNames = (nodeId: string): string[] => {
|
|
||||||
const template = useNodeTemplate(nodeId);
|
|
||||||
|
|
||||||
const fieldNames = useMemo(() => {
|
|
||||||
const fields = map(template.inputs).filter((field) => {
|
|
||||||
return (
|
|
||||||
(['any', 'direct'].includes(field.input) || isSingleOrCollection(field.type)) &&
|
|
||||||
keys(TEMPLATE_BUILDER_MAP).includes(field.type.name)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
const _fieldNames = getSortedFilteredFieldNames(fields);
|
|
||||||
if (_fieldNames.length === 0) {
|
|
||||||
return EMPTY_ARRAY;
|
|
||||||
}
|
|
||||||
return _fieldNames;
|
|
||||||
}, [template.inputs]);
|
|
||||||
|
|
||||||
return fieldNames;
|
|
||||||
};
|
|
@ -1,29 +0,0 @@
|
|||||||
import { EMPTY_ARRAY } from 'app/store/constants';
|
|
||||||
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
|
|
||||||
import { isSingleOrCollection } from 'features/nodes/types/field';
|
|
||||||
import { getSortedFilteredFieldNames } from 'features/nodes/util/node/getSortedFilteredFieldNames';
|
|
||||||
import { TEMPLATE_BUILDER_MAP } from 'features/nodes/util/schema/buildFieldInputTemplate';
|
|
||||||
import { keys, map } from 'lodash-es';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
export const useConnectionInputFieldNames = (nodeId: string): string[] => {
|
|
||||||
const template = useNodeTemplate(nodeId);
|
|
||||||
const fieldNames = useMemo(() => {
|
|
||||||
// get the visible fields
|
|
||||||
const fields = map(template.inputs).filter(
|
|
||||||
(field) =>
|
|
||||||
(field.input === 'connection' && !isSingleOrCollection(field.type)) ||
|
|
||||||
!keys(TEMPLATE_BUILDER_MAP).includes(field.type.name)
|
|
||||||
);
|
|
||||||
|
|
||||||
const _fieldNames = getSortedFilteredFieldNames(fields);
|
|
||||||
|
|
||||||
if (_fieldNames.length === 0) {
|
|
||||||
return EMPTY_ARRAY;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _fieldNames;
|
|
||||||
}, [template.inputs]);
|
|
||||||
|
|
||||||
return fieldNames;
|
|
||||||
};
|
|
@ -1,20 +0,0 @@
|
|||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
|
||||||
|
|
||||||
export const useDoesFieldExist = (nodeId: string, fieldName?: string) => {
|
|
||||||
const doesFieldExist = useAppSelector((s) => {
|
|
||||||
const node = s.nodes.present.nodes.find((n) => n.id === nodeId);
|
|
||||||
if (!isInvocationNode(node)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (fieldName === undefined) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!node.data.inputs[fieldName]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
return doesFieldExist;
|
|
||||||
};
|
|
@ -1,9 +1,14 @@
|
|||||||
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
|
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
|
||||||
import type { FieldInputTemplate } from 'features/nodes/types/field';
|
import type { FieldInputTemplate } from 'features/nodes/types/field';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
export const useFieldInputTemplate = (nodeId: string, fieldName: string): FieldInputTemplate | null => {
|
export const useFieldInputTemplate = (nodeId: string, fieldName: string): FieldInputTemplate => {
|
||||||
const template = useNodeTemplate(nodeId);
|
const template = useNodeTemplate(nodeId);
|
||||||
const fieldTemplate = useMemo(() => template.inputs[fieldName] ?? null, [fieldName, template.inputs]);
|
const fieldTemplate = useMemo(() => {
|
||||||
|
const _fieldTemplate = template.inputs[fieldName];
|
||||||
|
assert(_fieldTemplate, `Field template for field ${fieldName} not found`);
|
||||||
|
return _fieldTemplate;
|
||||||
|
}, [fieldName, template.inputs]);
|
||||||
return fieldTemplate;
|
return fieldTemplate;
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
import { useNodeData } from 'features/nodes/hooks/useNodeData';
|
||||||
|
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
|
||||||
|
import type { FieldInputTemplate } from 'features/nodes/types/field';
|
||||||
|
import { isSingleOrCollection } from 'features/nodes/types/field';
|
||||||
|
import { TEMPLATE_BUILDER_MAP } from 'features/nodes/util/schema/buildFieldInputTemplate';
|
||||||
|
import { difference, filter, keys } from 'lodash-es';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
const isConnectionInputField = (field: FieldInputTemplate) => {
|
||||||
|
return (
|
||||||
|
(field.input === 'connection' && !isSingleOrCollection(field.type)) ||
|
||||||
|
!keys(TEMPLATE_BUILDER_MAP).includes(field.type.name)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isAnyOrDirectInputField = (field: FieldInputTemplate) => {
|
||||||
|
return (
|
||||||
|
(['any', 'direct'].includes(field.input) || isSingleOrCollection(field.type)) &&
|
||||||
|
keys(TEMPLATE_BUILDER_MAP).includes(field.type.name)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useFieldNames = (nodeId: string) => {
|
||||||
|
const template = useNodeTemplate(nodeId);
|
||||||
|
const node = useNodeData(nodeId);
|
||||||
|
const fieldNames = useMemo(() => {
|
||||||
|
const instanceFields = keys(node.inputs);
|
||||||
|
const allTemplateFields = keys(template.inputs);
|
||||||
|
const missingFields = difference(instanceFields, allTemplateFields);
|
||||||
|
const connectionFields = filter(template.inputs, isConnectionInputField).map((f) => f.name);
|
||||||
|
const anyOrDirectFields = filter(template.inputs, isAnyOrDirectInputField).map((f) => f.name);
|
||||||
|
return {
|
||||||
|
missingFields,
|
||||||
|
connectionFields,
|
||||||
|
anyOrDirectFields,
|
||||||
|
};
|
||||||
|
}, [node.inputs, template.inputs]);
|
||||||
|
return fieldNames;
|
||||||
|
};
|
@ -1,14 +1,14 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
||||||
import { selectNodeData } from 'features/nodes/store/selectors';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
export const useNodeLabel = (nodeId: string) => {
|
export const useNodeLabel = (nodeId: string) => {
|
||||||
const selector = useMemo(
|
const selector = useMemo(
|
||||||
() =>
|
() =>
|
||||||
createSelector(selectNodesSlice, (nodes) => {
|
createSelector(selectNodesSlice, (nodesSlice) => {
|
||||||
return selectNodeData(nodes, nodeId)?.label ?? null;
|
const node = nodesSlice.nodes.find((node) => node.id === nodeId);
|
||||||
|
return node?.data.label;
|
||||||
}),
|
}),
|
||||||
[nodeId]
|
[nodeId]
|
||||||
);
|
);
|
||||||
|
@ -1,8 +1,24 @@
|
|||||||
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
|
import { useStore } from '@nanostores/react';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
||||||
|
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
export const useNodeTemplateTitle = (nodeId: string): string | null => {
|
export const useNodeTemplateTitle = (nodeId: string): string | null => {
|
||||||
const template = useNodeTemplate(nodeId);
|
const templates = useStore($templates);
|
||||||
const title = useMemo(() => template.title, [template.title]);
|
const selector = useMemo(
|
||||||
|
() =>
|
||||||
|
createSelector(selectNodesSlice, (nodesSlice) => {
|
||||||
|
const node = nodesSlice.nodes.find((node) => node.id === nodeId);
|
||||||
|
if (!isInvocationNode(node)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const template = templates[node.data.type];
|
||||||
|
return template?.title ?? null;
|
||||||
|
}),
|
||||||
|
[nodeId, templates]
|
||||||
|
);
|
||||||
|
const title = useAppSelector(selector);
|
||||||
return title;
|
return title;
|
||||||
};
|
};
|
||||||
|
@ -275,10 +275,9 @@ export const nodesSlice = createSlice({
|
|||||||
const { nodeId, label } = action.payload;
|
const { nodeId, label } = action.payload;
|
||||||
const nodeIndex = state.nodes.findIndex((n) => n.id === nodeId);
|
const nodeIndex = state.nodes.findIndex((n) => n.id === nodeId);
|
||||||
const node = state.nodes?.[nodeIndex];
|
const node = state.nodes?.[nodeIndex];
|
||||||
if (!isInvocationNode(node)) {
|
if (isInvocationNode(node) || isNotesNode(node)) {
|
||||||
return;
|
node.data.label = label;
|
||||||
}
|
}
|
||||||
node.data.label = label;
|
|
||||||
},
|
},
|
||||||
nodeNotesChanged: (state, action: PayloadAction<{ nodeId: string; notes: string }>) => {
|
nodeNotesChanged: (state, action: PayloadAction<{ nodeId: string; notes: string }>) => {
|
||||||
const { nodeId, notes } = action.payload;
|
const { nodeId, notes } = action.payload;
|
||||||
|
@ -4,7 +4,7 @@ import type { InvocationNode, InvocationNodeData } from 'features/nodes/types/in
|
|||||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
const selectInvocationNode = (nodesSlice: NodesState, nodeId: string): InvocationNode => {
|
export const selectInvocationNode = (nodesSlice: NodesState, nodeId: string): InvocationNode => {
|
||||||
const node = nodesSlice.nodes.find((node) => node.id === nodeId);
|
const node = nodesSlice.nodes.find((node) => node.id === nodeId);
|
||||||
assert(isInvocationNode(node), `Node ${nodeId} is not an invocation node`);
|
assert(isInvocationNode(node), `Node ${nodeId} is not an invocation node`);
|
||||||
return node;
|
return node;
|
||||||
|
@ -1 +1 @@
|
|||||||
__version__ = "4.2.2"
|
__version__ = "4.2.2post1"
|
||||||
|
Loading…
Reference in New Issue
Block a user