mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): improved UI for missing node field templates
When a node is updated with new fields and workflow needs to be updated, the fields now display "Unknown input/output: FieldName".
This commit is contained in:
parent
86a74e929a
commit
ed79980dd4
@ -977,16 +977,16 @@
|
|||||||
"unhandledInputProperty": "Unhandled input property",
|
"unhandledInputProperty": "Unhandled input property",
|
||||||
"unhandledOutputProperty": "Unhandled output property",
|
"unhandledOutputProperty": "Unhandled output property",
|
||||||
"unknownField": "Unknown field",
|
"unknownField": "Unknown field",
|
||||||
"unknownFieldType": "$(nodes.unknownField) type",
|
"unknownFieldType": "$t(nodes.unknownField) type",
|
||||||
"unknownNode": "Unknown Node",
|
"unknownNode": "Unknown Node",
|
||||||
"unknownNodeType":"$t(nodes.unknownNode) type",
|
"unknownNodeType":"$t(nodes.unknownNode) type",
|
||||||
"unknownTemplate": "Unknown Template",
|
"unknownTemplate": "Unknown Template",
|
||||||
"unknownInput": "Unknown input",
|
"unknownInput": "Unknown input: {{name}}",
|
||||||
"unkownInvocation": "Unknown Invocation type",
|
"unkownInvocation": "Unknown Invocation type",
|
||||||
"unknownOutput": "Unknown output",
|
"unknownOutput": "Unknown output: {{name}}",
|
||||||
"updateNode": "Update Node",
|
"updateNode": "Update Node",
|
||||||
"updateApp": "Update App",
|
"updateApp": "Update App",
|
||||||
"updateAllNodes": "Update All Nodes",
|
"updateAllNodes": "Update Nodes",
|
||||||
"allNodesUpdated": "All Nodes Updated",
|
"allNodesUpdated": "All Nodes Updated",
|
||||||
"unableToUpdateNodes_one": "Unable to update {{count}} node",
|
"unableToUpdateNodes_one": "Unable to update {{count}} node",
|
||||||
"unableToUpdateNodes_other": "Unable to update {{count}} nodes",
|
"unableToUpdateNodes_other": "Unable to update {{count}} nodes",
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import { Box, Flex, FormControl, FormLabel } from '@chakra-ui/react';
|
import { Box, Flex, FormControl, FormLabel } from '@chakra-ui/react';
|
||||||
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 { useFieldTemplate } from 'features/nodes/hooks/useFieldTemplate';
|
import { useFieldInputInstance } from 'features/nodes/hooks/useFieldInputInstance';
|
||||||
|
import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate';
|
||||||
import { PropsWithChildren, memo, useMemo } from 'react';
|
import { PropsWithChildren, memo, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import EditableFieldTitle from './EditableFieldTitle';
|
import EditableFieldTitle from './EditableFieldTitle';
|
||||||
import FieldContextMenu from './FieldContextMenu';
|
import FieldContextMenu from './FieldContextMenu';
|
||||||
import FieldHandle from './FieldHandle';
|
import FieldHandle from './FieldHandle';
|
||||||
import InputFieldRenderer from './InputFieldRenderer';
|
import InputFieldRenderer from './InputFieldRenderer';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
@ -16,7 +17,8 @@ interface Props {
|
|||||||
|
|
||||||
const InputField = ({ nodeId, fieldName }: Props) => {
|
const InputField = ({ nodeId, fieldName }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const fieldTemplate = useFieldTemplate(nodeId, fieldName, 'input');
|
const fieldTemplate = useFieldInputTemplate(nodeId, fieldName);
|
||||||
|
const fieldInstance = useFieldInputInstance(nodeId, fieldName);
|
||||||
const doesFieldHaveValue = useDoesInputHaveValue(nodeId, fieldName);
|
const doesFieldHaveValue = useDoesInputHaveValue(nodeId, fieldName);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -28,7 +30,7 @@ const InputField = ({ nodeId, fieldName }: Props) => {
|
|||||||
} = useConnectionState({ nodeId, fieldName, kind: 'input' });
|
} = useConnectionState({ nodeId, fieldName, kind: 'input' });
|
||||||
|
|
||||||
const isMissingInput = useMemo(() => {
|
const isMissingInput = useMemo(() => {
|
||||||
if (fieldTemplate?.fieldKind !== 'input') {
|
if (!fieldTemplate) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,13 +47,35 @@ const InputField = ({ nodeId, fieldName }: Props) => {
|
|||||||
}
|
}
|
||||||
}, [fieldTemplate, isConnected, doesFieldHaveValue]);
|
}, [fieldTemplate, isConnected, doesFieldHaveValue]);
|
||||||
|
|
||||||
if (fieldTemplate?.fieldKind !== 'input') {
|
if (!fieldTemplate || !fieldInstance) {
|
||||||
return (
|
return (
|
||||||
<InputFieldWrapper shouldDim={shouldDim}>
|
<InputFieldWrapper shouldDim={shouldDim}>
|
||||||
<FormControl
|
<FormControl
|
||||||
sx={{ color: 'error.400', textAlign: 'left', fontSize: 'sm' }}
|
sx={{
|
||||||
|
alignItems: 'stretch',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
gap: 2,
|
||||||
|
h: 'full',
|
||||||
|
w: 'full',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{t('nodes.unknownInput')}: {fieldName}
|
<FormLabel
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
mb: 0,
|
||||||
|
px: 1,
|
||||||
|
gap: 2,
|
||||||
|
h: 'full',
|
||||||
|
fontWeight: 600,
|
||||||
|
color: 'error.400',
|
||||||
|
_dark: { color: 'error.300' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('nodes.unknownInput', {
|
||||||
|
name: fieldInstance?.label ?? fieldTemplate?.title ?? fieldName,
|
||||||
|
})}
|
||||||
|
</FormLabel>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</InputFieldWrapper>
|
</InputFieldWrapper>
|
||||||
);
|
);
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { Flex, FormControl, FormLabel, Tooltip } from '@chakra-ui/react';
|
import { Flex, FormControl, FormLabel, Tooltip } from '@chakra-ui/react';
|
||||||
import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
|
import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
|
||||||
import { useFieldTemplate } from 'features/nodes/hooks/useFieldTemplate';
|
import { useFieldOutputInstance } from 'features/nodes/hooks/useFieldOutputInstance';
|
||||||
|
import { useFieldOutputTemplate } from 'features/nodes/hooks/useFieldOutputTemplate';
|
||||||
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
|
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
|
||||||
import { PropsWithChildren, memo } from 'react';
|
import { PropsWithChildren, memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import FieldHandle from './FieldHandle';
|
import FieldHandle from './FieldHandle';
|
||||||
import FieldTooltipContent from './FieldTooltipContent';
|
import FieldTooltipContent from './FieldTooltipContent';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
@ -14,7 +15,8 @@ interface Props {
|
|||||||
|
|
||||||
const OutputField = ({ nodeId, fieldName }: Props) => {
|
const OutputField = ({ nodeId, fieldName }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const fieldTemplate = useFieldTemplate(nodeId, fieldName, 'output');
|
const fieldTemplate = useFieldOutputTemplate(nodeId, fieldName);
|
||||||
|
const fieldInstance = useFieldOutputInstance(nodeId, fieldName);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isConnected,
|
isConnected,
|
||||||
@ -24,13 +26,35 @@ const OutputField = ({ nodeId, fieldName }: Props) => {
|
|||||||
shouldDim,
|
shouldDim,
|
||||||
} = useConnectionState({ nodeId, fieldName, kind: 'output' });
|
} = useConnectionState({ nodeId, fieldName, kind: 'output' });
|
||||||
|
|
||||||
if (fieldTemplate?.fieldKind !== 'output') {
|
if (!fieldTemplate || !fieldInstance) {
|
||||||
return (
|
return (
|
||||||
<OutputFieldWrapper shouldDim={shouldDim}>
|
<OutputFieldWrapper shouldDim={shouldDim}>
|
||||||
<FormControl
|
<FormControl
|
||||||
sx={{ color: 'error.400', textAlign: 'right', fontSize: 'sm' }}
|
sx={{
|
||||||
|
alignItems: 'stretch',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
gap: 2,
|
||||||
|
h: 'full',
|
||||||
|
w: 'full',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{t('nodes.unknownOutput')}: {fieldName}
|
<FormLabel
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
mb: 0,
|
||||||
|
px: 1,
|
||||||
|
gap: 2,
|
||||||
|
h: 'full',
|
||||||
|
fontWeight: 600,
|
||||||
|
color: 'error.400',
|
||||||
|
_dark: { color: 'error.300' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('nodes.unknownOutput', {
|
||||||
|
name: fieldTemplate?.title ?? fieldName,
|
||||||
|
})}
|
||||||
|
</FormLabel>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</OutputFieldWrapper>
|
</OutputFieldWrapper>
|
||||||
);
|
);
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { Flex } from '@chakra-ui/layout';
|
import { Flex } from '@chakra-ui/layout';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
|
||||||
import { addNodePopoverOpened } from 'features/nodes/store/nodesSlice';
|
|
||||||
import { memo, useCallback } from 'react';
|
|
||||||
import { FaPlus, FaSync } from 'react-icons/fa';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import IAIButton from 'common/components/IAIButton';
|
import IAIButton from 'common/components/IAIButton';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
import { useGetNodesNeedUpdate } from 'features/nodes/hooks/useGetNodesNeedUpdate';
|
import { useGetNodesNeedUpdate } from 'features/nodes/hooks/useGetNodesNeedUpdate';
|
||||||
import { updateAllNodesRequested } from 'features/nodes/store/actions';
|
import { updateAllNodesRequested } from 'features/nodes/store/actions';
|
||||||
|
import { addNodePopoverOpened } from 'features/nodes/store/nodesSlice';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { FaExclamationTriangle, FaPlus } from 'react-icons/fa';
|
||||||
|
|
||||||
const TopLeftPanel = () => {
|
const TopLeftPanel = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@ -29,7 +29,10 @@ const TopLeftPanel = () => {
|
|||||||
onClick={handleOpenAddNodePopover}
|
onClick={handleOpenAddNodePopover}
|
||||||
/>
|
/>
|
||||||
{nodesNeedUpdate && (
|
{nodesNeedUpdate && (
|
||||||
<IAIButton leftIcon={<FaSync />} onClick={handleClickUpdateNodes}>
|
<IAIButton
|
||||||
|
leftIcon={<FaExclamationTriangle />}
|
||||||
|
onClick={handleClickUpdateNodes}
|
||||||
|
>
|
||||||
{t('nodes.updateAllNodes')}
|
{t('nodes.updateAllNodes')}
|
||||||
</IAIButton>
|
</IAIButton>
|
||||||
)}
|
)}
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { stateSelector } from 'app/store/store';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { isInvocationNode } from '../types/invocation';
|
||||||
|
|
||||||
|
export const useFieldInputInstance = (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 fieldTemplate = useAppSelector(selector);
|
||||||
|
|
||||||
|
return fieldTemplate;
|
||||||
|
};
|
@ -0,0 +1,29 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { stateSelector } from 'app/store/store';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { isInvocationNode } from '../types/invocation';
|
||||||
|
|
||||||
|
export const useFieldInputTemplate = (nodeId: string, fieldName: string) => {
|
||||||
|
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?.inputs[fieldName];
|
||||||
|
},
|
||||||
|
defaultSelectorOptions
|
||||||
|
),
|
||||||
|
[fieldName, nodeId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const fieldTemplate = useAppSelector(selector);
|
||||||
|
|
||||||
|
return fieldTemplate;
|
||||||
|
};
|
@ -0,0 +1,28 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { stateSelector } from 'app/store/store';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { isInvocationNode } from '../types/invocation';
|
||||||
|
|
||||||
|
export const useFieldOutputInstance = (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.outputs[fieldName];
|
||||||
|
},
|
||||||
|
defaultSelectorOptions
|
||||||
|
),
|
||||||
|
[fieldName, nodeId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const fieldTemplate = useAppSelector(selector);
|
||||||
|
|
||||||
|
return fieldTemplate;
|
||||||
|
};
|
@ -0,0 +1,29 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { stateSelector } from 'app/store/store';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { isInvocationNode } from '../types/invocation';
|
||||||
|
|
||||||
|
export const useFieldOutputTemplate = (nodeId: string, fieldName: string) => {
|
||||||
|
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?.outputs[fieldName];
|
||||||
|
},
|
||||||
|
defaultSelectorOptions
|
||||||
|
),
|
||||||
|
[fieldName, nodeId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const fieldTemplate = useAppSelector(selector);
|
||||||
|
|
||||||
|
return fieldTemplate;
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user