mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
refactor(ui): continued reorg of components & hooks
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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} />;
|
||||
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
@@ -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';
|
||||
@@ -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 })}
|
||||
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
|
||||
@@ -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} />;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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 })}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>(
|
||||
() =>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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', [
|
||||
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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 }));
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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) => {
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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) => {
|
||||
@@ -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);
|
||||
|
||||
@@ -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) => {
|
||||
@@ -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) => {
|
||||
@@ -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(
|
||||
@@ -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(() => {
|
||||
@@ -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) => {
|
||||
@@ -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];
|
||||
@@ -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(
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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) => {
|
||||
@@ -166,6 +166,6 @@ const pasteSelectionWithEdges = () => {
|
||||
|
||||
const api = { copySelection, pasteSelection, pasteSelectionWithEdges };
|
||||
|
||||
export const useCopyPaste = () => {
|
||||
export const useNodeCopyPaste = () => {
|
||||
return api;
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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(
|
||||
() =>
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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(
|
||||
@@ -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];
|
||||
@@ -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(
|
||||
@@ -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 });
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user