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 NodeWrapper from 'features/nodes/components/flow/nodes/common/NodeWrapper';
|
||||
import { useAnyOrDirectInputFieldNames } from 'features/nodes/hooks/useAnyOrDirectInputFieldNames';
|
||||
import { useConnectionInputFieldNames } from 'features/nodes/hooks/useConnectionInputFieldNames';
|
||||
import { InvocationInputFieldCheck } from 'features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck';
|
||||
import { useFieldNames } from 'features/nodes/hooks/useFieldNames';
|
||||
import { useOutputFieldNames } from 'features/nodes/hooks/useOutputFieldNames';
|
||||
import { useWithFooter } from 'features/nodes/hooks/useWithFooter';
|
||||
import { memo } from 'react';
|
||||
@ -20,8 +20,7 @@ type Props = {
|
||||
};
|
||||
|
||||
const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
|
||||
const inputConnectionFieldNames = useConnectionInputFieldNames(nodeId);
|
||||
const inputAnyOrDirectFieldNames = useAnyOrDirectInputFieldNames(nodeId);
|
||||
const fieldNames = useFieldNames(nodeId);
|
||||
const withFooter = useWithFooter(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">
|
||||
<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`}>
|
||||
<InputField nodeId={nodeId} fieldName={fieldName} />
|
||||
<InvocationInputFieldCheck nodeId={nodeId} fieldName={fieldName}>
|
||||
<InputField nodeId={nodeId} fieldName={fieldName} />
|
||||
</InvocationInputFieldCheck>
|
||||
</GridItem>
|
||||
))}
|
||||
{outputFieldNames.map((fieldName, i) => (
|
||||
@ -52,8 +53,23 @@ const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
|
||||
</GridItem>
|
||||
))}
|
||||
</Grid>
|
||||
{inputAnyOrDirectFieldNames.map((fieldName) => (
|
||||
<InputField key={`${nodeId}.${fieldName}.input-field`} nodeId={nodeId} fieldName={fieldName} />
|
||||
{fieldNames.anyOrDirectFields.map((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>
|
||||
|
@ -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 { useDoesInputHaveValue } from 'features/nodes/hooks/useDoesInputHaveValue';
|
||||
import { useFieldInputInstance } from 'features/nodes/hooks/useFieldInputInstance';
|
||||
import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import EditableFieldTitle from './EditableFieldTitle';
|
||||
import FieldHandle from './FieldHandle';
|
||||
import FieldLinearViewToggle from './FieldLinearViewToggle';
|
||||
import InputFieldRenderer from './InputFieldRenderer';
|
||||
import { InputFieldWrapper } from './InputFieldWrapper';
|
||||
|
||||
interface Props {
|
||||
nodeId: string;
|
||||
@ -18,9 +16,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const InputField = ({ nodeId, fieldName }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const fieldTemplate = useFieldInputTemplate(nodeId, fieldName);
|
||||
const fieldInstance = useFieldInputInstance(nodeId, fieldName);
|
||||
const doesFieldHaveValue = useDoesInputHaveValue(nodeId, fieldName);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
@ -55,20 +51,6 @@ const InputField = ({ nodeId, fieldName }: Props) => {
|
||||
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) {
|
||||
return (
|
||||
<InputFieldWrapper shouldDim={shouldDim}>
|
||||
@ -134,27 +116,3 @@ const InputField = ({ nodeId, fieldName }: Props) => {
|
||||
};
|
||||
|
||||
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 { useAppDispatch } from 'app/store/storeHooks';
|
||||
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 { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
|
||||
import { workflowExposedFieldRemoved } from 'features/nodes/store/workflowSlice';
|
||||
@ -102,9 +102,9 @@ const LinearViewFieldInternal = ({ nodeId, fieldName }: Props) => {
|
||||
|
||||
const LinearViewField = ({ nodeId, fieldName }: Props) => {
|
||||
return (
|
||||
<MissingFallback nodeId={nodeId} fieldName={fieldName}>
|
||||
<InvocationInputFieldCheck nodeId={nodeId} fieldName={fieldName}>
|
||||
<LinearViewFieldInternal nodeId={nodeId} fieldName={fieldName} />
|
||||
</MissingFallback>
|
||||
</InvocationInputFieldCheck>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -48,7 +48,7 @@ const NotesNode = (props: NodeProps<NotesNodeData>) => {
|
||||
gap={1}
|
||||
>
|
||||
<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>
|
||||
</>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Flex, FormLabel, Icon, IconButton, Spacer, Tooltip } from '@invoke-ai/ui-library';
|
||||
import FieldTooltipContent from 'features/nodes/components/flow/nodes/Invocation/fields/FieldTooltipContent';
|
||||
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 { useFieldOriginalValue } from 'features/nodes/hooks/useFieldOriginalValue';
|
||||
import { useFieldTemplateTitle } from 'features/nodes/hooks/useFieldTemplateTitle';
|
||||
@ -53,9 +53,9 @@ const WorkflowFieldInternal = ({ nodeId, fieldName }: Props) => {
|
||||
|
||||
const WorkflowField = ({ nodeId, fieldName }: Props) => {
|
||||
return (
|
||||
<MissingFallback nodeId={nodeId} fieldName={fieldName}>
|
||||
<InvocationInputFieldCheck 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 type { FieldInputTemplate } from 'features/nodes/types/field';
|
||||
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 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;
|
||||
};
|
||||
|
@ -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 { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
||||
import { selectNodeData } from 'features/nodes/store/selectors';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useNodeLabel = (nodeId: string) => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodes) => {
|
||||
return selectNodeData(nodes, nodeId)?.label ?? null;
|
||||
createSelector(selectNodesSlice, (nodesSlice) => {
|
||||
const node = nodesSlice.nodes.find((node) => node.id === nodeId);
|
||||
return node?.data.label;
|
||||
}),
|
||||
[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';
|
||||
|
||||
export const useNodeTemplateTitle = (nodeId: string): string | null => {
|
||||
const template = useNodeTemplate(nodeId);
|
||||
const title = useMemo(() => template.title, [template.title]);
|
||||
const templates = useStore($templates);
|
||||
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;
|
||||
};
|
||||
|
@ -275,10 +275,9 @@ export const nodesSlice = createSlice({
|
||||
const { nodeId, label } = action.payload;
|
||||
const nodeIndex = state.nodes.findIndex((n) => n.id === nodeId);
|
||||
const node = state.nodes?.[nodeIndex];
|
||||
if (!isInvocationNode(node)) {
|
||||
return;
|
||||
if (isInvocationNode(node) || isNotesNode(node)) {
|
||||
node.data.label = label;
|
||||
}
|
||||
node.data.label = label;
|
||||
},
|
||||
nodeNotesChanged: (state, action: PayloadAction<{ nodeId: string; notes: string }>) => {
|
||||
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 { 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);
|
||||
assert(isInvocationNode(node), `Node ${nodeId} is not an invocation node`);
|
||||
return node;
|
||||
|
@ -1 +1 @@
|
||||
__version__ = "4.2.2"
|
||||
__version__ = "4.2.2post1"
|
||||
|
Loading…
Reference in New Issue
Block a user