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:
psychedelicious 2023-11-25 21:10:22 +11:00
parent 86a74e929a
commit ed79980dd4
8 changed files with 188 additions and 23 deletions

View File

@ -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",

View File

@ -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>
); );

View File

@ -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>
); );

View File

@ -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>
)} )}

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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;
};