mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
refactor(ui): workflows component structure (WIP)
- Simplify and de-insane-ify component structure, hooks, selectors, etc. - Some perf improvements by using data attributes for styling instead of dynamic CSS-in-JS. - Add field notes and start of linear view config, got blocked when I ran into deeper layout issues that made it very difficult to handle field configs. So those are WIP in this commit.
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
import { Flex, Grid, GridItem } from '@invoke-ai/ui-library';
|
||||
import NodeWrapper from 'features/nodes/components/flow/nodes/common/NodeWrapper';
|
||||
import { InvocationInputFieldCheck } from 'features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck';
|
||||
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 { useOutputFieldNames } from 'features/nodes/hooks/useOutputFieldNames';
|
||||
import { useWithFooter } from 'features/nodes/hooks/useWithFooter';
|
||||
import { memo } from 'react';
|
||||
|
||||
import InputField from './fields/InputField';
|
||||
import OutputField from './fields/OutputField';
|
||||
import { InputFieldViewNodes } from './fields/InputFieldViewNodes';
|
||||
import InvocationNodeFooter from './InvocationNodeFooter';
|
||||
import InvocationNodeHeader from './InvocationNodeHeader';
|
||||
|
||||
@@ -42,34 +43,28 @@ const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
|
||||
<Grid gridTemplateColumns="1fr auto" gridAutoRows="1fr">
|
||||
{fieldNames.connectionFields.map((fieldName, i) => (
|
||||
<GridItem gridColumnStart={1} gridRowStart={i + 1} key={`${nodeId}.${fieldName}.input-field`}>
|
||||
<InvocationInputFieldCheck nodeId={nodeId} fieldName={fieldName}>
|
||||
<InputField nodeId={nodeId} fieldName={fieldName} />
|
||||
</InvocationInputFieldCheck>
|
||||
<InputFieldGate nodeId={nodeId} fieldName={fieldName}>
|
||||
<InputFieldViewNodes nodeId={nodeId} fieldName={fieldName} />
|
||||
</InputFieldGate>
|
||||
</GridItem>
|
||||
))}
|
||||
{outputFieldNames.map((fieldName, i) => (
|
||||
<GridItem gridColumnStart={2} gridRowStart={i + 1} key={`${nodeId}.${fieldName}.output-field`}>
|
||||
<OutputField nodeId={nodeId} fieldName={fieldName} />
|
||||
<OutputFieldGate nodeId={nodeId} fieldName={fieldName}>
|
||||
<OutputFieldNodesEditorView nodeId={nodeId} fieldName={fieldName} />
|
||||
</OutputFieldGate>
|
||||
</GridItem>
|
||||
))}
|
||||
</Grid>
|
||||
{fieldNames.anyOrDirectFields.map((fieldName) => (
|
||||
<InvocationInputFieldCheck
|
||||
key={`${nodeId}.${fieldName}.input-field`}
|
||||
nodeId={nodeId}
|
||||
fieldName={fieldName}
|
||||
>
|
||||
<InputField nodeId={nodeId} fieldName={fieldName} />
|
||||
</InvocationInputFieldCheck>
|
||||
<InputFieldGate key={`${nodeId}.${fieldName}.input-field`} nodeId={nodeId} fieldName={fieldName}>
|
||||
<InputFieldViewNodes nodeId={nodeId} fieldName={fieldName} />
|
||||
</InputFieldGate>
|
||||
))}
|
||||
{fieldNames.missingFields.map((fieldName) => (
|
||||
<InvocationInputFieldCheck
|
||||
key={`${nodeId}.${fieldName}.input-field`}
|
||||
nodeId={nodeId}
|
||||
fieldName={fieldName}
|
||||
>
|
||||
<InputField nodeId={nodeId} fieldName={fieldName} />
|
||||
</InvocationInputFieldCheck>
|
||||
<InputFieldGate key={`${nodeId}.${fieldName}.input-field`} nodeId={nodeId} fieldName={fieldName}>
|
||||
<InputFieldViewNodes nodeId={nodeId} fieldName={fieldName} />
|
||||
</InputFieldGate>
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
@@ -1,72 +1,79 @@
|
||||
import { Tooltip } from '@invoke-ai/ui-library';
|
||||
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Box, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { getFieldColor } from 'features/nodes/components/flow/edges/util/getEdgeColor';
|
||||
import { useFieldTypeName } from 'features/nodes/hooks/usePrettyFieldType';
|
||||
import type { ValidationResult } from 'features/nodes/store/util/validateConnection';
|
||||
import { HANDLE_TOOLTIP_OPEN_DELAY, MODEL_TYPES } from 'features/nodes/types/constants';
|
||||
import { type FieldInputTemplate, type FieldOutputTemplate, isSingle } from 'features/nodes/types/field';
|
||||
import type { FieldInputTemplate, FieldOutputTemplate } from 'features/nodes/types/field';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { HandleType } from 'reactflow';
|
||||
import { Handle, Position } from 'reactflow';
|
||||
|
||||
type FieldHandleProps = {
|
||||
fieldTemplate: FieldInputTemplate | FieldOutputTemplate;
|
||||
type Props = {
|
||||
handleType: HandleType;
|
||||
fieldTemplate: FieldInputTemplate | FieldOutputTemplate;
|
||||
isConnectionInProgress: boolean;
|
||||
isConnectionStartField: boolean;
|
||||
validationResult: ValidationResult;
|
||||
};
|
||||
|
||||
const FieldHandle = (props: FieldHandleProps) => {
|
||||
const { fieldTemplate, handleType, isConnectionInProgress, isConnectionStartField, validationResult } = props;
|
||||
const sx = {
|
||||
position: 'relative',
|
||||
width: 'full',
|
||||
height: 'full',
|
||||
borderStyle: 'solid',
|
||||
borderWidth: 4,
|
||||
pointerEvents: 'none',
|
||||
'&[data-cardinality="SINGLE"]': {
|
||||
borderWidth: 0,
|
||||
},
|
||||
borderRadius: '100%',
|
||||
'&[data-is-model-field="true"], &[data-is-batch-field="true"]': {
|
||||
borderRadius: 4,
|
||||
},
|
||||
'&[data-is-batch-field="true"]': {
|
||||
transform: 'rotate(45deg)',
|
||||
},
|
||||
'&[data-is-connection-in-progress="true"][data-is-connection-start-field="false"][data-is-connection-valid="false"]':
|
||||
{
|
||||
filter: 'opacity(0.4) grayscale(0.7)',
|
||||
cursor: 'not-allowed',
|
||||
},
|
||||
'&[data-is-connection-in-progress="true"][data-is-connection-start-field="true"][data-is-connection-valid="false"]': {
|
||||
cursor: 'grab',
|
||||
},
|
||||
'&[data-is-connection-in-progress="false"] &[data-is-connection-valid="true"]': {
|
||||
cursor: 'crosshair',
|
||||
},
|
||||
} satisfies SystemStyleObject;
|
||||
|
||||
const handleStyleBase = {
|
||||
position: 'absolute',
|
||||
width: '1rem',
|
||||
height: '1rem',
|
||||
zIndex: 1,
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
} satisfies CSSProperties;
|
||||
|
||||
const targetHandleStyle = {
|
||||
...handleStyleBase,
|
||||
insetInlineStart: '-1rem',
|
||||
} satisfies CSSProperties;
|
||||
|
||||
const sourceHandleStyle = {
|
||||
...handleStyleBase,
|
||||
insetInlineEnd: '-1rem',
|
||||
} satisfies CSSProperties;
|
||||
|
||||
export const FieldHandle = memo((props: Props) => {
|
||||
const { fieldTemplate, isConnectionInProgress, isConnectionStartField, validationResult, handleType } = props;
|
||||
const { t } = useTranslation();
|
||||
const { name } = fieldTemplate;
|
||||
const type = fieldTemplate.type;
|
||||
const fieldTypeName = useFieldTypeName(type);
|
||||
const styles: CSSProperties = useMemo(() => {
|
||||
const isModelType = MODEL_TYPES.some((t) => t === type.name);
|
||||
const color = getFieldColor(type);
|
||||
const s: CSSProperties = {
|
||||
backgroundColor: !isSingle(type) ? colorTokenToCssVar('base.900') : color,
|
||||
position: 'absolute',
|
||||
width: '1rem',
|
||||
height: '1rem',
|
||||
borderWidth: !isSingle(type) ? 4 : 0,
|
||||
borderStyle: 'solid',
|
||||
borderColor: color,
|
||||
borderRadius: isModelType || type.batch ? 4 : '100%',
|
||||
zIndex: 1,
|
||||
transformOrigin: 'center',
|
||||
};
|
||||
|
||||
if (type.batch) {
|
||||
s.transform = 'rotate(45deg) translateX(-0.3rem) translateY(-0.3rem)';
|
||||
}
|
||||
|
||||
if (handleType === 'target') {
|
||||
s.insetInlineStart = '-1rem';
|
||||
} else {
|
||||
s.insetInlineEnd = '-1rem';
|
||||
}
|
||||
|
||||
if (isConnectionInProgress && !isConnectionStartField && !validationResult.isValid) {
|
||||
s.filter = 'opacity(0.4) grayscale(0.7)';
|
||||
}
|
||||
|
||||
if (isConnectionInProgress && !validationResult.isValid) {
|
||||
if (isConnectionStartField) {
|
||||
s.cursor = 'grab';
|
||||
} else {
|
||||
s.cursor = 'not-allowed';
|
||||
}
|
||||
} else {
|
||||
s.cursor = 'crosshair';
|
||||
}
|
||||
|
||||
return s;
|
||||
}, [handleType, isConnectionInProgress, isConnectionStartField, type, validationResult.isValid]);
|
||||
const fieldTypeName = useFieldTypeName(fieldTemplate.type);
|
||||
const fieldColor = useMemo(() => getFieldColor(fieldTemplate.type), [fieldTemplate.type]);
|
||||
const isModelField = useMemo(() => MODEL_TYPES.some((t) => t === fieldTemplate.type.name), [fieldTemplate.type]);
|
||||
|
||||
const tooltip = useMemo(() => {
|
||||
if (isConnectionInProgress && validationResult.messageTKey) {
|
||||
@@ -83,12 +90,24 @@ const FieldHandle = (props: FieldHandleProps) => {
|
||||
>
|
||||
<Handle
|
||||
type={handleType}
|
||||
id={name}
|
||||
id={fieldTemplate.name}
|
||||
position={handleType === 'target' ? Position.Left : Position.Right}
|
||||
style={styles}
|
||||
/>
|
||||
style={handleType === 'target' ? targetHandleStyle : sourceHandleStyle}
|
||||
>
|
||||
<Box
|
||||
sx={sx}
|
||||
data-cardinality={fieldTemplate.type.cardinality}
|
||||
data-is-batch-field={fieldTemplate.type.batch}
|
||||
data-is-model-field={isModelField}
|
||||
data-is-connection-in-progress={isConnectionInProgress}
|
||||
data-is-connection-start-field={isConnectionStartField}
|
||||
data-is-connection-valid={validationResult.isValid}
|
||||
backgroundColor={fieldTemplate.type.cardinality === 'SINGLE' ? fieldColor : 'base.900'}
|
||||
borderColor={fieldColor}
|
||||
/>
|
||||
</Handle>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default memo(FieldHandle);
|
||||
FieldHandle.displayName = 'FieldHandle';
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
FormControl,
|
||||
FormLabel,
|
||||
IconButton,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
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 { fieldLinearViewConfigChanged } from 'features/nodes/store/nodesSlice';
|
||||
import type { FieldInputInstance } from 'features/nodes/types/field';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiWrenchFill } from 'react-icons/pi';
|
||||
|
||||
type Props = {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
const parseNotesDisplay = (
|
||||
notesDisplay: string
|
||||
): NonNullable<FieldInputInstance['linearViewConfig']>['notesDisplay'] => {
|
||||
switch (notesDisplay) {
|
||||
case 'none':
|
||||
return 'none';
|
||||
case 'helper-text':
|
||||
return 'helper-text';
|
||||
case 'icon-with-popover':
|
||||
return 'icon-with-popover';
|
||||
default:
|
||||
return 'none';
|
||||
}
|
||||
};
|
||||
|
||||
export const FieldLinearViewConfigIconButton = memo(({ nodeId, fieldName }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const isExposed = useFieldIsExposed(nodeId, fieldName);
|
||||
const linearViewConfig = useFieldLinearViewConfig(nodeId, fieldName);
|
||||
const onChangeNotesDisplay = useCallback(
|
||||
(e: ChangeEvent<HTMLSelectElement>) => {
|
||||
const notesDisplay = parseNotesDisplay(e.target.value);
|
||||
dispatch(
|
||||
fieldLinearViewConfigChanged({ nodeId, fieldName, linearViewConfig: { ...linearViewConfig, notesDisplay } })
|
||||
);
|
||||
},
|
||||
[dispatch, fieldName, linearViewConfig, nodeId]
|
||||
);
|
||||
|
||||
if (!isExposed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
tooltip="Linear View Config"
|
||||
aria-label="Linear View Config"
|
||||
icon={<PiWrenchFill />}
|
||||
pointerEvents="auto"
|
||||
size="xs"
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent p={2} w={256}>
|
||||
<FormControl orientation="vertical">
|
||||
<FormLabel>{t('nodes.notesDisplay')}</FormLabel>
|
||||
<Select value={linearViewConfig?.notesDisplay ?? 'none'} onChange={onChangeNotesDisplay}>
|
||||
<option value="none">{t('common.none')}</option>
|
||||
<option value="helper-text">{t('nodes.helperText')}</option>
|
||||
<option value="icon-with-popover">{t('nodes.iconWithPopover')}</option>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
});
|
||||
|
||||
FieldLinearViewConfigIconButton.displayName = 'FieldLinearViewConfigIconButton';
|
||||
@@ -1,13 +1,12 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useFieldIsExposed } from 'features/nodes/hooks/useFieldIsExposed';
|
||||
import { useFieldValue } from 'features/nodes/hooks/useFieldValue';
|
||||
import {
|
||||
selectWorkflowSlice,
|
||||
workflowExposedFieldAdded,
|
||||
workflowExposedFieldRemoved,
|
||||
} from 'features/nodes/store/workflowSlice';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiMinusBold, PiPlusBold } from 'react-icons/pi';
|
||||
|
||||
@@ -20,15 +19,7 @@ const FieldLinearViewToggle = ({ nodeId, fieldName }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const value = useFieldValue(nodeId, fieldName);
|
||||
const selectIsExposed = useMemo(
|
||||
() =>
|
||||
createSelector(selectWorkflowSlice, (workflow) => {
|
||||
return Boolean(workflow.exposedFields.find((f) => f.nodeId === nodeId && f.fieldName === fieldName));
|
||||
}),
|
||||
[fieldName, nodeId]
|
||||
);
|
||||
|
||||
const isExposed = useAppSelector(selectIsExposed);
|
||||
const isExposed = useFieldIsExposed(nodeId, fieldName);
|
||||
|
||||
const handleExposeField = useCallback(() => {
|
||||
dispatch(workflowExposedFieldAdded({ nodeId, fieldName, value }));
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate';
|
||||
import { useFieldValue } from 'features/nodes/hooks/useFieldValue';
|
||||
import { fieldValueReset } from 'features/nodes/store/nodesSlice';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
|
||||
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;
|
||||
@@ -14,29 +8,9 @@ type Props = {
|
||||
};
|
||||
|
||||
const FieldResetToDefaultValueButton = ({ nodeId, fieldName }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const value = useFieldValue(nodeId, fieldName);
|
||||
const fieldTemplate = useFieldInputTemplate(nodeId, fieldName);
|
||||
const isDisabled = useMemo(() => {
|
||||
return isEqual(value, fieldTemplate.default);
|
||||
}, [value, fieldTemplate.default]);
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(fieldValueReset({ nodeId, fieldName, value: fieldTemplate.default }));
|
||||
}, [dispatch, fieldName, fieldTemplate.default, nodeId]);
|
||||
const { isValueChanged, resetToDefaultValue } = useFieldDefaultValue(nodeId, fieldName);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
tooltip={t('nodes.resetToDefaultValue')}
|
||||
aria-label={t('nodes.resetToDefaultValue')}
|
||||
icon={<PiArrowCounterClockwiseBold />}
|
||||
onClick={onClick}
|
||||
isDisabled={isDisabled}
|
||||
pointerEvents="auto"
|
||||
size="xs"
|
||||
/>
|
||||
);
|
||||
return <FieldResetValueButton onClick={resetToDefaultValue} isDisabled={!isValueChanged} />;
|
||||
};
|
||||
|
||||
export default memo(FieldResetToDefaultValueButton);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
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);
|
||||
@@ -0,0 +1,25 @@
|
||||
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';
|
||||
@@ -0,0 +1,23 @@
|
||||
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 type { PropsWithChildren } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
type Props = PropsWithChildren<{
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
}>;
|
||||
|
||||
export const InputFieldGate = memo(({ nodeId, fieldName, children }: Props) => {
|
||||
const hasInstance = useFieldInputInstanceExists(nodeId, fieldName);
|
||||
const hasTemplate = useFieldInputTemplateExists(nodeId, fieldName);
|
||||
|
||||
if (!hasTemplate || !hasInstance) {
|
||||
return <InputFieldUnknownPlaceholder nodeId={nodeId} fieldName={fieldName} />;
|
||||
}
|
||||
|
||||
return children;
|
||||
});
|
||||
|
||||
InputFieldGate.displayName = 'InputFieldGate';
|
||||
@@ -0,0 +1,27 @@
|
||||
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 { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type Props = {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export const InputFieldUnknownPlaceholder = memo(({ nodeId, fieldName }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const name = useFieldInputName(nodeId, fieldName);
|
||||
|
||||
return (
|
||||
<InputFieldWrapper shouldDim={false}>
|
||||
<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 })}
|
||||
</FormLabel>
|
||||
</FormControl>
|
||||
</InputFieldWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
InputFieldUnknownPlaceholder.displayName = 'InputFieldUnknownPlaceholder';
|
||||
@@ -3,23 +3,22 @@ import { Box, Circle, Flex, Icon, IconButton, Spacer, Tooltip } from '@invoke-ai
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { DndListDropIndicator } from 'features/dnd/DndListDropIndicator';
|
||||
import { FieldNotesIconButton } from 'features/nodes/components/flow/nodes/Invocation/fields/FieldNotesIconButton';
|
||||
import { InvocationInputFieldCheck } from 'features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck';
|
||||
import FieldResetToInitialLinearViewValueButton from 'features/nodes/components/flow/nodes/Invocation/fields/FieldResetToInitialLinearViewValueButton';
|
||||
import { useLinearViewFieldDnd } from 'features/nodes/components/sidePanel/workflow/useLinearViewFieldDnd';
|
||||
import { useFieldOriginalValue } from 'features/nodes/hooks/useFieldOriginalValue';
|
||||
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 type { FieldIdentifier } from 'features/nodes/types/field';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowCounterClockwiseBold, PiInfoBold, PiTrashSimpleBold } from 'react-icons/pi';
|
||||
import { PiInfoBold, PiTrashSimpleBold } from 'react-icons/pi';
|
||||
|
||||
import EditableFieldTitle from './EditableFieldTitle';
|
||||
import FieldTooltipContent from './FieldTooltipContent';
|
||||
import InputFieldRenderer from './InputFieldRenderer';
|
||||
|
||||
type Props = {
|
||||
fieldIdentifier: FieldIdentifier;
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
const sx = {
|
||||
@@ -35,25 +34,24 @@ const sx = {
|
||||
transitionProperty: 'common',
|
||||
} satisfies SystemStyleObject;
|
||||
|
||||
const LinearViewFieldInternal = ({ fieldIdentifier }: Props) => {
|
||||
export const InputFieldViewLinear = memo(({ nodeId, fieldName }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { isValueChanged, onReset } = useFieldOriginalValue(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
|
||||
const { isMouseOverNode, handleMouseOut, handleMouseOver } = useMouseOverNode(fieldIdentifier.nodeId);
|
||||
const { isMouseOverNode, handleMouseOut, handleMouseOver } = useMouseOverNode(nodeId);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleRemoveField = useCallback(() => {
|
||||
dispatch(workflowExposedFieldRemoved(fieldIdentifier));
|
||||
}, [dispatch, fieldIdentifier]);
|
||||
dispatch(workflowExposedFieldRemoved({ nodeId, fieldName }));
|
||||
}, [dispatch, fieldName, nodeId]);
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [dndListState, isDragging] = useLinearViewFieldDnd(ref, fieldIdentifier);
|
||||
const [dndListState, isDragging] = useLinearViewFieldDnd(ref, { nodeId, fieldName });
|
||||
|
||||
return (
|
||||
<Box position="relative" w="full">
|
||||
<Flex
|
||||
ref={ref}
|
||||
// This is used to trigger the post-move flash animation
|
||||
data-field-name={`${fieldIdentifier.nodeId}-${fieldIdentifier.fieldName}`}
|
||||
data-field-name={`${nodeId}-${fieldName}`}
|
||||
data-is-dragging={isDragging}
|
||||
onMouseEnter={handleMouseOver}
|
||||
onMouseLeave={handleMouseOut}
|
||||
@@ -61,28 +59,13 @@ const LinearViewFieldInternal = ({ fieldIdentifier }: Props) => {
|
||||
>
|
||||
<Flex flexDir="column" w="full">
|
||||
<Flex alignItems="center" gap={2}>
|
||||
<EditableFieldTitle nodeId={fieldIdentifier.nodeId} fieldName={fieldIdentifier.fieldName} kind="inputs" />
|
||||
<EditableFieldTitle nodeId={nodeId} fieldName={fieldName} kind="inputs" />
|
||||
<Spacer />
|
||||
{isMouseOverNode && <Circle me={2} size={2} borderRadius="full" bg="invokeBlue.500" />}
|
||||
<FieldNotesIconButton nodeId={fieldIdentifier.nodeId} fieldName={fieldIdentifier.fieldName} />
|
||||
{isValueChanged && (
|
||||
<IconButton
|
||||
aria-label={t('nodes.resetToDefaultValue')}
|
||||
tooltip={t('nodes.resetToDefaultValue')}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onReset}
|
||||
icon={<PiArrowCounterClockwiseBold />}
|
||||
/>
|
||||
)}
|
||||
<FieldNotesIconButton nodeId={nodeId} fieldName={fieldName} />
|
||||
<FieldResetToInitialLinearViewValueButton nodeId={nodeId} fieldName={fieldName} />
|
||||
<Tooltip
|
||||
label={
|
||||
<FieldTooltipContent
|
||||
nodeId={fieldIdentifier.nodeId}
|
||||
fieldName={fieldIdentifier.fieldName}
|
||||
kind="inputs"
|
||||
/>
|
||||
}
|
||||
label={<FieldTooltipContent nodeId={nodeId} fieldName={fieldName} kind="inputs" />}
|
||||
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
|
||||
placement="top"
|
||||
>
|
||||
@@ -99,20 +82,12 @@ const LinearViewFieldInternal = ({ fieldIdentifier }: Props) => {
|
||||
icon={<PiTrashSimpleBold />}
|
||||
/>
|
||||
</Flex>
|
||||
<InputFieldRenderer nodeId={fieldIdentifier.nodeId} fieldName={fieldIdentifier.fieldName} />
|
||||
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<DndListDropIndicator dndState={dndListState} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
const LinearViewField = ({ fieldIdentifier }: Props) => {
|
||||
return (
|
||||
<InvocationInputFieldCheck nodeId={fieldIdentifier.nodeId} fieldName={fieldIdentifier.fieldName}>
|
||||
<LinearViewFieldInternal fieldIdentifier={fieldIdentifier} />
|
||||
</InvocationInputFieldCheck>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(LinearViewField);
|
||||
InputFieldViewLinear.displayName = 'InputFieldViewLinear';
|
||||
@@ -1,4 +1,5 @@
|
||||
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';
|
||||
@@ -8,7 +9,6 @@ import { useFieldIsInvalid } from 'features/nodes/hooks/useFieldIsInvalid';
|
||||
import { memo, useCallback, useState } from 'react';
|
||||
|
||||
import EditableFieldTitle from './EditableFieldTitle';
|
||||
import FieldHandle from './FieldHandle';
|
||||
import FieldLinearViewToggle from './FieldLinearViewToggle';
|
||||
import InputFieldRenderer from './InputFieldRenderer';
|
||||
import { InputFieldWrapper } from './InputFieldWrapper';
|
||||
@@ -18,13 +18,13 @@ interface Props {
|
||||
fieldName: string;
|
||||
}
|
||||
|
||||
const InputField = ({ nodeId, fieldName }: Props) => {
|
||||
export const InputFieldViewNodes = memo(({ nodeId, fieldName }: Props) => {
|
||||
const fieldTemplate = useFieldInputTemplate(nodeId, fieldName);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const isInvalid = useFieldIsInvalid(nodeId, fieldName);
|
||||
|
||||
const { isConnected, isConnectionInProgress, isConnectionStartField, validationResult, shouldDim } =
|
||||
useConnectionState({ nodeId, fieldName, kind: 'inputs' });
|
||||
useConnectionState(nodeId, fieldName, 'inputs');
|
||||
|
||||
const onMouseEnter = useCallback(() => {
|
||||
setIsHovered(true);
|
||||
@@ -49,8 +49,8 @@ const InputField = ({ nodeId, fieldName }: Props) => {
|
||||
</FormControl>
|
||||
|
||||
<FieldHandle
|
||||
fieldTemplate={fieldTemplate}
|
||||
handleType="target"
|
||||
fieldTemplate={fieldTemplate}
|
||||
isConnectionInProgress={isConnectionInProgress}
|
||||
isConnectionStartField={isConnectionStartField}
|
||||
validationResult={validationResult}
|
||||
@@ -73,10 +73,14 @@ const InputField = ({ 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 />
|
||||
{isHovered && <FieldLinearViewConfigIconButton nodeId={nodeId} fieldName={fieldName} />}
|
||||
{isHovered && <FieldNotesIconButton nodeId={nodeId} fieldName={fieldName} />}
|
||||
{isHovered && <FieldResetToDefaultValueButton nodeId={nodeId} fieldName={fieldName} />}
|
||||
{isHovered && <FieldLinearViewToggle nodeId={nodeId} fieldName={fieldName} />}
|
||||
{isHovered && (
|
||||
<>
|
||||
<FieldLinearViewConfigIconButton nodeId={nodeId} fieldName={fieldName} />
|
||||
<FieldNotesIconButton nodeId={nodeId} fieldName={fieldName} />
|
||||
<FieldResetToDefaultValueButton nodeId={nodeId} fieldName={fieldName} />
|
||||
<FieldLinearViewToggle nodeId={nodeId} fieldName={fieldName} />
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
|
||||
</Flex>
|
||||
@@ -84,8 +88,8 @@ const InputField = ({ nodeId, fieldName }: Props) => {
|
||||
|
||||
{fieldTemplate.input !== 'direct' && (
|
||||
<FieldHandle
|
||||
fieldTemplate={fieldTemplate}
|
||||
handleType="target"
|
||||
fieldTemplate={fieldTemplate}
|
||||
isConnectionInProgress={isConnectionInProgress}
|
||||
isConnectionStartField={isConnectionStartField}
|
||||
validationResult={validationResult}
|
||||
@@ -93,6 +97,6 @@ const InputField = ({ nodeId, fieldName }: Props) => {
|
||||
)}
|
||||
</InputFieldWrapper>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default memo(InputField);
|
||||
InputFieldViewNodes.displayName = 'InputFieldViewNodes';
|
||||
@@ -0,0 +1,31 @@
|
||||
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';
|
||||
@@ -1,24 +1,29 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
const sx = {
|
||||
position: 'relative',
|
||||
minH: 8,
|
||||
py: 0.5,
|
||||
alignItems: 'center',
|
||||
transitionProperty: 'opacity',
|
||||
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
|
||||
position="relative"
|
||||
minH={8}
|
||||
py={0.5}
|
||||
alignItems="center"
|
||||
opacity={shouldDim ? 0.5 : 1}
|
||||
transitionProperty="opacity"
|
||||
transitionDuration="0.1s"
|
||||
w="full"
|
||||
h="full"
|
||||
>
|
||||
<Flex sx={sx} data-should-dim={shouldDim}>
|
||||
{children}
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { Flex, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { selectInvocationNode, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type Props = PropsWithChildren<{
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
}>;
|
||||
|
||||
export const InvocationInputFieldCheck = memo(({ nodeId, fieldName, children }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const templates = useStore($templates);
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createMemoizedSelector(selectNodesSlice, (nodesSlice) => {
|
||||
const node = selectInvocationNode(nodesSlice, nodeId);
|
||||
const instance = node.data.inputs[fieldName];
|
||||
const template = templates[node.data.type];
|
||||
const fieldTemplate = template?.inputs[fieldName];
|
||||
return {
|
||||
name: instance?.label || fieldTemplate?.title || fieldName,
|
||||
hasInstance: Boolean(instance),
|
||||
hasTemplate: Boolean(fieldTemplate),
|
||||
};
|
||||
}),
|
||||
[fieldName, nodeId, templates]
|
||||
);
|
||||
const { hasInstance, hasTemplate, name } = useAppSelector(selector);
|
||||
|
||||
if (!hasTemplate || !hasInstance) {
|
||||
return (
|
||||
<Flex position="relative" minH={8} py={0.5} alignItems="center" w="full" h="full">
|
||||
<FormControl
|
||||
isInvalid={true}
|
||||
alignItems="stretch"
|
||||
justifyContent="center"
|
||||
flexDir="column"
|
||||
gap={2}
|
||||
h="full"
|
||||
w="full"
|
||||
>
|
||||
<FormLabel display="flex" mb={0} px={1} py={2} gap={2}>
|
||||
{t('nodes.unknownInput', { name })}
|
||||
</FormLabel>
|
||||
</FormControl>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
});
|
||||
|
||||
InvocationInputFieldCheck.displayName = 'InvocationInputFieldCheck';
|
||||
@@ -1,82 +0,0 @@
|
||||
import { Flex, FormControl, FormLabel, Tooltip } from '@invoke-ai/ui-library';
|
||||
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 type { PropsWithChildren } from 'react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import FieldHandle from './FieldHandle';
|
||||
import FieldTooltipContent from './FieldTooltipContent';
|
||||
|
||||
interface Props {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
}
|
||||
|
||||
const OutputField = ({ nodeId, fieldName }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const fieldTemplate = useFieldOutputTemplate(nodeId, fieldName);
|
||||
|
||||
const { isConnected, isConnectionInProgress, isConnectionStartField, validationResult, shouldDim } =
|
||||
useConnectionState({ nodeId, fieldName, kind: 'outputs' });
|
||||
|
||||
if (!fieldTemplate) {
|
||||
return (
|
||||
<OutputFieldWrapper shouldDim={shouldDim}>
|
||||
<FormControl 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: fieldName,
|
||||
})}
|
||||
</FormLabel>
|
||||
</FormControl>
|
||||
</OutputFieldWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<OutputFieldWrapper shouldDim={shouldDim}>
|
||||
<Tooltip
|
||||
label={<FieldTooltipContent nodeId={nodeId} fieldName={fieldName} kind="outputs" />}
|
||||
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
|
||||
placement="top"
|
||||
shouldWrapChildren
|
||||
>
|
||||
<FormControl isDisabled={isConnected} pe={2}>
|
||||
<FormLabel mb={0}>{fieldTemplate?.title}</FormLabel>
|
||||
</FormControl>
|
||||
</Tooltip>
|
||||
<FieldHandle
|
||||
fieldTemplate={fieldTemplate}
|
||||
handleType="source"
|
||||
isConnectionInProgress={isConnectionInProgress}
|
||||
isConnectionStartField={isConnectionStartField}
|
||||
validationResult={validationResult}
|
||||
/>
|
||||
</OutputFieldWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(OutputField);
|
||||
|
||||
type OutputFieldWrapperProps = PropsWithChildren<{
|
||||
shouldDim: boolean;
|
||||
}>;
|
||||
|
||||
const OutputFieldWrapper = memo(({ shouldDim, children }: OutputFieldWrapperProps) => (
|
||||
<Flex
|
||||
position="relative"
|
||||
minH={8}
|
||||
py={0.5}
|
||||
alignItems="center"
|
||||
opacity={shouldDim ? 0.5 : 1}
|
||||
transitionProperty="opacity"
|
||||
transitionDuration="0.1s"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
{children}
|
||||
</Flex>
|
||||
));
|
||||
|
||||
OutputFieldWrapper.displayName = 'OutputFieldWrapper';
|
||||
@@ -0,0 +1,21 @@
|
||||
import { OutputFieldUnknownPlaceholder } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldUnknownPlaceholder';
|
||||
import { useFieldOutputTemplateExists } from 'features/nodes/hooks/useFieldOutputTemplateExists';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
type Props = PropsWithChildren<{
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
}>;
|
||||
|
||||
export const OutputFieldGate = memo(({ nodeId, fieldName, children }: Props) => {
|
||||
const hasTemplate = useFieldOutputTemplateExists(nodeId, fieldName);
|
||||
|
||||
if (!hasTemplate) {
|
||||
return <OutputFieldUnknownPlaceholder nodeId={nodeId} fieldName={fieldName} />;
|
||||
}
|
||||
|
||||
return children;
|
||||
});
|
||||
|
||||
OutputFieldGate.displayName = 'OutputFieldGate';
|
||||
@@ -0,0 +1,46 @@
|
||||
import { FormControl, FormLabel, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { FieldHandle } from 'features/nodes/components/flow/nodes/Invocation/fields/FieldHandle';
|
||||
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 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');
|
||||
|
||||
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>
|
||||
<FieldHandle
|
||||
handleType="source"
|
||||
fieldTemplate={fieldTemplate}
|
||||
isConnectionInProgress={isConnectionInProgress}
|
||||
isConnectionStartField={isConnectionStartField}
|
||||
validationResult={validationResult}
|
||||
/>
|
||||
</OutputFieldWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
OutputFieldNodesEditorView.displayName = 'OutputFieldNodesEditorView';
|
||||
@@ -0,0 +1,27 @@
|
||||
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 { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type Props = {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export const OutputFieldUnknownPlaceholder = memo(({ nodeId, fieldName }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const name = useFieldOutputName(nodeId, fieldName);
|
||||
|
||||
return (
|
||||
<OutputFieldWrapper shouldDim={false}>
|
||||
<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 })}
|
||||
</FormLabel>
|
||||
</FormControl>
|
||||
</OutputFieldWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
OutputFieldUnknownPlaceholder.displayName = 'OutputFieldUnknownPlaceholder';
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
const sx = {
|
||||
position: 'relative',
|
||||
minH: 8,
|
||||
py: 0.5,
|
||||
alignItems: 'center',
|
||||
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>
|
||||
));
|
||||
|
||||
OutputFieldWrapper.displayName = 'OutputFieldWrapper';
|
||||
@@ -12,9 +12,9 @@ import { memo, useCallback, useRef } from 'react';
|
||||
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
|
||||
import { Panel, PanelGroup } from 'react-resizable-panels';
|
||||
|
||||
import InspectorPanel from './inspector/InspectorPanel';
|
||||
import { WorkflowViewMode } from './viewMode/WorkflowViewMode';
|
||||
import WorkflowPanel from './workflow/WorkflowPanel';
|
||||
import WorkflowNodeInspectorPanel from './inspector/WorkflowNodeInspectorPanel';
|
||||
import { FieldsSimpleView } from './viewMode/WorkflowSimpleView';
|
||||
import WorkflowFieldsLinearViewPanel from './workflow/WorkflowPanel';
|
||||
import { WorkflowListMenu } from './WorkflowListMenu/WorkflowListMenu';
|
||||
import { WorkflowListMenuTrigger } from './WorkflowListMenu/WorkflowListMenuTrigger';
|
||||
|
||||
@@ -25,7 +25,7 @@ const overlayScrollbarsStyles: CSSProperties = {
|
||||
width: '100%',
|
||||
};
|
||||
|
||||
const NodeEditorPanelGroup = () => {
|
||||
const WorkflowsLeftPanel = () => {
|
||||
const mode = useAppSelector(selectWorkflowMode);
|
||||
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
||||
const workflowListMenu = useWorkflowListMenu();
|
||||
@@ -50,9 +50,9 @@ const NodeEditorPanelGroup = () => {
|
||||
</OverlayScrollbarsComponent>
|
||||
)}
|
||||
|
||||
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
|
||||
{mode === 'view' && <WorkflowViewMode />}
|
||||
{mode === 'edit' && (
|
||||
{mode === 'view' && <FieldsSimpleView />}
|
||||
{mode === 'edit' && (
|
||||
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
|
||||
<PanelGroup
|
||||
ref={panelGroupRef}
|
||||
id="workflow-panel-group"
|
||||
@@ -61,19 +61,19 @@ const NodeEditorPanelGroup = () => {
|
||||
style={panelGroupStyles}
|
||||
>
|
||||
<Panel id="workflow" collapsible minSize={25}>
|
||||
<WorkflowPanel />
|
||||
<WorkflowFieldsLinearViewPanel />
|
||||
</Panel>
|
||||
<ResizeHandle onDoubleClick={handleDoubleClickHandle} />
|
||||
<Panel id="inspector" collapsible minSize={25}>
|
||||
<InspectorPanel />
|
||||
<WorkflowNodeInspectorPanel />
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
)}
|
||||
</OverlayScrollbarsComponent>
|
||||
</OverlayScrollbarsComponent>
|
||||
)}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(NodeEditorPanelGroup);
|
||||
export default memo(WorkflowsLeftPanel);
|
||||
|
||||
@@ -7,7 +7,7 @@ import InspectorDetailsTab from './InspectorDetailsTab';
|
||||
import InspectorOutputsTab from './InspectorOutputsTab';
|
||||
import InspectorTemplateTab from './InspectorTemplateTab';
|
||||
|
||||
const InspectorPanel = () => {
|
||||
const WorkflowNodeInspectorPanel = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Flex layerStyle="first" flexDir="column" w="full" h="full" borderRadius="base" p={2} gap={2}>
|
||||
@@ -38,4 +38,4 @@ const InspectorPanel = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(InspectorPanel);
|
||||
export default memo(WorkflowNodeInspectorPanel);
|
||||
@@ -1,52 +0,0 @@
|
||||
import { Flex, FormLabel, IconButton, Spacer } from '@invoke-ai/ui-library';
|
||||
import { FieldNotesIconButton } from 'features/nodes/components/flow/nodes/Invocation/fields/FieldNotesIconButton';
|
||||
import InputFieldRenderer from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer';
|
||||
import { InvocationInputFieldCheck } from 'features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck';
|
||||
import { useFieldLabel } from 'features/nodes/hooks/useFieldLabel';
|
||||
import { useFieldOriginalValue } from 'features/nodes/hooks/useFieldOriginalValue';
|
||||
import { useFieldTemplateTitle } from 'features/nodes/hooks/useFieldTemplateTitle';
|
||||
import { t } from 'i18next';
|
||||
import { memo } from 'react';
|
||||
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
|
||||
|
||||
type Props = {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
const WorkflowFieldInternal = ({ nodeId, fieldName }: Props) => {
|
||||
const label = useFieldLabel(nodeId, fieldName);
|
||||
const fieldTemplateTitle = useFieldTemplateTitle(nodeId, fieldName, 'inputs');
|
||||
const { isValueChanged, onReset } = useFieldOriginalValue(nodeId, fieldName);
|
||||
|
||||
return (
|
||||
<Flex position="relative" w="full" gap="2" flexDir="column">
|
||||
<Flex alignItems="center" gap={1}>
|
||||
<FormLabel fontSize="sm">{label || fieldTemplateTitle}</FormLabel>
|
||||
<Spacer />
|
||||
{isValueChanged && (
|
||||
<IconButton
|
||||
aria-label={t('nodes.resetToDefaultValue')}
|
||||
tooltip={t('nodes.resetToDefaultValue')}
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
onClick={onReset}
|
||||
icon={<PiArrowCounterClockwiseBold />}
|
||||
/>
|
||||
)}
|
||||
<FieldNotesIconButton nodeId={nodeId} fieldName={fieldName} readOnly />
|
||||
</Flex>
|
||||
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const WorkflowField = ({ nodeId, fieldName }: Props) => {
|
||||
return (
|
||||
<InvocationInputFieldCheck nodeId={nodeId} fieldName={fieldName}>
|
||||
<WorkflowFieldInternal nodeId={nodeId} fieldName={fieldName} />
|
||||
</InvocationInputFieldCheck>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(WorkflowField);
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||
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 { InputFieldGate } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate';
|
||||
import { InputFieldViewSimple } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldViewSimple';
|
||||
import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
|
||||
import { t } from 'i18next';
|
||||
import { memo } from 'react';
|
||||
import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo';
|
||||
|
||||
import { EmptyState } from './EmptyState';
|
||||
|
||||
const selectExposedFields = createMemoizedSelector(selectWorkflowSlice, (workflow) => workflow.exposedFields);
|
||||
|
||||
export const FieldsSimpleView = 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">
|
||||
<FieldSimpleViewContent />
|
||||
</Flex>
|
||||
</ScrollableContent>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
FieldsSimpleView.displayName = 'FieldsSimpleView';
|
||||
|
||||
const FieldSimpleViewContent = memo(() => {
|
||||
const { isLoading } = useGetOpenAPISchemaQuery();
|
||||
const exposedFields = useAppSelector(selectExposedFields);
|
||||
|
||||
if (isLoading) {
|
||||
return <IAINoContentFallback label={t('nodes.loadingNodes')} icon={null} />;
|
||||
}
|
||||
|
||||
if (exposedFields.length === 0) {
|
||||
return <EmptyState />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{exposedFields.map(({ nodeId, fieldName }) => (
|
||||
<InputFieldGate key={`${nodeId}.${fieldName}`} nodeId={nodeId} fieldName={fieldName}>
|
||||
<InputFieldViewSimple nodeId={nodeId} fieldName={fieldName} />
|
||||
</InputFieldGate>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
FieldSimpleViewContent.displayName = 'FieldSimpleViewContent';
|
||||
@@ -1,40 +0,0 @@
|
||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||
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 { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
|
||||
import { t } from 'i18next';
|
||||
import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo';
|
||||
|
||||
import { EmptyState } from './EmptyState';
|
||||
import WorkflowField from './WorkflowField';
|
||||
|
||||
const selector = createMemoizedSelector(selectWorkflowSlice, (workflow) => {
|
||||
return {
|
||||
fields: workflow.exposedFields,
|
||||
name: workflow.name,
|
||||
};
|
||||
});
|
||||
|
||||
export const WorkflowViewMode = () => {
|
||||
const { isLoading } = useGetOpenAPISchemaQuery();
|
||||
const { fields } = useAppSelector(selector);
|
||||
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">
|
||||
{isLoading ? (
|
||||
<IAINoContentFallback label={t('nodes.loadingNodes')} icon={null} />
|
||||
) : fields.length ? (
|
||||
fields.map(({ nodeId, fieldName }) => (
|
||||
<WorkflowField key={`${nodeId}.${fieldName}`} nodeId={nodeId} fieldName={fieldName} />
|
||||
))
|
||||
) : (
|
||||
<EmptyState />
|
||||
)}
|
||||
</Flex>
|
||||
</ScrollableContent>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { extractClosestEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
|
||||
import { reorderWithEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/util/reorder-with-edge';
|
||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
@@ -10,7 +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 LinearViewFieldInternal from 'features/nodes/components/flow/nodes/Invocation/fields/LinearViewField';
|
||||
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';
|
||||
@@ -23,13 +24,11 @@ const selector = createMemoizedSelector(selectWorkflowSlice, (workflow) => workf
|
||||
|
||||
const WorkflowLinearTab = () => {
|
||||
return (
|
||||
<Box position="relative" w="full" h="full">
|
||||
<ScrollableContent>
|
||||
<Flex position="relative" flexDir="column" alignItems="flex-start" p={1} py={2} gap={2} h="full" w="full">
|
||||
<FieldListContent />
|
||||
</Flex>
|
||||
</ScrollableContent>
|
||||
</Box>
|
||||
<ScrollableContent>
|
||||
<Flex position="relative" flexDir="column" alignItems="flex-start" p={1} py={2} gap={2} h="full" w="full">
|
||||
<FieldListContent />
|
||||
</Flex>
|
||||
</ScrollableContent>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -142,11 +141,10 @@ const FieldListInnerContent = memo(({ fields }: { fields: FieldIdentifier[] }) =
|
||||
|
||||
return (
|
||||
<>
|
||||
{fields.map((fieldIdentifier) => (
|
||||
<LinearViewFieldInternal
|
||||
key={`${fieldIdentifier.nodeId}.${fieldIdentifier.fieldName}`}
|
||||
fieldIdentifier={fieldIdentifier}
|
||||
/>
|
||||
{fields.map(({ nodeId, fieldName }) => (
|
||||
<InputFieldGate key={`${nodeId}.${fieldName}`} nodeId={nodeId} fieldName={fieldName}>
|
||||
<InputFieldViewLinear nodeId={nodeId} fieldName={fieldName} />
|
||||
</InputFieldGate>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ import WorkflowGeneralTab from './WorkflowGeneralTab';
|
||||
import WorkflowJSONTab from './WorkflowJSONTab';
|
||||
import WorkflowLinearTab from './WorkflowLinearTab';
|
||||
|
||||
const WorkflowPanel = () => {
|
||||
const WorkflowFieldsLinearViewPanel = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Flex layerStyle="first" flexDir="column" w="full" h="full" borderRadius="base" p={2} gap={2}>
|
||||
@@ -33,4 +33,4 @@ const WorkflowPanel = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(WorkflowPanel);
|
||||
export default memo(WorkflowFieldsLinearViewPanel);
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { PropsWithChildren } from 'react';
|
||||
import { createContext, memo, useContext } from 'react';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
type ViewType = 'user-linear' | 'editor-linear' | 'editor-nodes';
|
||||
type ViewType = 'linear-user' | 'linear-editor' | 'nodes-editor';
|
||||
|
||||
const ViewContext = createContext<ViewType | null>(null);
|
||||
|
||||
|
||||
@@ -6,29 +6,22 @@ import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { makeConnectionErrorSelector } from 'features/nodes/store/util/makeConnectionErrorSelector';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
type UseConnectionStateProps = {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
kind: 'inputs' | 'outputs';
|
||||
};
|
||||
|
||||
export const useConnectionState = ({ nodeId, fieldName, kind }: UseConnectionStateProps) => {
|
||||
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) =>
|
||||
Boolean(
|
||||
nodes.edges.filter((edge) => {
|
||||
return (
|
||||
(kind === 'inputs' ? edge.target : edge.source) === nodeId &&
|
||||
(kind === 'inputs' ? edge.targetHandle : edge.sourceHandle) === fieldName
|
||||
);
|
||||
}).length
|
||||
)
|
||||
),
|
||||
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]
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate';
|
||||
import { useFieldValue } from 'features/nodes/hooks/useFieldValue';
|
||||
import { fieldValueReset } from 'features/nodes/store/nodesSlice';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
export const useFieldDefaultValue = (nodeId: string, fieldName: string) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const value = useFieldValue(nodeId, fieldName);
|
||||
const fieldTemplate = useFieldInputTemplate(nodeId, fieldName);
|
||||
|
||||
const isValueChanged = useMemo(() => {
|
||||
return !isEqual(value, fieldTemplate.default);
|
||||
}, [value, fieldTemplate.default]);
|
||||
|
||||
const resetToDefaultValue = useCallback(() => {
|
||||
dispatch(fieldValueReset({ nodeId, fieldName, value: fieldTemplate.default }));
|
||||
}, [dispatch, fieldName, fieldTemplate.default, nodeId]);
|
||||
|
||||
return { defaultValue: fieldTemplate.default, isValueChanged, resetToDefaultValue };
|
||||
};
|
||||
@@ -6,9 +6,9 @@ import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
export const useFieldOriginalValue = (nodeId: string, fieldName: string) => {
|
||||
export const useFieldInitialLinearViewValue = (nodeId: string, fieldName: string) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const selectOriginalExposedFieldValues = useMemo(
|
||||
const selectInitialLinearViewValue = useMemo(
|
||||
() =>
|
||||
createSelector(
|
||||
selectWorkflowSlice,
|
||||
@@ -17,12 +17,12 @@ export const useFieldOriginalValue = (nodeId: string, fieldName: string) => {
|
||||
),
|
||||
[nodeId, fieldName]
|
||||
);
|
||||
const originalValue = useAppSelector(selectOriginalExposedFieldValues);
|
||||
const initialLinearViewValue = useAppSelector(selectInitialLinearViewValue);
|
||||
const value = useFieldValue(nodeId, fieldName);
|
||||
const isValueChanged = useMemo(() => !isEqual(value, originalValue), [value, originalValue]);
|
||||
const onReset = useCallback(() => {
|
||||
dispatch(fieldValueReset({ nodeId, fieldName, value: originalValue }));
|
||||
}, [dispatch, fieldName, nodeId, originalValue]);
|
||||
const isValueChanged = useMemo(() => !isEqual(value, initialLinearViewValue), [value, initialLinearViewValue]);
|
||||
const resetToInitialLinearViewValue = useCallback(() => {
|
||||
dispatch(fieldValueReset({ nodeId, fieldName, value: initialLinearViewValue }));
|
||||
}, [dispatch, fieldName, nodeId, initialLinearViewValue]);
|
||||
|
||||
return { originalValue, isValueChanged, onReset };
|
||||
return { initialLinearViewValue, isValueChanged, resetToInitialLinearViewValue };
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
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) => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodesSlice) => {
|
||||
const node = selectInvocationNode(nodesSlice, nodeId);
|
||||
const instance = node.data.inputs[fieldName];
|
||||
return Boolean(instance);
|
||||
}),
|
||||
[fieldName, nodeId]
|
||||
);
|
||||
|
||||
const exists = useAppSelector(selector);
|
||||
|
||||
return exists;
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
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) => {
|
||||
const templates = useStore($templates);
|
||||
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodesSlice) => {
|
||||
const node = selectInvocationNode(nodesSlice, nodeId);
|
||||
const instance = node.data.inputs[fieldName];
|
||||
const nodeTemplate = templates[node.data.type];
|
||||
const fieldTemplate = nodeTemplate?.inputs[fieldName];
|
||||
const name = instance?.label || fieldTemplate?.title || fieldName;
|
||||
return name;
|
||||
}),
|
||||
[fieldName, nodeId, templates]
|
||||
);
|
||||
|
||||
const name = useAppSelector(selector);
|
||||
|
||||
return name;
|
||||
};
|
||||
@@ -7,7 +7,7 @@ export const useFieldInputTemplate = (nodeId: string, fieldName: string): FieldI
|
||||
const template = useNodeTemplate(nodeId);
|
||||
const fieldTemplate = useMemo(() => {
|
||||
const _fieldTemplate = template.inputs[fieldName];
|
||||
assert(_fieldTemplate, `Field template for field ${fieldName} not found`);
|
||||
assert(_fieldTemplate, `Template for input field ${fieldName} not found.`);
|
||||
return _fieldTemplate;
|
||||
}, [fieldName, template.inputs]);
|
||||
return fieldTemplate;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
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) => {
|
||||
const templates = useStore($templates);
|
||||
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodesSlice) => {
|
||||
const node = selectInvocationNode(nodesSlice, nodeId);
|
||||
const nodeTemplate = templates[node.data.type];
|
||||
const fieldTemplate = nodeTemplate?.inputs[fieldName];
|
||||
return Boolean(fieldTemplate);
|
||||
}),
|
||||
[fieldName, nodeId, templates]
|
||||
);
|
||||
|
||||
const exists = useAppSelector(selector);
|
||||
|
||||
return exists;
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useFieldIsExposed = (nodeId: string, fieldName: string) => {
|
||||
const selectIsExposed = useMemo(
|
||||
() =>
|
||||
createSelector(selectWorkflowSlice, (workflow) => {
|
||||
return Boolean(workflow.exposedFields.find((f) => f.nodeId === nodeId && f.fieldName === fieldName));
|
||||
}),
|
||||
[fieldName, nodeId]
|
||||
);
|
||||
|
||||
const isExposed = useAppSelector(selectIsExposed);
|
||||
return isExposed;
|
||||
};
|
||||
@@ -22,7 +22,7 @@ import { useMemo } from 'react';
|
||||
|
||||
export const useFieldIsInvalid = (nodeId: string, fieldName: string) => {
|
||||
const template = useFieldInputTemplate(nodeId, fieldName);
|
||||
const connectionState = useConnectionState({ nodeId, fieldName, kind: 'inputs' });
|
||||
const connectionState = useConnectionState(nodeId, fieldName, 'inputs');
|
||||
|
||||
const selectIsInvalid = useMemo(() => {
|
||||
return createSelector(selectNodesSlice, (nodes) => {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
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) => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodes) => {
|
||||
const node = nodes.nodes.find((node) => node.id === nodeId);
|
||||
if (!isInvocationNode(node)) {
|
||||
return;
|
||||
}
|
||||
return node?.data.inputs[fieldName]?.linearViewConfig;
|
||||
}),
|
||||
[fieldName, nodeId]
|
||||
);
|
||||
|
||||
const linearViewConfig = useAppSelector(selector);
|
||||
return linearViewConfig;
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
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) => {
|
||||
const templates = useStore($templates);
|
||||
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodesSlice) => {
|
||||
const node = selectInvocationNode(nodesSlice, nodeId);
|
||||
const nodeTemplate = templates[node.data.type];
|
||||
const fieldTemplate = nodeTemplate?.outputs[fieldName];
|
||||
const name = fieldTemplate?.title || fieldName;
|
||||
return name;
|
||||
}),
|
||||
[fieldName, nodeId, templates]
|
||||
);
|
||||
|
||||
const name = useAppSelector(selector);
|
||||
|
||||
return name;
|
||||
};
|
||||
@@ -1,9 +1,14 @@
|
||||
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
|
||||
import type { FieldOutputTemplate } from 'features/nodes/types/field';
|
||||
import { useMemo } from 'react';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
export const useFieldOutputTemplate = (nodeId: string, fieldName: string): FieldOutputTemplate | null => {
|
||||
export const useFieldOutputTemplate = (nodeId: string, fieldName: string): FieldOutputTemplate => {
|
||||
const template = useNodeTemplate(nodeId);
|
||||
const fieldTemplate = useMemo(() => template.outputs[fieldName] ?? null, [fieldName, template.outputs]);
|
||||
const fieldTemplate = useMemo(() => {
|
||||
const _fieldTemplate = template.outputs[fieldName];
|
||||
assert(_fieldTemplate, `Template for output field ${fieldName} not found`);
|
||||
return _fieldTemplate;
|
||||
}, [fieldName, template.outputs]);
|
||||
return fieldTemplate;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
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) => {
|
||||
const templates = useStore($templates);
|
||||
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodesSlice) => {
|
||||
const node = selectInvocationNode(nodesSlice, nodeId);
|
||||
const nodeTemplate = templates[node.data.type];
|
||||
const fieldTemplate = nodeTemplate?.outputs[fieldName];
|
||||
return Boolean(fieldTemplate);
|
||||
}),
|
||||
[fieldName, nodeId, templates]
|
||||
);
|
||||
|
||||
const exists = useAppSelector(selector);
|
||||
|
||||
return exists;
|
||||
};
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
ControlLoRAModelFieldValue,
|
||||
ControlNetModelFieldValue,
|
||||
EnumFieldValue,
|
||||
FieldInputInstance,
|
||||
FieldValue,
|
||||
FloatFieldValue,
|
||||
FloatGeneratorFieldValue,
|
||||
@@ -424,6 +425,21 @@ export const nodesSlice = createSlice({
|
||||
}
|
||||
field.notes = val;
|
||||
},
|
||||
fieldLinearViewConfigChanged: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
linearViewConfig?: FieldInputInstance['linearViewConfig'];
|
||||
}>
|
||||
) => {
|
||||
const { nodeId, fieldName, linearViewConfig } = action.payload;
|
||||
const field = getField(nodeId, fieldName, state);
|
||||
if (!field) {
|
||||
return;
|
||||
}
|
||||
field.linearViewConfig = linearViewConfig;
|
||||
},
|
||||
notesNodeValueChanged: (state, action: PayloadAction<{ nodeId: string; value: string }>) => {
|
||||
const { nodeId, value } = action.payload;
|
||||
const nodeIndex = state.nodes.findIndex((n) => n.id === nodeId);
|
||||
@@ -492,6 +508,7 @@ export const {
|
||||
fieldIntegerGeneratorValueChanged,
|
||||
fieldStringGeneratorValueChanged,
|
||||
fieldNotesChanged,
|
||||
fieldLinearViewConfigChanged,
|
||||
nodeEditorReset,
|
||||
nodeIsIntermediateChanged,
|
||||
nodeIsOpenChanged,
|
||||
|
||||
@@ -35,10 +35,14 @@ import { zBoardField, zColorField, zImageField, zModelIdentifierField, zSchedule
|
||||
// #region Base schemas & misc
|
||||
const zFieldInput = z.enum(['connection', 'direct', 'any', 'batch']);
|
||||
const zFieldUIComponent = z.enum(['none', 'textarea', 'slider']);
|
||||
const zFieldInputInstanceLinearViewConfigBase = z.object({
|
||||
notesDisplay: z.enum(['none', 'helper-text', 'icon-with-popover']).nullish(),
|
||||
});
|
||||
const zFieldInputInstanceBase = z.object({
|
||||
name: z.string().trim().min(1),
|
||||
label: z.string().nullish(),
|
||||
notes: z.string().nullish(),
|
||||
linearViewConfig: zFieldInputInstanceLinearViewConfigBase.nullish(),
|
||||
});
|
||||
const zFieldTemplateBase = z.object({
|
||||
name: z.string().min(1),
|
||||
|
||||
@@ -5,7 +5,7 @@ import { CanvasRightPanel } from 'features/controlLayers/components/CanvasRightP
|
||||
import { useDndMonitor } from 'features/dnd/useDndMonitor';
|
||||
import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent';
|
||||
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
|
||||
import NodeEditorPanelGroup from 'features/nodes/components/sidePanel/NodeEditorPanelGroup';
|
||||
import WorkflowsLeftPanel from 'features/nodes/components/sidePanel/NodeEditorPanelGroup';
|
||||
import QueueControls from 'features/queue/components/QueueControls';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton';
|
||||
@@ -13,7 +13,7 @@ import FloatingParametersPanelButtons from 'features/ui/components/FloatingParam
|
||||
import ParametersPanelTextToImage from 'features/ui/components/ParametersPanels/ParametersPanelTextToImage';
|
||||
import ModelManagerTab from 'features/ui/components/tabs/ModelManagerTab';
|
||||
import QueueTab from 'features/ui/components/tabs/QueueTab';
|
||||
import { WorkflowsTabContent } from 'features/ui/components/tabs/WorkflowsTabContent';
|
||||
import { WorkflowsMainPanel } from 'features/ui/components/tabs/WorkflowsTabContent';
|
||||
import { VerticalNavBar } from 'features/ui/components/VerticalNavBar';
|
||||
import type { UsePanelOptions } from 'features/ui/hooks/usePanel';
|
||||
import { usePanel } from 'features/ui/hooks/usePanel';
|
||||
@@ -165,7 +165,6 @@ const RightPanelContent = memo(() => {
|
||||
if (tab === 'canvas') {
|
||||
return <CanvasRightPanel />;
|
||||
}
|
||||
|
||||
if (tab === 'upscaling' || tab === 'workflows') {
|
||||
return <GalleryPanelContent />;
|
||||
}
|
||||
@@ -183,9 +182,8 @@ const LeftPanelContent = memo(() => {
|
||||
if (tab === 'upscaling') {
|
||||
return <ParametersPanelUpscale />;
|
||||
}
|
||||
|
||||
if (tab === 'workflows') {
|
||||
return <NodeEditorPanelGroup />;
|
||||
return <WorkflowsLeftPanel />;
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -194,6 +192,7 @@ LeftPanelContent.displayName = 'LeftPanelContent';
|
||||
|
||||
const MainPanelContent = memo(() => {
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
|
||||
if (tab === 'canvas') {
|
||||
return <CanvasMainPanelContent />;
|
||||
}
|
||||
@@ -201,7 +200,7 @@ const MainPanelContent = memo(() => {
|
||||
return <ImageViewer />;
|
||||
}
|
||||
if (tab === 'workflows') {
|
||||
return <WorkflowsTabContent />;
|
||||
return <WorkflowsMainPanel />;
|
||||
}
|
||||
if (tab === 'models') {
|
||||
return <ModelManagerTab />;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { selectWorkflowMode } from 'features/nodes/store/workflowSlice';
|
||||
import { memo } from 'react';
|
||||
import { ReactFlowProvider } from 'reactflow';
|
||||
|
||||
export const WorkflowsTabContent = memo(() => {
|
||||
export const WorkflowsMainPanel = memo(() => {
|
||||
const mode = useAppSelector(selectWorkflowMode);
|
||||
|
||||
if (mode === 'edit') {
|
||||
@@ -19,4 +19,4 @@ export const WorkflowsTabContent = memo(() => {
|
||||
return <ImageViewer />;
|
||||
});
|
||||
|
||||
WorkflowsTabContent.displayName = 'WorkflowsTabContent';
|
||||
WorkflowsMainPanel.displayName = 'WorkflowsMainPanel';
|
||||
|
||||
Reference in New Issue
Block a user