refactor(ui): continued reorg of components & hooks

This commit is contained in:
psychedelicious
2025-01-20 16:11:55 +11:00
parent bfd70be50b
commit 011910a08c
78 changed files with 634 additions and 547 deletions

View File

@@ -1,6 +1,6 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { $nodeExecutionStates } from 'features/nodes/hooks/useExecutionState';
import { $nodeExecutionStates } from 'features/nodes/hooks/useNodeExecutionState';
import { workflowLoaded, workflowLoadRequested } from 'features/nodes/store/actions';
import { $templates } from 'features/nodes/store/nodesSlice';
import { $needsFit } from 'features/nodes/store/reactFlowInstance';

View File

@@ -3,9 +3,9 @@ import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppSelector, useAppStore } from 'app/store/storeHooks';
import { useFocusRegion, useIsRegionFocused } from 'common/hooks/focus';
import { useConnection } from 'features/nodes/hooks/useConnection';
import { useCopyPaste } from 'features/nodes/hooks/useCopyPaste';
import { useSyncExecutionState } from 'features/nodes/hooks/useExecutionState';
import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection';
import { useNodeCopyPaste } from 'features/nodes/hooks/useNodeCopyPaste';
import { useSyncExecutionState } from 'features/nodes/hooks/useNodeExecutionState';
import { useWorkflowWatcher } from 'features/nodes/hooks/useWorkflowWatcher';
import {
$addNodeCmdk,
@@ -208,7 +208,7 @@ export const Flow = memo(() => {
// #endregion
const { copySelection, pasteSelection, pasteSelectionWithEdges } = useCopyPaste();
const { copySelection, pasteSelection, pasteSelectionWithEdges } = useNodeCopyPaste();
useRegisteredHotkeys({
id: 'copySelection',

View File

@@ -3,12 +3,12 @@ import NodeWrapper from 'features/nodes/components/flow/nodes/common/NodeWrapper
import { InputFieldGate } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate';
import { OutputFieldGate } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldGate';
import { OutputFieldNodesEditorView } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldNodesEditorView';
import { useFieldNames } from 'features/nodes/hooks/useFieldNames';
import { useInputFieldNamesByStatus } from 'features/nodes/hooks/useInputFieldNamesByStatus';
import { useOutputFieldNames } from 'features/nodes/hooks/useOutputFieldNames';
import { useWithFooter } from 'features/nodes/hooks/useWithFooter';
import { memo } from 'react';
import { InputFieldViewNodes } from './fields/InputFieldViewNodes';
import { InputFieldEditModeNodes } from './fields/InputFieldEditModeNodes';
import InvocationNodeFooter from './InvocationNodeFooter';
import InvocationNodeHeader from './InvocationNodeHeader';
@@ -21,7 +21,7 @@ type Props = {
};
const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
const fieldNames = useFieldNames(nodeId);
const fieldNames = useInputFieldNamesByStatus(nodeId);
const withFooter = useWithFooter(nodeId);
const outputFieldNames = useOutputFieldNames(nodeId);
@@ -44,7 +44,7 @@ const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
{fieldNames.connectionFields.map((fieldName, i) => (
<GridItem gridColumnStart={1} gridRowStart={i + 1} key={`${nodeId}.${fieldName}.input-field`}>
<InputFieldGate nodeId={nodeId} fieldName={fieldName}>
<InputFieldViewNodes nodeId={nodeId} fieldName={fieldName} />
<InputFieldEditModeNodes nodeId={nodeId} fieldName={fieldName} />
</InputFieldGate>
</GridItem>
))}
@@ -58,12 +58,12 @@ const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
</Grid>
{fieldNames.anyOrDirectFields.map((fieldName) => (
<InputFieldGate key={`${nodeId}.${fieldName}.input-field`} nodeId={nodeId} fieldName={fieldName}>
<InputFieldViewNodes nodeId={nodeId} fieldName={fieldName} />
<InputFieldEditModeNodes nodeId={nodeId} fieldName={fieldName} />
</InputFieldGate>
))}
{fieldNames.missingFields.map((fieldName) => (
<InputFieldGate key={`${nodeId}.${fieldName}.input-field`} nodeId={nodeId} fieldName={fieldName}>
<InputFieldViewNodes nodeId={nodeId} fieldName={fieldName} />
<InputFieldEditModeNodes nodeId={nodeId} fieldName={fieldName} />
</InputFieldGate>
))}
</Flex>

View File

@@ -1,6 +1,6 @@
import type { ChakraProps } from '@invoke-ai/ui-library';
import { Flex, FormControlGroup } from '@invoke-ai/ui-library';
import { useHasImageOutput } from 'features/nodes/hooks/useHasImageOutput';
import { useNodeHasImageOutput } from 'features/nodes/hooks/useNodeHasImageOutput';
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { memo } from 'react';
@@ -15,7 +15,7 @@ type Props = {
const props: ChakraProps = { w: 'unset' };
const InvocationNodeFooter = ({ nodeId }: Props) => {
const hasImageOutput = useHasImageOutput(nodeId);
const hasImageOutput = useNodeHasImageOutput(nodeId);
const isCacheEnabled = useFeatureStatus('invocationCache');
return (
<Flex

View File

@@ -1,31 +1,31 @@
import { FormControl, FormLabel, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useNode } from 'features/nodes/hooks/useNode';
import { useInvocationNodeNotes } from 'features/nodes/hooks/useNodeNotes';
import { nodeNotesChanged } from 'features/nodes/store/nodesSlice';
import { isInvocationNode } from 'features/nodes/types/invocation';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
const NotesTextarea = ({ nodeId }: { nodeId: string }) => {
type Props = {
nodeId: string;
};
export const InvocationNodeNotesTextarea = memo(({ nodeId }: Props) => {
const dispatch = useAppDispatch();
const node = useNode(nodeId);
const { t } = useTranslation();
const notes = useInvocationNodeNotes(nodeId);
const handleNotesChanged = useCallback(
(e: ChangeEvent<HTMLTextAreaElement>) => {
dispatch(nodeNotesChanged({ nodeId, notes: e.target.value }));
},
[dispatch, nodeId]
);
if (!isInvocationNode(node)) {
return null;
}
return (
<FormControl orientation="vertical" h="full">
<FormLabel>{t('nodes.notes')}</FormLabel>
<Textarea value={node.data?.notes} onChange={handleNotesChanged} rows={10} resize="none" />
<Textarea value={notes} onChange={handleNotesChanged} rows={10} resize="none" />
</FormControl>
);
};
});
export default memo(NotesTextarea);
InvocationNodeNotesTextarea.displayName = 'InvocationNodeNotesTextarea';

View File

@@ -1,6 +1,6 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Badge, CircularProgress, Flex, Icon, Image, Text, Tooltip } from '@invoke-ai/ui-library';
import { useExecutionState } from 'features/nodes/hooks/useExecutionState';
import { useNodeExecutionState } from 'features/nodes/hooks/useNodeExecutionState';
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
import type { NodeExecutionState } from 'features/nodes/types/invocation';
import { zNodeStatus } from 'features/nodes/types/invocation';
@@ -22,7 +22,7 @@ const circleStyles: SystemStyleObject = {
};
const InvocationNodeStatusIndicator = ({ nodeId }: Props) => {
const nodeExecutionState = useExecutionState(nodeId);
const nodeExecutionState = useNodeExecutionState(nodeId);
if (!nodeExecutionState) {
return null;

View File

@@ -1,7 +1,7 @@
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useHasImageOutput } from 'features/nodes/hooks/useHasImageOutput';
import { useIsIntermediate } from 'features/nodes/hooks/useIsIntermediate';
import { useNodeHasImageOutput } from 'features/nodes/hooks/useNodeHasImageOutput';
import { nodeIsIntermediateChanged } from 'features/nodes/store/nodesSlice';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
@@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next';
const SaveToGalleryCheckbox = ({ nodeId }: { nodeId: string }) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const hasImageOutput = useHasImageOutput(nodeId);
const hasImageOutput = useNodeHasImageOutput(nodeId);
const isIntermediate = useIsIntermediate(nodeId);
const handleChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
@@ -30,7 +30,7 @@ const SaveToGalleryCheckbox = ({ nodeId }: { nodeId: string }) => {
return (
<FormControl className="nopan">
<FormLabel>{t('nodes.saveToGallery')} </FormLabel>
<FormLabel m={0}>{t('nodes.saveToGallery')} </FormLabel>
<Checkbox onChange={handleChange} isChecked={!isIntermediate} />
</FormControl>
);

View File

@@ -23,7 +23,7 @@ const UseCacheCheckbox = ({ nodeId }: { nodeId: string }) => {
const { t } = useTranslation();
return (
<FormControl>
<FormLabel>{t('invocationCache.useCache')}</FormLabel>
<FormLabel m={0}>{t('invocationCache.useCache')}</FormLabel>
<Checkbox className="nopan" onChange={handleChange} isChecked={useCache} />
</FormControl>
);

View File

@@ -1,16 +0,0 @@
import { FieldResetValueButton } from 'features/nodes/components/flow/nodes/Invocation/fields/FieldResetValueButton';
import { useFieldDefaultValue } from 'features/nodes/hooks/useFieldDefaultValue';
import { memo } from 'react';
type Props = {
nodeId: string;
fieldName: string;
};
const FieldResetToDefaultValueButton = ({ nodeId, fieldName }: Props) => {
const { isValueChanged, resetToDefaultValue } = useFieldDefaultValue(nodeId, fieldName);
return <FieldResetValueButton onClick={resetToDefaultValue} isDisabled={!isValueChanged} />;
};
export default memo(FieldResetToDefaultValueButton);

View File

@@ -1,16 +0,0 @@
import { FieldResetValueButton } from 'features/nodes/components/flow/nodes/Invocation/fields/FieldResetValueButton';
import { useFieldInitialLinearViewValue } from 'features/nodes/hooks/useFieldInitialLinearViewValue';
import { memo } from 'react';
type Props = {
nodeId: string;
fieldName: string;
};
const FieldResetToInitialLinearViewValueButton = ({ nodeId, fieldName }: Props) => {
const { isValueChanged, resetToInitialLinearViewValue } = useFieldInitialLinearViewValue(nodeId, fieldName);
return <FieldResetValueButton onClick={resetToInitialLinearViewValue} isDisabled={!isValueChanged} />;
};
export default memo(FieldResetToInitialLinearViewValueButton);

View File

@@ -1,25 +0,0 @@
import type { IconButtonProps } from '@invoke-ai/ui-library';
import { IconButton } from '@invoke-ai/ui-library';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
type Props = Omit<IconButtonProps, 'aria-label'>;
export const FieldResetValueButton = memo((props: Props) => {
const { t } = useTranslation();
return (
<IconButton
variant="ghost"
tooltip={t('nodes.resetToDefaultValue')}
aria-label={t('nodes.resetToDefaultValue')}
icon={<PiArrowCounterClockwiseBold />}
pointerEvents="auto"
size="xs"
{...props}
/>
);
});
FieldResetValueButton.displayName = 'FieldResetValueButton';

View File

@@ -1,63 +0,0 @@
import { Flex, Text } from '@invoke-ai/ui-library';
import { useFieldInputInstance } from 'features/nodes/hooks/useFieldInputInstance';
import { useFieldTemplate } from 'features/nodes/hooks/useFieldTemplate';
import { useFieldTypeName } from 'features/nodes/hooks/usePrettyFieldType';
import { isFieldInputInstance, isFieldInputTemplate } from 'features/nodes/types/field';
import { startCase } from 'lodash-es';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
interface Props {
nodeId: string;
fieldName: string;
kind: 'inputs' | 'outputs';
}
const FieldTooltipContent = ({ nodeId, fieldName, kind }: Props) => {
const field = useFieldInputInstance(nodeId, fieldName);
const fieldTemplate = useFieldTemplate(nodeId, fieldName, kind);
const isInputTemplate = isFieldInputTemplate(fieldTemplate);
const fieldTypeName = useFieldTypeName(fieldTemplate?.type);
const { t } = useTranslation();
const fieldTitle = useMemo(() => {
if (isFieldInputInstance(field)) {
if (field.label && fieldTemplate?.title) {
return `${field.label} (${fieldTemplate.title})`;
}
if (field.label && !fieldTemplate) {
return field.label;
}
if (!field.label && fieldTemplate) {
return fieldTemplate.title;
}
return t('nodes.unknownField');
} else {
return fieldTemplate?.title || t('nodes.unknownField');
}
}, [field, fieldTemplate, t]);
return (
<Flex flexDir="column">
<Text fontWeight="semibold">{fieldTitle}</Text>
{fieldTemplate && (
<Text opacity={0.7} fontStyle="oblique 5deg">
{fieldTemplate.description}
</Text>
)}
{fieldTypeName && (
<Text>
{t('parameters.type')}: {fieldTypeName}
</Text>
)}
{isInputTemplate && (
<Text>
{t('common.input')}: {startCase(fieldTemplate.input)}
</Text>
)}
</Flex>
);
};
export default memo(FieldTooltipContent);

View File

@@ -1,11 +1,8 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useFieldIsExposed } from 'features/nodes/hooks/useFieldIsExposed';
import { useFieldValue } from 'features/nodes/hooks/useFieldValue';
import {
workflowExposedFieldAdded,
workflowExposedFieldRemoved,
} from 'features/nodes/store/workflowSlice';
import { useInputFieldIsExposed } from 'features/nodes/hooks/useInputFieldIsExposed';
import { useInputFieldValue } from 'features/nodes/hooks/useInputFieldValue';
import { workflowExposedFieldAdded, workflowExposedFieldRemoved } from 'features/nodes/store/workflowSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiMinusBold, PiPlusBold } from 'react-icons/pi';
@@ -15,11 +12,11 @@ type Props = {
fieldName: string;
};
const FieldLinearViewToggle = ({ nodeId, fieldName }: Props) => {
export const InputFieldAddRemoveLinearViewIconButton = memo(({ nodeId, fieldName }: Props) => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const value = useFieldValue(nodeId, fieldName);
const isExposed = useFieldIsExposed(nodeId, fieldName);
const value = useInputFieldValue(nodeId, fieldName);
const isExposed = useInputFieldIsExposed(nodeId, fieldName);
const handleExposeField = useCallback(() => {
dispatch(workflowExposedFieldAdded({ nodeId, fieldName, value }));
@@ -54,6 +51,6 @@ const FieldLinearViewToggle = ({ nodeId, fieldName }: Props) => {
/>
);
}
};
});
export default memo(FieldLinearViewToggle);
InputFieldAddRemoveLinearViewIconButton.displayName = 'InputFieldAddRemoveLinearViewIconButton';

View File

@@ -1,20 +1,18 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Box, Circle, Flex, Icon, IconButton, Spacer, Tooltip } from '@invoke-ai/ui-library';
import { Box, Circle, Flex, IconButton, Spacer } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { DndListDropIndicator } from 'features/dnd/DndListDropIndicator';
import { FieldNotesIconButton } from 'features/nodes/components/flow/nodes/Invocation/fields/FieldNotesIconButton';
import FieldResetToInitialLinearViewValueButton from 'features/nodes/components/flow/nodes/Invocation/fields/FieldResetToInitialLinearViewValueButton';
import { InputFieldNotesIconButton } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldNotesIconButton';
import InputFieldResetToInitialValueIconButton from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldResetToInitialValueIconButton';
import { useLinearViewFieldDnd } from 'features/nodes/components/sidePanel/workflow/useLinearViewFieldDnd';
import { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
import { workflowExposedFieldRemoved } from 'features/nodes/store/workflowSlice';
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
import { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { PiInfoBold, PiTrashSimpleBold } from 'react-icons/pi';
import { PiTrashSimpleBold } from 'react-icons/pi';
import EditableFieldTitle from './EditableFieldTitle';
import FieldTooltipContent from './FieldTooltipContent';
import InputFieldRenderer from './InputFieldRenderer';
import { InputFieldRenderer } from './InputFieldRenderer';
import { InputFieldTitle } from './InputFieldTitle';
type Props = {
nodeId: string;
@@ -34,7 +32,7 @@ const sx = {
transitionProperty: 'common',
} satisfies SystemStyleObject;
export const InputFieldViewLinear = memo(({ nodeId, fieldName }: Props) => {
export const InputFieldEditModeLinear = memo(({ nodeId, fieldName }: Props) => {
const dispatch = useAppDispatch();
const { isMouseOverNode, handleMouseOut, handleMouseOver } = useMouseOverNode(nodeId);
const { t } = useTranslation();
@@ -59,20 +57,11 @@ export const InputFieldViewLinear = memo(({ nodeId, fieldName }: Props) => {
>
<Flex flexDir="column" w="full">
<Flex alignItems="center" gap={2}>
<EditableFieldTitle nodeId={nodeId} fieldName={fieldName} kind="inputs" />
<InputFieldTitle nodeId={nodeId} fieldName={fieldName} />
<Spacer />
{isMouseOverNode && <Circle me={2} size={2} borderRadius="full" bg="invokeBlue.500" />}
<FieldNotesIconButton nodeId={nodeId} fieldName={fieldName} />
<FieldResetToInitialLinearViewValueButton nodeId={nodeId} fieldName={fieldName} />
<Tooltip
label={<FieldTooltipContent nodeId={nodeId} fieldName={fieldName} kind="inputs" />}
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
placement="top"
>
<Flex h="full" alignItems="center">
<Icon fontSize="sm" color="base.300" as={PiInfoBold} />
</Flex>
</Tooltip>
<InputFieldNotesIconButton nodeId={nodeId} fieldName={fieldName} />
<InputFieldResetToInitialValueIconButton nodeId={nodeId} fieldName={fieldName} />
<IconButton
aria-label={t('nodes.removeLinearView')}
tooltip={t('nodes.removeLinearView')}
@@ -90,4 +79,4 @@ export const InputFieldViewLinear = memo(({ nodeId, fieldName }: Props) => {
);
});
InputFieldViewLinear.displayName = 'InputFieldViewLinear';
InputFieldEditModeLinear.displayName = 'InputFieldEditModeLinear';

View File

@@ -1,16 +1,17 @@
import { Flex, FormControl } from '@invoke-ai/ui-library';
import { FieldHandle } from 'features/nodes/components/flow/nodes/Invocation/fields/FieldHandle';
import { FieldLinearViewConfigIconButton } from 'features/nodes/components/flow/nodes/Invocation/fields/FieldLinearViewConfigIconButton';
import { FieldNotesIconButton } from 'features/nodes/components/flow/nodes/Invocation/fields/FieldNotesIconButton';
import FieldResetToDefaultValueButton from 'features/nodes/components/flow/nodes/Invocation/fields/FieldResetToDefaultValueButton';
import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate';
import { useFieldIsInvalid } from 'features/nodes/hooks/useFieldIsInvalid';
import { InputFieldLinearViewConfigIconButton } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldLinearViewConfigIconButton';
import { InputFieldNotesIconButton } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldNotesIconButton';
import { InputFieldResetToDefaultValueIconButton } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldResetToDefaultValueIconButton';
import { useInputFieldConnectionState } from 'features/nodes/hooks/useInputFieldConnectionState';
import { useInputFieldIsConnected } from 'features/nodes/hooks/useInputFieldIsConnected';
import { useInputFieldIsInvalid } from 'features/nodes/hooks/useInputFieldIsInvalid';
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
import { memo, useCallback, useState } from 'react';
import EditableFieldTitle from './EditableFieldTitle';
import FieldLinearViewToggle from './FieldLinearViewToggle';
import InputFieldRenderer from './InputFieldRenderer';
import { InputFieldAddRemoveLinearViewIconButton } from './InputFieldAddRemoveLinearViewIconButton';
import { InputFieldRenderer } from './InputFieldRenderer';
import { InputFieldTitle } from './InputFieldTitle';
import { InputFieldWrapper } from './InputFieldWrapper';
interface Props {
@@ -18,13 +19,15 @@ interface Props {
fieldName: string;
}
export const InputFieldViewNodes = memo(({ nodeId, fieldName }: Props) => {
const fieldTemplate = useFieldInputTemplate(nodeId, fieldName);
export const InputFieldEditModeNodes = memo(({ nodeId, fieldName }: Props) => {
const fieldTemplate = useInputFieldTemplate(nodeId, fieldName);
const [isHovered, setIsHovered] = useState(false);
const isInvalid = useFieldIsInvalid(nodeId, fieldName);
const { isConnected, isConnectionInProgress, isConnectionStartField, validationResult, shouldDim } =
useConnectionState(nodeId, fieldName, 'inputs');
const isInvalid = useInputFieldIsInvalid(nodeId, fieldName);
const isConnected = useInputFieldIsConnected(nodeId, fieldName);
const { isConnectionInProgress, isConnectionStartField, validationResult } = useInputFieldConnectionState(
nodeId,
fieldName
);
const onMouseEnter = useCallback(() => {
setIsHovered(true);
@@ -36,15 +39,13 @@ export const InputFieldViewNodes = memo(({ nodeId, fieldName }: Props) => {
if (fieldTemplate.input === 'connection' || isConnected) {
return (
<InputFieldWrapper shouldDim={shouldDim}>
<InputFieldWrapper>
<FormControl isInvalid={isInvalid} isDisabled={isConnected} px={2}>
<EditableFieldTitle
<InputFieldTitle
nodeId={nodeId}
fieldName={fieldName}
kind="inputs"
isInvalid={isInvalid}
withTooltip
shouldDim
isDisabled={(isConnectionInProgress && !validationResult.isValid && !isConnectionStartField) || isConnected}
/>
</FormControl>
@@ -60,7 +61,7 @@ export const InputFieldViewNodes = memo(({ nodeId, fieldName }: Props) => {
}
return (
<InputFieldWrapper shouldDim={shouldDim}>
<InputFieldWrapper>
<FormControl
isInvalid={isInvalid}
isDisabled={isConnected}
@@ -72,13 +73,13 @@ export const InputFieldViewNodes = memo(({ nodeId, fieldName }: Props) => {
>
<Flex flexDir="column" w="full" gap={1} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
<Flex gap={1}>
<EditableFieldTitle nodeId={nodeId} fieldName={fieldName} kind="inputs" isInvalid={isInvalid} withTooltip />
<InputFieldTitle nodeId={nodeId} fieldName={fieldName} isInvalid={isInvalid} />
{isHovered && (
<>
<FieldLinearViewConfigIconButton nodeId={nodeId} fieldName={fieldName} />
<FieldNotesIconButton nodeId={nodeId} fieldName={fieldName} />
<FieldResetToDefaultValueButton nodeId={nodeId} fieldName={fieldName} />
<FieldLinearViewToggle nodeId={nodeId} fieldName={fieldName} />
<InputFieldLinearViewConfigIconButton nodeId={nodeId} fieldName={fieldName} />
<InputFieldNotesIconButton nodeId={nodeId} fieldName={fieldName} />
<InputFieldResetToDefaultValueIconButton nodeId={nodeId} fieldName={fieldName} />
<InputFieldAddRemoveLinearViewIconButton nodeId={nodeId} fieldName={fieldName} />
</>
)}
</Flex>
@@ -99,4 +100,4 @@ export const InputFieldViewNodes = memo(({ nodeId, fieldName }: Props) => {
);
});
InputFieldViewNodes.displayName = 'InputFieldViewNodes';
InputFieldEditModeNodes.displayName = 'InputFieldEditModeNodes';

View File

@@ -1,6 +1,6 @@
import { InputFieldUnknownPlaceholder } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldUnknownPlaceholder';
import { useFieldInputInstanceExists } from 'features/nodes/hooks/useFieldInputInstanceExists';
import { useFieldInputTemplateExists } from 'features/nodes/hooks/useFieldInputTemplateExists';
import { useInputFieldInstanceExists } from 'features/nodes/hooks/useInputFieldInstanceExists';
import { useInputFieldTemplateExists } from 'features/nodes/hooks/useInputFieldTemplateExists';
import type { PropsWithChildren } from 'react';
import { memo } from 'react';
@@ -10,8 +10,8 @@ type Props = PropsWithChildren<{
}>;
export const InputFieldGate = memo(({ nodeId, fieldName, children }: Props) => {
const hasInstance = useFieldInputInstanceExists(nodeId, fieldName);
const hasTemplate = useFieldInputTemplateExists(nodeId, fieldName);
const hasInstance = useInputFieldInstanceExists(nodeId, fieldName);
const hasTemplate = useInputFieldTemplateExists(nodeId, fieldName);
if (!hasTemplate || !hasInstance) {
return <InputFieldUnknownPlaceholder nodeId={nodeId} fieldName={fieldName} />;

View File

@@ -8,8 +8,8 @@ import {
Select,
} from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useFieldIsExposed } from 'features/nodes/hooks/useFieldIsExposed';
import { useFieldLinearViewConfig } from 'features/nodes/hooks/useFieldLinearViewConfig';
import { useInputFieldIsExposed } from 'features/nodes/hooks/useInputFieldIsExposed';
import { useInputFieldLinearViewConfig } from 'features/nodes/hooks/useInputFieldLinearViewConfig';
import { fieldLinearViewConfigChanged } from 'features/nodes/store/nodesSlice';
import type { FieldInputInstance } from 'features/nodes/types/field';
import type { ChangeEvent } from 'react';
@@ -37,11 +37,11 @@ const parseNotesDisplay = (
}
};
export const FieldLinearViewConfigIconButton = memo(({ nodeId, fieldName }: Props) => {
export const InputFieldLinearViewConfigIconButton = memo(({ nodeId, fieldName }: Props) => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const isExposed = useFieldIsExposed(nodeId, fieldName);
const linearViewConfig = useFieldLinearViewConfig(nodeId, fieldName);
const isExposed = useInputFieldIsExposed(nodeId, fieldName);
const linearViewConfig = useInputFieldLinearViewConfig(nodeId, fieldName);
const onChangeNotesDisplay = useCallback(
(e: ChangeEvent<HTMLSelectElement>) => {
const notesDisplay = parseNotesDisplay(e.target.value);
@@ -82,4 +82,4 @@ export const FieldLinearViewConfigIconButton = memo(({ nodeId, fieldName }: Prop
);
});
FieldLinearViewConfigIconButton.displayName = 'FieldLinearViewConfigIconButton';
InputFieldLinearViewConfigIconButton.displayName = 'InputFieldLinearViewConfigIconButton';

View File

@@ -10,7 +10,7 @@ import {
Textarea,
} from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useFieldNotes } from 'features/nodes/hooks/useFieldNotes';
import { useInputFieldNotes } from 'features/nodes/hooks/useInputFieldNotes';
import { fieldNotesChanged } from 'features/nodes/store/nodesSlice';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
@@ -23,10 +23,10 @@ type Props = {
readOnly?: boolean;
};
export const FieldNotesIconButton = memo(({ nodeId, fieldName, readOnly }: Props) => {
export const InputFieldNotesIconButton = memo(({ nodeId, fieldName, readOnly }: Props) => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const notes = useFieldNotes(nodeId, fieldName);
const notes = useInputFieldNotes(nodeId, fieldName);
const onChange = useCallback(
(e: ChangeEvent<HTMLTextAreaElement>) => {
dispatch(fieldNotesChanged({ nodeId, fieldName, val: e.target.value }));
@@ -75,4 +75,4 @@ export const FieldNotesIconButton = memo(({ nodeId, fieldName, readOnly }: Props
);
});
FieldNotesIconButton.displayName = 'FieldNotesIconButton';
InputFieldNotesIconButton.displayName = 'InputFieldNotesIconButton';

View File

@@ -5,8 +5,8 @@ import ModelIdentifierFieldInputComponent from 'features/nodes/components/flow/n
import { NumberFieldCollectionInputComponent } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/NumberFieldCollectionInputComponent';
import { StringFieldCollectionInputComponent } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/StringFieldCollectionInputComponent';
import { StringGeneratorFieldInputComponent } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/StringGeneratorFieldComponent';
import { useFieldInputInstance } from 'features/nodes/hooks/useFieldInputInstance';
import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate';
import { useInputFieldInstance } from 'features/nodes/hooks/useInputFieldInstance';
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
import {
isBoardFieldInputInstance,
isBoardFieldInputTemplate,
@@ -110,9 +110,9 @@ type InputFieldProps = {
fieldName: string;
};
const InputFieldRenderer = ({ nodeId, fieldName }: InputFieldProps) => {
const fieldInstance = useFieldInputInstance(nodeId, fieldName);
const fieldTemplate = useFieldInputTemplate(nodeId, fieldName);
export const InputFieldRenderer = memo(({ nodeId, fieldName }: InputFieldProps) => {
const fieldInstance = useInputFieldInstance(nodeId, fieldName);
const fieldTemplate = useInputFieldTemplate(nodeId, fieldName);
if (isStringFieldCollectionInputInstance(fieldInstance) && isStringFieldCollectionInputTemplate(fieldTemplate)) {
return <StringFieldCollectionInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
@@ -258,10 +258,7 @@ const InputFieldRenderer = ({ nodeId, fieldName }: InputFieldProps) => {
return <StringGeneratorFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
}
if (fieldTemplate) {
// Fallback for when there is no component for the type
return null;
}
};
return null;
});
export default memo(InputFieldRenderer);
InputFieldRenderer.displayName = 'InputFieldRenderer';

View File

@@ -0,0 +1,30 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useInputFieldDefaultValue } from 'features/nodes/hooks/useInputFieldDefaultValue';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
type Props = {
nodeId: string;
fieldName: string;
};
export const InputFieldResetToDefaultValueIconButton = memo(({ nodeId, fieldName }: Props) => {
const { t } = useTranslation();
const { isValueChanged, resetToDefaultValue } = useInputFieldDefaultValue(nodeId, fieldName);
return (
<IconButton
variant="ghost"
tooltip={t('nodes.resetToDefaultValue')}
aria-label={t('nodes.resetToDefaultValue')}
icon={<PiArrowCounterClockwiseBold />}
pointerEvents="auto"
size="xs"
onClick={resetToDefaultValue}
isDisabled={!isValueChanged}
/>
);
});
InputFieldResetToDefaultValueIconButton.displayName = 'InputFieldResetToDefaultValueIconButton';

View File

@@ -0,0 +1,30 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useInputFieldInitialLinearViewValue } from 'features/nodes/hooks/useInputFieldInitialLinearViewValue';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
type Props = {
nodeId: string;
fieldName: string;
};
const InputFieldResetToInitialValueIconButton = ({ nodeId, fieldName }: Props) => {
const { t } = useTranslation();
const { isValueChanged, resetToInitialLinearViewValue } = useInputFieldInitialLinearViewValue(nodeId, fieldName);
return (
<IconButton
variant="ghost"
tooltip={t('nodes.resetToDefaultValue')}
aria-label={t('nodes.resetToDefaultValue')}
icon={<PiArrowCounterClockwiseBold />}
pointerEvents="auto"
size="xs"
onClick={resetToInitialLinearViewValue}
isDisabled={!isValueChanged}
/>
);
};
export default memo(InputFieldResetToInitialValueIconButton);

View File

@@ -1,37 +1,54 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import {
Editable,
EditableInput,
EditablePreview,
Flex,
forwardRef,
Tooltip,
useEditableControls,
} from '@invoke-ai/ui-library';
import { Editable, EditableInput, EditablePreview, Flex, Tooltip, useEditableControls } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useFieldLabel } from 'features/nodes/hooks/useFieldLabel';
import { useFieldTemplateTitle } from 'features/nodes/hooks/useFieldTemplateTitle';
import { InputFieldTooltip } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldTooltip';
import { useInputFieldLabel } from 'features/nodes/hooks/useInputFieldLabel';
import { useInputFieldTemplateTitle } from 'features/nodes/hooks/useInputFieldTemplateTitle';
import { fieldLabelChanged } from 'features/nodes/store/nodesSlice';
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
import type { MouseEvent } from 'react';
import { memo, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import FieldTooltipContent from './FieldTooltipContent';
const editablePreviewStyles: SystemStyleObject = {
p: 0,
fontWeight: 'semibold',
textAlign: 'left',
color: 'base.300',
_hover: {
fontWeight: 'semibold !important',
},
'&[data-is-invalid="true"]': {
color: 'error.300',
},
'&[data-is-disabled="true"]': {
opacity: 0.5,
},
};
const editableInputStyles: SystemStyleObject = {
p: 0,
w: 'full',
fontWeight: 'semibold',
color: 'base.100',
_focusVisible: {
p: 0,
textAlign: 'left',
boxShadow: 'none',
},
};
interface Props {
nodeId: string;
fieldName: string;
kind: 'inputs' | 'outputs';
isInvalid?: boolean;
withTooltip?: boolean;
shouldDim?: boolean;
isDisabled?: boolean;
}
const EditableFieldTitle = forwardRef((props: Props, ref) => {
const { nodeId, fieldName, kind, isInvalid = false, withTooltip = false, shouldDim = false } = props;
const label = useFieldLabel(nodeId, fieldName);
const fieldTemplateTitle = useFieldTemplateTitle(nodeId, fieldName, kind);
export const InputFieldTitle = memo((props: Props) => {
const { nodeId, fieldName, isInvalid, isDisabled } = props;
const label = useInputFieldLabel(nodeId, fieldName);
const fieldTemplateTitle = useInputFieldTemplateTitle(nodeId, fieldName);
const { t } = useTranslation();
const dispatch = useAppDispatch();
@@ -62,7 +79,6 @@ const EditableFieldTitle = forwardRef((props: Props, ref) => {
onChange={handleChange}
onSubmit={handleSubmit}
as={Flex}
ref={ref}
position="relative"
overflow="hidden"
alignItems="center"
@@ -71,15 +87,14 @@ const EditableFieldTitle = forwardRef((props: Props, ref) => {
w="full"
>
<Tooltip
label={withTooltip ? <FieldTooltipContent nodeId={nodeId} fieldName={fieldName} kind="inputs" /> : undefined}
label={<InputFieldTooltip nodeId={nodeId} fieldName={fieldName} />}
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
>
<EditablePreview
fontWeight="semibold"
sx={editablePreviewStyles}
noOfLines={1}
color={isInvalid ? 'error.300' : 'base.300'}
opacity={shouldDim ? 0.5 : 1}
data-is-invalid={isInvalid}
data-is-disabled={isDisabled}
/>
</Tooltip>
<EditableInput className="nodrag" sx={editableInputStyles} />
@@ -88,26 +103,7 @@ const EditableFieldTitle = forwardRef((props: Props, ref) => {
);
});
const editableInputStyles: SystemStyleObject = {
p: 0,
w: 'full',
fontWeight: 'semibold',
color: 'base.100',
_focusVisible: {
p: 0,
textAlign: 'left',
boxShadow: 'none',
},
};
const editablePreviewStyles: SystemStyleObject = {
p: 0,
textAlign: 'left',
_hover: {
fontWeight: 'semibold !important',
},
};
export default memo(EditableFieldTitle);
InputFieldTitle.displayName = 'InputFieldTitle';
const EditableControls = memo(() => {
const { isEditing, getEditButtonProps } = useEditableControls();

View File

@@ -0,0 +1,49 @@
import { Flex, Text } from '@invoke-ai/ui-library';
import { useInputFieldInstance } from 'features/nodes/hooks/useInputFieldInstance';
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
import { useFieldTypeName } from 'features/nodes/hooks/usePrettyFieldType';
import { startCase } from 'lodash-es';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
interface Props {
nodeId: string;
fieldName: string;
}
export const InputFieldTooltip = memo(({ nodeId, fieldName }: Props) => {
const { t } = useTranslation();
const fieldInstance = useInputFieldInstance(nodeId, fieldName);
const fieldTemplate = useInputFieldTemplate(nodeId, fieldName);
const fieldTypeName = useFieldTypeName(fieldTemplate.type);
const fieldTitle = useMemo(() => {
if (fieldInstance.label && fieldTemplate.title) {
return `${fieldInstance.label} (${fieldTemplate.title})`;
}
if (fieldInstance.label && !fieldTemplate.title) {
return fieldInstance.label;
}
return fieldTemplate.title;
}, [fieldInstance, fieldTemplate]);
return (
<Flex flexDir="column">
<Text fontWeight="semibold">{fieldTitle}</Text>
<Text opacity={0.7} fontStyle="oblique 5deg">
{fieldTemplate.description}
</Text>
<Text>
{t('parameters.type')}: {fieldTypeName}
</Text>
<Text>
{t('common.input')}: {startCase(fieldTemplate.input)}
</Text>
</Flex>
);
});
InputFieldTooltip.displayName = 'FieldTooltipContent';

View File

@@ -1,6 +1,6 @@
import { FormControl, FormLabel } from '@invoke-ai/ui-library';
import { InputFieldWrapper } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldWrapper';
import { useFieldInputName } from 'features/nodes/hooks/useFieldInputName';
import { useInputFieldName } from 'features/nodes/hooks/useInputFieldName';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -11,10 +11,10 @@ type Props = {
export const InputFieldUnknownPlaceholder = memo(({ nodeId, fieldName }: Props) => {
const { t } = useTranslation();
const name = useFieldInputName(nodeId, fieldName);
const name = useInputFieldName(nodeId, fieldName);
return (
<InputFieldWrapper shouldDim={false}>
<InputFieldWrapper>
<FormControl isInvalid={true} alignItems="stretch" justifyContent="center" gap={2} h="full" w="full">
<FormLabel display="flex" mb={0} px={1} py={2} gap={2}>
{t('nodes.unknownInput', { name })}

View File

@@ -0,0 +1,31 @@
import { Flex, FormLabel, Spacer } from '@invoke-ai/ui-library';
import { InputFieldNotesIconButton } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldNotesIconButton';
import { InputFieldRenderer } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer';
import InputFieldResetToInitialValueIconButton from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldResetToInitialValueIconButton';
import { useInputFieldLabel } from 'features/nodes/hooks/useInputFieldLabel';
import { useInputFieldTemplateTitle } from 'features/nodes/hooks/useInputFieldTemplateTitle';
import { memo } from 'react';
type Props = {
nodeId: string;
fieldName: string;
};
export const InputFieldViewMode = memo(({ nodeId, fieldName }: Props) => {
const label = useInputFieldLabel(nodeId, fieldName);
const fieldTemplateTitle = useInputFieldTemplateTitle(nodeId, fieldName);
return (
<Flex position="relative" w="full" gap="2" flexDir="column">
<Flex alignItems="center" gap={1}>
<FormLabel fontSize="sm">{label || fieldTemplateTitle}</FormLabel>
<Spacer />
<InputFieldResetToInitialValueIconButton nodeId={nodeId} fieldName={fieldName} />
<InputFieldNotesIconButton nodeId={nodeId} fieldName={fieldName} readOnly />
</Flex>
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
</Flex>
);
});
InputFieldViewMode.displayName = 'InputFieldViewMode';

View File

@@ -1,31 +0,0 @@
import { Flex, FormLabel, Spacer } from '@invoke-ai/ui-library';
import { FieldNotesIconButton } from 'features/nodes/components/flow/nodes/Invocation/fields/FieldNotesIconButton';
import FieldResetToInitialLinearViewValueButton from 'features/nodes/components/flow/nodes/Invocation/fields/FieldResetToInitialLinearViewValueButton';
import InputFieldRenderer from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer';
import { useFieldLabel } from 'features/nodes/hooks/useFieldLabel';
import { useFieldTemplateTitle } from 'features/nodes/hooks/useFieldTemplateTitle';
import { memo } from 'react';
type Props = {
nodeId: string;
fieldName: string;
};
export const InputFieldViewSimple = memo(({ nodeId, fieldName }: Props) => {
const label = useFieldLabel(nodeId, fieldName);
const fieldTemplateTitle = useFieldTemplateTitle(nodeId, fieldName, 'inputs');
return (
<Flex position="relative" w="full" gap="2" flexDir="column">
<Flex alignItems="center" gap={1}>
<FormLabel fontSize="sm">{label || fieldTemplateTitle}</FormLabel>
<Spacer />
<FieldResetToInitialLinearViewValueButton nodeId={nodeId} fieldName={fieldName} />
<FieldNotesIconButton nodeId={nodeId} fieldName={fieldName} readOnly />
</Flex>
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
</Flex>
);
});
InputFieldViewSimple.displayName = 'InputFieldViewSimple';

View File

@@ -12,21 +12,10 @@ const sx = {
transitionDuration: '0.1s',
w: 'full',
h: 'full',
'&[data-should-dim="true"]': {
opacity: 0.5,
},
} satisfies SystemStyleObject;
type InputFieldWrapperProps = PropsWithChildren<{
shouldDim: boolean;
}>;
export const InputFieldWrapper = memo(({ shouldDim, children }: InputFieldWrapperProps) => {
return (
<Flex sx={sx} data-should-dim={shouldDim}>
{children}
</Flex>
);
export const InputFieldWrapper = memo(({ children }: PropsWithChildren) => {
return <Flex sx={sx}>{children}</Flex>;
});
InputFieldWrapper.displayName = 'InputFieldWrapper';

View File

@@ -1,5 +1,5 @@
import { OutputFieldUnknownPlaceholder } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldUnknownPlaceholder';
import { useFieldOutputTemplateExists } from 'features/nodes/hooks/useFieldOutputTemplateExists';
import { useOutputFieldTemplateExists } from 'features/nodes/hooks/useOutputFieldTemplateExists';
import type { PropsWithChildren } from 'react';
import { memo } from 'react';
@@ -9,7 +9,7 @@ type Props = PropsWithChildren<{
}>;
export const OutputFieldGate = memo(({ nodeId, fieldName, children }: Props) => {
const hasTemplate = useFieldOutputTemplateExists(nodeId, fieldName);
const hasTemplate = useOutputFieldTemplateExists(nodeId, fieldName);
if (!hasTemplate) {
return <OutputFieldUnknownPlaceholder nodeId={nodeId} fieldName={fieldName} />;

View File

@@ -1,37 +1,32 @@
import { FormControl, FormLabel, Tooltip } from '@invoke-ai/ui-library';
import { FieldHandle } from 'features/nodes/components/flow/nodes/Invocation/fields/FieldHandle';
import { OutputFieldTitle } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldTitle';
import { OutputFieldWrapper } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldWrapper';
import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
import { useFieldOutputTemplate } from 'features/nodes/hooks/useFieldOutputTemplate';
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
import { useOutputFieldConnectionState } from 'features/nodes/hooks/useOutputFieldConnectionState';
import { useOutputFieldIsConnected } from 'features/nodes/hooks/useOutputFieldIsConnected';
import { useOutputFieldTemplate } from 'features/nodes/hooks/useOutputFieldTemplate';
import type { PropsWithChildren } from 'react';
import { memo } from 'react';
import FieldTooltipContent from './FieldTooltipContent';
type Props = PropsWithChildren<{
nodeId: string;
fieldName: string;
}>;
export const OutputFieldNodesEditorView = memo(({ nodeId, fieldName }: Props) => {
const fieldTemplate = useFieldOutputTemplate(nodeId, fieldName);
const { isConnected, isConnectionInProgress, isConnectionStartField, validationResult, shouldDim } =
useConnectionState(nodeId, fieldName, 'outputs');
const fieldTemplate = useOutputFieldTemplate(nodeId, fieldName);
const isConnected = useOutputFieldIsConnected(nodeId, fieldName);
const { isConnectionInProgress, isConnectionStartField, validationResult } = useOutputFieldConnectionState(
nodeId,
fieldName
);
return (
<OutputFieldWrapper shouldDim={shouldDim}>
<Tooltip
label={<FieldTooltipContent nodeId={nodeId} fieldName={fieldName} kind="outputs" />}
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
placement="top"
shouldWrapChildren
>
<FormControl isDisabled={isConnected}>
<FormLabel mb={0}>{fieldTemplate?.title}</FormLabel>
</FormControl>
</Tooltip>
<OutputFieldWrapper>
<OutputFieldTitle
nodeId={nodeId}
fieldName={fieldName}
isDisabled={(isConnectionInProgress && !validationResult.isValid && !isConnectionStartField) || isConnected}
/>
<FieldHandle
handleType="source"
fieldTemplate={fieldTemplate}

View File

@@ -0,0 +1,43 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Text, Tooltip } from '@invoke-ai/ui-library';
import { useOutputFieldTemplate } from 'features/nodes/hooks/useOutputFieldTemplate';
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
import type { PropsWithChildren } from 'react';
import { memo } from 'react';
import { OutputFieldTooltip } from './OutputFieldTooltip';
const sx = {
fontSize: 'sm',
color: 'base.300',
fontWeight: 'semibold',
pe: 2,
'&[data-is-disabled="true"]': {
opacity: 0.5,
},
} satisfies SystemStyleObject;
type Props = PropsWithChildren<{
nodeId: string;
fieldName: string;
isDisabled?: boolean;
}>;
export const OutputFieldTitle = memo(({ nodeId, fieldName, isDisabled }: Props) => {
const fieldTemplate = useOutputFieldTemplate(nodeId, fieldName);
return (
<Tooltip
label={<OutputFieldTooltip nodeId={nodeId} fieldName={fieldName} />}
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
placement="top"
shouldWrapChildren
>
<Text data-is-disabled={isDisabled} sx={sx}>
{fieldTemplate.title}
</Text>
</Tooltip>
);
});
OutputFieldTitle.displayName = 'OutputFieldTitle';

View File

@@ -0,0 +1,29 @@
import { Flex, Text } from '@invoke-ai/ui-library';
import { useOutputFieldTemplate } from 'features/nodes/hooks/useOutputFieldTemplate';
import { useFieldTypeName } from 'features/nodes/hooks/usePrettyFieldType';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
interface Props {
nodeId: string;
fieldName: string;
}
export const OutputFieldTooltip = memo(({ nodeId, fieldName }: Props) => {
const fieldTemplate = useOutputFieldTemplate(nodeId, fieldName);
const fieldTypeName = useFieldTypeName(fieldTemplate.type);
const { t } = useTranslation();
return (
<Flex flexDir="column">
<Text fontWeight="semibold">{fieldTemplate.title}</Text>
<Text opacity={0.7} fontStyle="oblique 5deg">
{fieldTemplate.description}
</Text>
<Text>
{t('parameters.type')}: {fieldTypeName}
</Text>
</Flex>
);
});
OutputFieldTooltip.displayName = 'OutputFieldTooltip';

View File

@@ -1,6 +1,6 @@
import { FormControl, FormLabel } from '@invoke-ai/ui-library';
import { OutputFieldWrapper } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldWrapper';
import { useFieldOutputName } from 'features/nodes/hooks/useFieldOutputName';
import { useOutputFieldName } from 'features/nodes/hooks/useOutputFieldName';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -11,10 +11,10 @@ type Props = {
export const OutputFieldUnknownPlaceholder = memo(({ nodeId, fieldName }: Props) => {
const { t } = useTranslation();
const name = useFieldOutputName(nodeId, fieldName);
const name = useOutputFieldName(nodeId, fieldName);
return (
<OutputFieldWrapper shouldDim={false}>
<OutputFieldWrapper>
<FormControl isInvalid={true} alignItems="stretch" justifyContent="space-between" gap={2} h="full" w="full">
<FormLabel display="flex" alignItems="center" h="full" color="error.300" mb={0} px={1} gap={2}>
{t('nodes.unknownOutput', { name })}

View File

@@ -11,19 +11,8 @@ const sx = {
transitionProperty: 'opacity',
transitionDuration: '0.1s',
justifyContent: 'flex-end',
'&[data-should-dim="true"]': {
opacity: 0.5,
},
} satisfies SystemStyleObject;
type OutputFieldWrapperProps = PropsWithChildren<{
shouldDim: boolean;
}>;
export const OutputFieldWrapper = memo(({ shouldDim, children }: OutputFieldWrapperProps) => (
<Flex sx={sx} data-should-dim={shouldDim}>
{children}
</Flex>
));
export const OutputFieldWrapper = memo(({ children }: PropsWithChildren) => <Flex sx={sx}>{children}</Flex>);
OutputFieldWrapper.displayName = 'OutputFieldWrapper';

View File

@@ -10,7 +10,7 @@ import { addImagesToNodeImageFieldCollectionDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
import { DndImage } from 'features/dnd/DndImage';
import { DndImageIcon } from 'features/dnd/DndImageIcon';
import { useFieldIsInvalid } from 'features/nodes/hooks/useFieldIsInvalid';
import { useInputFieldIsInvalid } from 'features/nodes/hooks/useInputFieldIsInvalid';
import { fieldImageCollectionValueChanged } from 'features/nodes/store/nodesSlice';
import type { ImageField } from 'features/nodes/types/common';
import type { ImageFieldCollectionInputInstance, ImageFieldCollectionInputTemplate } from 'features/nodes/types/field';
@@ -39,7 +39,7 @@ export const ImageFieldCollectionInputComponent = memo(
const { nodeId, field } = props;
const store = useAppStore();
const isInvalid = useFieldIsInvalid(nodeId, field.name);
const isInvalid = useInputFieldIsInvalid(nodeId, field.name);
const dndTargetData = useMemo<AddImagesToNodeImageFieldCollection>(
() =>

View File

@@ -12,7 +12,7 @@ import {
import { NUMPY_RAND_MAX } from 'app/constants';
import { useAppStore } from 'app/store/nanostores/store';
import { getOverlayScrollbarsParams, overlayScrollbarsStyles } from 'common/components/OverlayScrollbars/constants';
import { useFieldIsInvalid } from 'features/nodes/hooks/useFieldIsInvalid';
import { useInputFieldIsInvalid } from 'features/nodes/hooks/useInputFieldIsInvalid';
import { fieldNumberCollectionValueChanged } from 'features/nodes/store/nodesSlice';
import type {
FloatFieldCollectionInputInstance,
@@ -48,7 +48,7 @@ export const NumberFieldCollectionInputComponent = memo(
const store = useAppStore();
const { t } = useTranslation();
const isInvalid = useFieldIsInvalid(nodeId, field.name);
const isInvalid = useInputFieldIsInvalid(nodeId, field.name);
const isIntegerField = useMemo(() => fieldTemplate.type.name === 'IntegerField', [fieldTemplate.type]);
const onRemoveNumber = useCallback(

View File

@@ -2,7 +2,7 @@ import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Button, Divider, Flex, FormLabel, Grid, GridItem, IconButton, Input } from '@invoke-ai/ui-library';
import { useAppStore } from 'app/store/nanostores/store';
import { getOverlayScrollbarsParams, overlayScrollbarsStyles } from 'common/components/OverlayScrollbars/constants';
import { useFieldIsInvalid } from 'features/nodes/hooks/useFieldIsInvalid';
import { useInputFieldIsInvalid } from 'features/nodes/hooks/useInputFieldIsInvalid';
import { fieldStringCollectionValueChanged } from 'features/nodes/store/nodesSlice';
import type {
StringFieldCollectionInputInstance,
@@ -32,7 +32,7 @@ export const StringFieldCollectionInputComponent = memo(
const { t } = useTranslation();
const store = useAppStore();
const isInvalid = useFieldIsInvalid(nodeId, field.name);
const isInvalid = useInputFieldIsInvalid(nodeId, field.name);
const onRemoveString = useCallback(
(index: number) => {

View File

@@ -2,7 +2,7 @@ import type { ChakraProps } from '@invoke-ai/ui-library';
import { Box, useGlobalMenuClose, useToken } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector, useAppStore } from 'app/store/storeHooks';
import NodeSelectionOverlay from 'common/components/NodeSelectionOverlay';
import { useExecutionState } from 'features/nodes/hooks/useExecutionState';
import { useNodeExecutionState } from 'features/nodes/hooks/useNodeExecutionState';
import { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
import { nodesChanged } from 'features/nodes/store/nodesSlice';
import { selectNodes } from 'features/nodes/store/selectors';
@@ -24,7 +24,7 @@ const NodeWrapper = (props: NodeWrapperProps) => {
const store = useAppStore();
const { isMouseOverNode, handleMouseOut, handleMouseOver } = useMouseOverNode(nodeId);
const executionState = useExecutionState(nodeId);
const executionState = useNodeExecutionState(nodeId);
const isInProgress = executionState?.status === zNodeStatus.enum.IN_PROGRESS;
const [nodeInProgress, shadowsXl, shadowsBase] = useToken('shadows', [

View File

@@ -13,7 +13,7 @@ import WorkflowFieldsLinearViewPanel from './workflow/WorkflowPanel';
const panelGroupStyles: CSSProperties = { height: '100%', width: '100%' };
export const LinearViewLeftPanelContent = memo(() => {
export const EditModeLeftPanelContent = memo(() => {
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
const handleDoubleClickHandle = useCallback(() => {
@@ -46,4 +46,4 @@ export const LinearViewLeftPanelContent = memo(() => {
);
});
LinearViewLeftPanelContent.displayName = 'LinearViewLeftPanelContent';
EditModeLeftPanelContent.displayName = 'EditModeLeftPanelContent';

View File

@@ -2,12 +2,12 @@ import 'reactflow/dist/style.css';
import { Box, Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { LinearViewLeftPanelContent } from 'features/nodes/components/sidePanel/LinearViewLeftPanelContent';
import { EditModeLeftPanelContent } from 'features/nodes/components/sidePanel/EditModeLeftPanelContent';
import { useWorkflowListMenu } from 'features/nodes/store/workflowListMenu';
import { selectWorkflowMode } from 'features/nodes/store/workflowSlice';
import { memo } from 'react';
import { SimpleViewLeftPanelContent } from './viewMode/SimpleViewLeftPanelContent';
import { ViewModeLeftPanelContent } from './viewMode/ViewModeLeftPanelContent';
import { WorkflowListMenu } from './WorkflowListMenu/WorkflowListMenu';
import { WorkflowListMenuTrigger } from './WorkflowListMenu/WorkflowListMenuTrigger';
@@ -21,8 +21,8 @@ const WorkflowsTabLeftPanel = () => {
<Flex w="full" h="full" position="relative">
<Box position="absolute" top={0} left={0} right={0} bottom={0}>
{workflowListMenu.isOpen && <WorkflowListMenu />}
{mode === 'view' && <SimpleViewLeftPanelContent />}
{mode === 'edit' && <LinearViewLeftPanelContent />}
{mode === 'view' && <ViewModeLeftPanelContent />}
{mode === 'edit' && <EditModeLeftPanelContent />}
</Box>
</Flex>
</Flex>

View File

@@ -4,7 +4,8 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import NotesTextarea from 'features/nodes/components/flow/nodes/Invocation/NotesTextarea';
import { InvocationNodeNotesTextarea } from 'features/nodes/components/flow/nodes/Invocation/InvocationNodeNotesTextarea';
import { useNodeIsInvocationNode } from 'features/nodes/hooks/useNodeIsInvocationNode';
import { useNodeNeedsUpdate } from 'features/nodes/hooks/useNodeNeedsUpdate';
import { $templates } from 'features/nodes/store/nodesSlice';
import { selectLastSelectedNode, selectNodesSlice } from 'features/nodes/store/selectors';
@@ -55,6 +56,7 @@ type ContentProps = {
const Content = memo((props: ContentProps) => {
const { t } = useTranslation();
const needsUpdate = useNodeNeedsUpdate(props.nodeId);
const isInvocationNode = useNodeIsInvocationNode(props.nodeId);
return (
<Box position="relative" w="full" h="full">
<ScrollableContent>
@@ -74,7 +76,7 @@ const Content = memo((props: ContentProps) => {
</Text>
</FormControl>
</HStack>
<NotesTextarea nodeId={props.nodeId} />
{isInvocationNode && <InvocationNodeNotesTextarea nodeId={props.nodeId} />}
</Flex>
</ScrollableContent>
</Box>

View File

@@ -5,7 +5,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
import { useExecutionState } from 'features/nodes/hooks/useExecutionState';
import { useNodeExecutionState } from 'features/nodes/hooks/useNodeExecutionState';
import { $templates } from 'features/nodes/store/nodesSlice';
import { selectLastSelectedNode, selectNodesSlice } from 'features/nodes/store/selectors';
import { isInvocationNode } from 'features/nodes/types/invocation';
@@ -35,7 +35,7 @@ const InspectorOutputsTab = () => {
[templates]
);
const data = useAppSelector(selector);
const nes = useExecutionState(data?.nodeId);
const nes = useNodeExecutionState(data?.nodeId);
const { t } = useTranslation();
if (!data || !nes) {

View File

@@ -4,7 +4,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { InputFieldGate } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate';
import { InputFieldViewSimple } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldViewSimple';
import { InputFieldViewMode } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldViewMode';
import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
import { t } from 'i18next';
import { memo } from 'react';
@@ -14,20 +14,20 @@ import { EmptyState } from './EmptyState';
const selectExposedFields = createMemoizedSelector(selectWorkflowSlice, (workflow) => workflow.exposedFields);
export const SimpleViewLeftPanelContent = memo(() => {
export const ViewModeLeftPanelContent = memo(() => {
return (
<Box position="relative" w="full" h="full">
<ScrollableContent>
<Flex position="relative" flexDir="column" alignItems="flex-start" p={1} gap={2} w="full" h="full">
<SimpleViewLeftPanelContentInner />
<ViewModeLeftPanelContentInner />
</Flex>
</ScrollableContent>
</Box>
);
});
SimpleViewLeftPanelContent.displayName = 'SimpleViewLeftPanelContent';
ViewModeLeftPanelContent.displayName = 'ViewModeLeftPanelContent';
const SimpleViewLeftPanelContentInner = memo(() => {
const ViewModeLeftPanelContentInner = memo(() => {
const { isLoading } = useGetOpenAPISchemaQuery();
const exposedFields = useAppSelector(selectExposedFields);
@@ -43,10 +43,10 @@ const SimpleViewLeftPanelContentInner = memo(() => {
<>
{exposedFields.map(({ nodeId, fieldName }) => (
<InputFieldGate key={`${nodeId}.${fieldName}`} nodeId={nodeId} fieldName={fieldName}>
<InputFieldViewSimple nodeId={nodeId} fieldName={fieldName} />
<InputFieldViewMode nodeId={nodeId} fieldName={fieldName} />
</InputFieldGate>
))}
</>
);
});
SimpleViewLeftPanelContentInner.displayName = ' SimpleViewLeftPanelContentInner';
ViewModeLeftPanelContentInner.displayName = ' ViewModeLeftPanelContentInner';

View File

@@ -10,8 +10,8 @@ import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
import { deepClone } from 'common/util/deepClone';
import { singleWorkflowFieldDndSource } from 'features/dnd/dnd';
import { triggerPostMoveFlash } from 'features/dnd/util';
import { InputFieldEditModeLinear } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldEditModeLinear';
import { InputFieldGate } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate';
import { InputFieldViewLinear } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldViewLinear';
import { selectWorkflowSlice, workflowExposedFieldsReordered } from 'features/nodes/store/workflowSlice';
import type { FieldIdentifier } from 'features/nodes/types/field';
import { isEqual } from 'lodash-es';
@@ -143,7 +143,7 @@ const FieldListInnerContent = memo(({ fields }: { fields: FieldIdentifier[] }) =
<>
{fields.map(({ nodeId, fieldName }) => (
<InputFieldGate key={`${nodeId}.${fieldName}`} nodeId={nodeId} fieldName={fieldName}>
<InputFieldViewLinear nodeId={nodeId} fieldName={fieldName} />
<InputFieldEditModeLinear nodeId={nodeId} fieldName={fieldName} />
</InputFieldGate>
))}
</>

View File

@@ -1,19 +1,24 @@
import { useNode } from 'features/nodes/hooks/useNode';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNode, selectNodesSlice } from 'features/nodes/store/selectors';
import { isBatchNode, isInvocationNode } from 'features/nodes/types/invocation';
import { useMemo } from 'react';
export const useBatchGroupId = (nodeId: string) => {
const node = useNode(nodeId);
const selector = useMemo(() => {
return createSelector(selectNodesSlice, (nodes) => {
const node = selectNode(nodes, nodeId);
if (!isInvocationNode(node)) {
return;
}
if (!isBatchNode(node)) {
return;
}
return node.data.inputs['batch_group_id']?.value as string;
});
}, [nodeId]);
const batchGroupId = useMemo(() => {
if (!isInvocationNode(node)) {
return;
}
if (!isBatchNode(node)) {
return;
}
return node.data.inputs['batch_group_id']?.value as string;
}, [node]);
const batchGroupId = useAppSelector(selector);
return batchGroupId;
};

View File

@@ -1,59 +0,0 @@
import { useStore } from '@nanostores/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { $edgePendingUpdate, $pendingConnection, $templates } from 'features/nodes/store/nodesSlice';
import { selectNodesSlice } from 'features/nodes/store/selectors';
import { makeConnectionErrorSelector } from 'features/nodes/store/util/makeConnectionErrorSelector';
import { useMemo } from 'react';
export const useConnectionState = (nodeId: string, fieldName: string, kind: 'inputs' | 'outputs') => {
const pendingConnection = useStore($pendingConnection);
const templates = useStore($templates);
const edgePendingUpdate = useStore($edgePendingUpdate);
const selectIsConnected = useMemo(
() =>
createSelector(selectNodesSlice, (nodes) => {
const firstConnectedEdge = nodes.edges.find((edge) => {
return (
(kind === 'inputs' ? edge.target : edge.source) === nodeId &&
(kind === 'inputs' ? edge.targetHandle : edge.sourceHandle) === fieldName
);
});
return firstConnectedEdge !== undefined;
}),
[fieldName, kind, nodeId]
);
const selectValidationResult = useMemo(
() => makeConnectionErrorSelector(templates, nodeId, fieldName, kind === 'inputs' ? 'target' : 'source'),
[templates, nodeId, fieldName, kind]
);
const isConnected = useAppSelector(selectIsConnected);
const isConnectionInProgress = useMemo(() => Boolean(pendingConnection), [pendingConnection]);
const isConnectionStartField = useMemo(() => {
if (!pendingConnection) {
return false;
}
return (
pendingConnection.nodeId === nodeId &&
pendingConnection.handleId === fieldName &&
pendingConnection.fieldTemplate.fieldKind === { inputs: 'input', outputs: 'output' }[kind]
);
}, [fieldName, kind, nodeId, pendingConnection]);
const validationResult = useAppSelector((s) => selectValidationResult(s, pendingConnection, edgePendingUpdate));
const shouldDim = useMemo(
() => Boolean(isConnectionInProgress && !validationResult.isValid && !isConnectionStartField),
[validationResult, isConnectionInProgress, isConnectionStartField]
);
return {
isConnected,
isConnectionInProgress,
isConnectionStartField,
validationResult,
shouldDim,
};
};

View File

@@ -1,36 +0,0 @@
import { useStore } from '@nanostores/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { $templates } from 'features/nodes/store/nodesSlice';
import { selectInvocationNodeType, selectNodesSlice } from 'features/nodes/store/selectors';
import type { FieldInputTemplate, FieldOutputTemplate } from 'features/nodes/types/field';
import { useMemo } from 'react';
import { assert } from 'tsafe';
export const useFieldTemplate = (
nodeId: string,
fieldName: string,
kind: 'inputs' | 'outputs'
): FieldInputTemplate | FieldOutputTemplate => {
const templates = useStore($templates);
const selectNodeType = useMemo(
() => createSelector(selectNodesSlice, (nodes) => selectInvocationNodeType(nodes, nodeId)),
[nodeId]
);
const nodeType = useAppSelector(selectNodeType);
const fieldTemplate = useMemo(() => {
const template = templates[nodeType];
assert(template, `Template for node type ${nodeType} not found`);
if (kind === 'inputs') {
const fieldTemplate = template.inputs[fieldName];
assert(fieldTemplate, `Field template for field ${fieldName} not found`);
return fieldTemplate;
} else {
const fieldTemplate = template.outputs[fieldName];
assert(fieldTemplate, `Field template for field ${fieldName} not found`);
return fieldTemplate;
}
}, [fieldName, kind, nodeType, templates]);
return fieldTemplate;
};

View File

@@ -1,8 +0,0 @@
import { useFieldTemplate } from 'features/nodes/hooks/useFieldTemplate';
import { useMemo } from 'react';
export const useFieldTemplateTitle = (nodeId: string, fieldName: string, kind: 'inputs' | 'outputs'): string => {
const fieldTemplate = useFieldTemplate(nodeId, fieldName, kind);
const fieldTemplateTitle = useMemo(() => fieldTemplate.title, [fieldTemplate]);
return fieldTemplateTitle;
};

View File

@@ -0,0 +1,35 @@
import { useStore } from '@nanostores/react';
import { useAppSelector } from 'app/store/storeHooks';
import { $edgePendingUpdate, $pendingConnection, $templates } from 'features/nodes/store/nodesSlice';
import { makeConnectionErrorSelector } from 'features/nodes/store/util/makeConnectionErrorSelector';
import { useMemo } from 'react';
export const useInputFieldConnectionState = (nodeId: string, fieldName: string) => {
const pendingConnection = useStore($pendingConnection);
const templates = useStore($templates);
const edgePendingUpdate = useStore($edgePendingUpdate);
const selectValidationResult = useMemo(
() => makeConnectionErrorSelector(templates, nodeId, fieldName, 'target'),
[templates, nodeId, fieldName]
);
const isConnectionInProgress = useMemo(() => Boolean(pendingConnection), [pendingConnection]);
const isConnectionStartField = useMemo(() => {
if (!pendingConnection) {
return false;
}
return (
pendingConnection.nodeId === nodeId &&
pendingConnection.handleId === fieldName &&
pendingConnection.fieldTemplate.fieldKind === 'input'
);
}, [fieldName, nodeId, pendingConnection]);
const validationResult = useAppSelector((s) => selectValidationResult(s, pendingConnection, edgePendingUpdate));
return {
isConnectionInProgress,
isConnectionStartField,
validationResult,
};
};

View File

@@ -1,15 +1,15 @@
import { useAppDispatch } from 'app/store/storeHooks';
import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate';
import { useFieldValue } from 'features/nodes/hooks/useFieldValue';
import { useInputFieldValue } from 'features/nodes/hooks/useInputFieldValue';
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
import { fieldValueReset } from 'features/nodes/store/nodesSlice';
import { isEqual } from 'lodash-es';
import { useCallback, useMemo } from 'react';
export const useFieldDefaultValue = (nodeId: string, fieldName: string) => {
export const useInputFieldDefaultValue = (nodeId: string, fieldName: string) => {
const dispatch = useAppDispatch();
const value = useFieldValue(nodeId, fieldName);
const fieldTemplate = useFieldInputTemplate(nodeId, fieldName);
const value = useInputFieldValue(nodeId, fieldName);
const fieldTemplate = useInputFieldTemplate(nodeId, fieldName);
const isValueChanged = useMemo(() => {
return !isEqual(value, fieldTemplate.default);

View File

@@ -1,12 +1,12 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useFieldValue } from 'features/nodes/hooks/useFieldValue';
import { useInputFieldValue } from 'features/nodes/hooks/useInputFieldValue';
import { fieldValueReset } from 'features/nodes/store/nodesSlice';
import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
import { isEqual } from 'lodash-es';
import { useCallback, useMemo } from 'react';
export const useFieldInitialLinearViewValue = (nodeId: string, fieldName: string) => {
export const useInputFieldInitialLinearViewValue = (nodeId: string, fieldName: string) => {
const dispatch = useAppDispatch();
const selectInitialLinearViewValue = useMemo(
() =>
@@ -18,7 +18,7 @@ export const useFieldInitialLinearViewValue = (nodeId: string, fieldName: string
[nodeId, fieldName]
);
const initialLinearViewValue = useAppSelector(selectInitialLinearViewValue);
const value = useFieldValue(nodeId, fieldName);
const value = useInputFieldValue(nodeId, fieldName);
const isValueChanged = useMemo(() => !isEqual(value, initialLinearViewValue), [value, initialLinearViewValue]);
const resetToInitialLinearViewValue = useCallback(() => {
dispatch(fieldValueReset({ nodeId, fieldName, value: initialLinearViewValue }));

View File

@@ -3,17 +3,20 @@ import { useAppSelector } from 'app/store/storeHooks';
import { selectFieldInputInstance, selectNodesSlice } from 'features/nodes/store/selectors';
import type { FieldInputInstance } from 'features/nodes/types/field';
import { useMemo } from 'react';
import { assert } from 'tsafe';
export const useFieldInputInstance = (nodeId: string, fieldName: string): FieldInputInstance | null => {
export const useInputFieldInstance = (nodeId: string, fieldName: string): FieldInputInstance => {
const selector = useMemo(
() =>
createMemoizedSelector(selectNodesSlice, (nodes) => {
return selectFieldInputInstance(nodes, nodeId, fieldName);
const instance = selectFieldInputInstance(nodes, nodeId, fieldName);
assert(instance, `Instance for input field ${fieldName} not found`);
return instance;
}),
[fieldName, nodeId]
);
const fieldData = useAppSelector(selector);
const instance = useAppSelector(selector);
return fieldData;
return instance;
};

View File

@@ -3,7 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { selectInvocationNode, selectNodesSlice } from 'features/nodes/store/selectors';
import { useMemo } from 'react';
export const useFieldInputInstanceExists = (nodeId: string, fieldName: string) => {
export const useInputFieldInstanceExists = (nodeId: string, fieldName: string) => {
const selector = useMemo(
() =>
createSelector(selectNodesSlice, (nodesSlice) => {

View File

@@ -0,0 +1,21 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNodesSlice } from 'features/nodes/store/selectors';
import { useMemo } from 'react';
export const useInputFieldIsConnected = (nodeId: string, fieldName: string) => {
const selector = useMemo(
() =>
createSelector(selectNodesSlice, (nodes) => {
const firstConnectedEdge = nodes.edges.find((edge) => {
return edge.target === nodeId && edge.targetHandle === fieldName;
});
return firstConnectedEdge !== undefined;
}),
[fieldName, nodeId]
);
const isConnected = useAppSelector(selector);
return isConnected;
};

View File

@@ -3,7 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
import { useMemo } from 'react';
export const useFieldIsExposed = (nodeId: string, fieldName: string) => {
export const useInputFieldIsExposed = (nodeId: string, fieldName: string) => {
const selectIsExposed = useMemo(
() =>
createSelector(selectWorkflowSlice, (workflow) => {

View File

@@ -1,7 +1,7 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate';
import { useInputFieldIsConnected } from 'features/nodes/hooks/useInputFieldIsConnected';
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
import { selectFieldInputInstance, selectNodesSlice } from 'features/nodes/store/selectors';
import {
isFloatFieldCollectionInputInstance,
@@ -20,9 +20,9 @@ import {
} from 'features/nodes/types/fieldValidators';
import { useMemo } from 'react';
export const useFieldIsInvalid = (nodeId: string, fieldName: string) => {
const template = useFieldInputTemplate(nodeId, fieldName);
const connectionState = useConnectionState(nodeId, fieldName, 'inputs');
export const useInputFieldIsInvalid = (nodeId: string, fieldName: string) => {
const template = useInputFieldTemplate(nodeId, fieldName);
const isConnected = useInputFieldIsConnected(nodeId, fieldName);
const selectIsInvalid = useMemo(() => {
return createSelector(selectNodesSlice, (nodes) => {
@@ -35,11 +35,11 @@ export const useFieldIsInvalid = (nodeId: string, fieldName: string) => {
// 'connection' input fields have no data validation - only connection validation
if (template.input === 'connection') {
return template.required && !connectionState.isConnected;
return template.required && !isConnected;
}
// 'any' input fields are valid if they are connected
if (template.input === 'any' && connectionState.isConnected) {
if (template.input === 'any' && isConnected) {
return false;
}
@@ -77,7 +77,7 @@ export const useFieldIsInvalid = (nodeId: string, fieldName: string) => {
// Field looks OK
return false;
});
}, [connectionState.isConnected, fieldName, nodeId, template]);
}, [nodeId, fieldName, template, isConnected]);
const isInvalid = useAppSelector(selectIsInvalid);

View File

@@ -3,7 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { selectFieldInputInstance, selectNodesSlice } from 'features/nodes/store/selectors';
import { useMemo } from 'react';
export const useFieldLabel = (nodeId: string, fieldName: string): string | null => {
export const useInputFieldLabel = (nodeId: string, fieldName: string): string | null => {
const selector = useMemo(
() =>
createSelector(selectNodesSlice, (nodes) => {

View File

@@ -4,7 +4,7 @@ import { selectNodesSlice } from 'features/nodes/store/selectors';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { useMemo } from 'react';
export const useFieldLinearViewConfig = (nodeId: string, fieldName: string) => {
export const useInputFieldLinearViewConfig = (nodeId: string, fieldName: string) => {
const selector = useMemo(
() =>
createSelector(selectNodesSlice, (nodes) => {

View File

@@ -5,7 +5,7 @@ import { $templates } from 'features/nodes/store/nodesSlice';
import { selectInvocationNode, selectNodesSlice } from 'features/nodes/store/selectors';
import { useMemo } from 'react';
export const useFieldInputName = (nodeId: string, fieldName: string) => {
export const useInputFieldName = (nodeId: string, fieldName: string) => {
const templates = useStore($templates);
const selector = useMemo(

View File

@@ -20,7 +20,7 @@ const isAnyOrDirectInputField = (field: FieldInputTemplate) => {
);
};
export const useFieldNames = (nodeId: string) => {
export const useInputFieldNamesByStatus = (nodeId: string) => {
const template = useNodeTemplate(nodeId);
const node = useNodeData(nodeId);
const fieldNames = useMemo(() => {

View File

@@ -4,7 +4,7 @@ import { selectNodesSlice } from 'features/nodes/store/selectors';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { useMemo } from 'react';
export const useFieldNotes = (nodeId: string, fieldName: string) => {
export const useInputFieldNotes = (nodeId: string, fieldName: string) => {
const selector = useMemo(
() =>
createSelector(selectNodesSlice, (nodes) => {

View File

@@ -3,7 +3,7 @@ import type { FieldInputTemplate } from 'features/nodes/types/field';
import { useMemo } from 'react';
import { assert } from 'tsafe';
export const useFieldInputTemplate = (nodeId: string, fieldName: string): FieldInputTemplate => {
export const useInputFieldTemplate = (nodeId: string, fieldName: string): FieldInputTemplate => {
const template = useNodeTemplate(nodeId);
const fieldTemplate = useMemo(() => {
const _fieldTemplate = template.inputs[fieldName];

View File

@@ -5,7 +5,7 @@ import { $templates } from 'features/nodes/store/nodesSlice';
import { selectInvocationNode, selectNodesSlice } from 'features/nodes/store/selectors';
import { useMemo } from 'react';
export const useFieldInputTemplateExists = (nodeId: string, fieldName: string) => {
export const useInputFieldTemplateExists = (nodeId: string, fieldName: string) => {
const templates = useStore($templates);
const selector = useMemo(

View File

@@ -0,0 +1,15 @@
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
import { useMemo } from 'react';
import { assert } from 'tsafe';
export const useInputFieldTemplateTitle = (nodeId: string, fieldName: string): string => {
const template = useNodeTemplate(nodeId);
const title = useMemo(() => {
const fieldTemplate = template.inputs[fieldName];
assert(fieldTemplate, `Template for input field ${fieldName} not found.`);
return fieldTemplate.title;
}, [fieldName, template.inputs]);
return title;
};

View File

@@ -4,7 +4,7 @@ import { selectNodesSlice } from 'features/nodes/store/selectors';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { useMemo } from 'react';
export const useFieldValue = (nodeId: string, fieldName: string) => {
export const useInputFieldValue = (nodeId: string, fieldName: string) => {
const selector = useMemo(
() =>
createMemoizedSelector(selectNodesSlice, (nodes) => {

View File

@@ -166,6 +166,6 @@ const pasteSelectionWithEdges = () => {
const api = { copySelection, pasteSelection, pasteSelectionWithEdges };
export const useCopyPaste = () => {
export const useNodeCopyPaste = () => {
return api;
};

View File

@@ -19,7 +19,7 @@ const initialNodeExecutionState: Omit<NodeExecutionState, 'nodeId'> = {
outputs: [],
};
export const useExecutionState = (nodeId?: string) => {
export const useNodeExecutionState = (nodeId?: string) => {
const executionStates = useStore($nodeExecutionStates, nodeId ? { keys: [nodeId] } : undefined);
const executionState = useMemo(() => (nodeId ? executionStates[nodeId] : undefined), [executionStates, nodeId]);
return executionState;

View File

@@ -2,7 +2,7 @@ import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
import { some } from 'lodash-es';
import { useMemo } from 'react';
export const useHasImageOutput = (nodeId: string): boolean => {
export const useNodeHasImageOutput = (nodeId: string): boolean => {
const template = useNodeTemplate(nodeId);
const hasImageOutput = useMemo(
() =>

View File

@@ -0,0 +1,20 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNode, selectNodesSlice } from 'features/nodes/store/selectors';
import { isInvocationNode as _isInvocationNode } from 'features/nodes/types/invocation';
import { useMemo } from 'react';
export const useNodeIsInvocationNode = (nodeId: string): boolean => {
const selector = useMemo(
() =>
createSelector(selectNodesSlice, (nodes) => {
const node = selectNode(nodes, nodeId);
return _isInvocationNode(node);
}),
[nodeId]
);
const isInvocationNode = useAppSelector(selector);
return isInvocationNode;
};

View File

@@ -0,0 +1,22 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNode, selectNodesSlice } from 'features/nodes/store/selectors';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { useMemo } from 'react';
import { assert } from 'tsafe';
export const useInvocationNodeNotes = (nodeId: string): string => {
const selector = useMemo(
() =>
createSelector(selectNodesSlice, (nodes) => {
const node = selectNode(nodes, nodeId);
assert(isInvocationNode(node), `Node with id ${nodeId} is not an invocation node`);
return node.data.notes;
}),
[nodeId]
);
const notes = useAppSelector(selector);
return notes;
};

View File

@@ -0,0 +1,35 @@
import { useStore } from '@nanostores/react';
import { useAppSelector } from 'app/store/storeHooks';
import { $edgePendingUpdate, $pendingConnection, $templates } from 'features/nodes/store/nodesSlice';
import { makeConnectionErrorSelector } from 'features/nodes/store/util/makeConnectionErrorSelector';
import { useMemo } from 'react';
export const useOutputFieldConnectionState = (nodeId: string, fieldName: string) => {
const pendingConnection = useStore($pendingConnection);
const templates = useStore($templates);
const edgePendingUpdate = useStore($edgePendingUpdate);
const selectValidationResult = useMemo(
() => makeConnectionErrorSelector(templates, nodeId, fieldName, 'source'),
[templates, nodeId, fieldName]
);
const isConnectionInProgress = useMemo(() => Boolean(pendingConnection), [pendingConnection]);
const isConnectionStartField = useMemo(() => {
if (!pendingConnection) {
return false;
}
return (
pendingConnection.nodeId === nodeId &&
pendingConnection.handleId === fieldName &&
pendingConnection.fieldTemplate.fieldKind === 'output'
);
}, [fieldName, nodeId, pendingConnection]);
const validationResult = useAppSelector((s) => selectValidationResult(s, pendingConnection, edgePendingUpdate));
return {
isConnectionInProgress,
isConnectionStartField,
validationResult,
};
};

View File

@@ -0,0 +1,21 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNodesSlice } from 'features/nodes/store/selectors';
import { useMemo } from 'react';
export const useOutputFieldIsConnected = (nodeId: string, fieldName: string) => {
const selector = useMemo(
() =>
createSelector(selectNodesSlice, (nodes) => {
const firstConnectedEdge = nodes.edges.find((edge) => {
return edge.source === nodeId && edge.sourceHandle === fieldName;
});
return firstConnectedEdge !== undefined;
}),
[fieldName, nodeId]
);
const isConnected = useAppSelector(selector);
return isConnected;
};

View File

@@ -5,7 +5,7 @@ import { $templates } from 'features/nodes/store/nodesSlice';
import { selectInvocationNode, selectNodesSlice } from 'features/nodes/store/selectors';
import { useMemo } from 'react';
export const useFieldOutputName = (nodeId: string, fieldName: string) => {
export const useOutputFieldName = (nodeId: string, fieldName: string) => {
const templates = useStore($templates);
const selector = useMemo(

View File

@@ -3,7 +3,7 @@ import type { FieldOutputTemplate } from 'features/nodes/types/field';
import { useMemo } from 'react';
import { assert } from 'tsafe';
export const useFieldOutputTemplate = (nodeId: string, fieldName: string): FieldOutputTemplate => {
export const useOutputFieldTemplate = (nodeId: string, fieldName: string): FieldOutputTemplate => {
const template = useNodeTemplate(nodeId);
const fieldTemplate = useMemo(() => {
const _fieldTemplate = template.outputs[fieldName];

View File

@@ -5,7 +5,7 @@ import { $templates } from 'features/nodes/store/nodesSlice';
import { selectInvocationNode, selectNodesSlice } from 'features/nodes/store/selectors';
import { useMemo } from 'react';
export const useFieldOutputTemplateExists = (nodeId: string, fieldName: string) => {
export const useOutputFieldTemplateExists = (nodeId: string, fieldName: string) => {
const templates = useStore($templates);
const selector = useMemo(

View File

@@ -2,13 +2,10 @@ import { type FieldType, isCollection, isSingleOrCollection } from 'features/nod
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
export const useFieldTypeName = (fieldType?: FieldType): string => {
export const useFieldTypeName = (fieldType: FieldType): string => {
const { t } = useTranslation();
const name = useMemo(() => {
if (!fieldType) {
return '';
}
const { name } = fieldType;
if (isCollection(fieldType)) {
return t('nodes.collectionFieldType', { name });

View File

@@ -1,10 +1,10 @@
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { useMemo } from 'react';
import { useHasImageOutput } from './useHasImageOutput';
import { useNodeHasImageOutput } from './useNodeHasImageOutput';
export const useWithFooter = (nodeId: string) => {
const hasImageOutput = useHasImageOutput(nodeId);
const hasImageOutput = useNodeHasImageOutput(nodeId);
const isCacheEnabled = useFeatureStatus('invocationCache');
const withFooter = useMemo(() => hasImageOutput || isCacheEnabled, [hasImageOutput, isCacheEnabled]);
return withFooter;

View File

@@ -3,7 +3,7 @@ import type { AppDispatch, RootState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
import { stagingAreaImageStaged } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { boardIdSelected, galleryViewChanged, imageSelected, offsetChanged } from 'features/gallery/store/gallerySlice';
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState';
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useNodeExecutionState';
import { zNodeStatus } from 'features/nodes/types/invocation';
import { CANVAS_OUTPUT_PREFIX } from 'features/nodes/util/graph/graphBuilderUtils';
import { boardsApi } from 'services/api/endpoints/boards';

View File

@@ -6,7 +6,7 @@ import { $bulkDownloadId } from 'app/store/nanostores/bulkDownloadId';
import { $queueId } from 'app/store/nanostores/queueId';
import type { AppStore } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState';
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useNodeExecutionState';
import { zNodeStatus } from 'features/nodes/types/invocation';
import ErrorToastDescription, { getTitle } from 'features/toast/ErrorToastDescription';
import { toast } from 'features/toast/toast';