mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): field context menu, add/remove from linear ui
This commit is contained in:
parent
64a6aa0293
commit
84cf8bdc08
@ -18,6 +18,7 @@ const SelectionOverlay = ({ isSelected, isHovered }: Props) => {
|
||||
opacity: isSelected ? 1 : 0.7,
|
||||
transitionProperty: 'common',
|
||||
transitionDuration: '0.1s',
|
||||
pointerEvents: 'none',
|
||||
shadow: isSelected
|
||||
? isHovered
|
||||
? 'hoverSelected.light'
|
||||
|
@ -79,6 +79,7 @@ const NodeTitle = ({ nodeId, title }: Props) => {
|
||||
fontSize="sm"
|
||||
sx={{
|
||||
p: 0,
|
||||
fontWeight: 'initial',
|
||||
_focusVisible: {
|
||||
p: 0,
|
||||
boxShadow: 'none',
|
||||
|
@ -1,26 +1,111 @@
|
||||
import { MenuItem, MenuList } from '@chakra-ui/react';
|
||||
import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu';
|
||||
import { MenuGroup, MenuItem, MenuList } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import {
|
||||
InputFieldTemplate,
|
||||
InputFieldValue,
|
||||
} from 'features/nodes/types/types';
|
||||
import { MouseEvent, useCallback } from 'react';
|
||||
IAIContextMenu,
|
||||
IAIContextMenuProps,
|
||||
} from 'common/components/IAIContextMenu';
|
||||
import {
|
||||
useFieldInputKind,
|
||||
useFieldLabel,
|
||||
useFieldTemplateTitle,
|
||||
} from 'features/nodes/hooks/useNodeData';
|
||||
import {
|
||||
workflowExposedFieldAdded,
|
||||
workflowExposedFieldRemoved,
|
||||
} from 'features/nodes/store/nodesSlice';
|
||||
import { MouseEvent, ReactNode, memo, useCallback, useMemo } from 'react';
|
||||
import { FaMinus, FaPlus } from 'react-icons/fa';
|
||||
import { menuListMotionProps } from 'theme/components/menu';
|
||||
|
||||
type Props = {
|
||||
nodeId: string;
|
||||
field: InputFieldValue;
|
||||
fieldTemplate: InputFieldTemplate;
|
||||
children: ContextMenuProps<HTMLDivElement>['children'];
|
||||
fieldName: string;
|
||||
kind: 'input' | 'output';
|
||||
children: IAIContextMenuProps<HTMLDivElement>['children'];
|
||||
};
|
||||
|
||||
const FieldContextMenu = (props: Props) => {
|
||||
const FieldContextMenu = ({ nodeId, fieldName, kind, children }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const label = useFieldLabel(nodeId, fieldName);
|
||||
const fieldTemplateTitle = useFieldTemplateTitle(nodeId, fieldName, kind);
|
||||
|
||||
const skipEvent = useCallback((e: MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
}, []);
|
||||
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(
|
||||
stateSelector,
|
||||
({ nodes }) => {
|
||||
const isExposed = Boolean(
|
||||
nodes.workflow.exposedFields.find(
|
||||
(f) => f.nodeId === nodeId && f.fieldName === fieldName
|
||||
)
|
||||
);
|
||||
|
||||
return { isExposed };
|
||||
},
|
||||
defaultSelectorOptions
|
||||
),
|
||||
[fieldName, nodeId]
|
||||
);
|
||||
|
||||
const input = useFieldInputKind(nodeId, fieldName);
|
||||
const mayExpose = useMemo(
|
||||
() => ['any', 'direct'].includes(input ?? '__UNKNOWN_INPUT__'),
|
||||
[input]
|
||||
);
|
||||
|
||||
const { isExposed } = useAppSelector(selector);
|
||||
|
||||
const handleExposeField = useCallback(() => {
|
||||
dispatch(workflowExposedFieldAdded({ nodeId, fieldName }));
|
||||
}, [dispatch, fieldName, nodeId]);
|
||||
|
||||
const handleUnexposeField = useCallback(() => {
|
||||
dispatch(workflowExposedFieldRemoved({ nodeId, fieldName }));
|
||||
}, [dispatch, fieldName, nodeId]);
|
||||
|
||||
const menuItems = useMemo(() => {
|
||||
const menuItems: ReactNode[] = [];
|
||||
if (mayExpose && !isExposed) {
|
||||
menuItems.push(
|
||||
<MenuItem
|
||||
key={`${nodeId}.${fieldName}.expose-field`}
|
||||
icon={<FaPlus />}
|
||||
onClick={handleExposeField}
|
||||
>
|
||||
Add to Linear View
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
if (mayExpose && isExposed) {
|
||||
menuItems.push(
|
||||
<MenuItem
|
||||
key={`${nodeId}.${fieldName}.unexpose-field`}
|
||||
icon={<FaMinus />}
|
||||
onClick={handleUnexposeField}
|
||||
>
|
||||
Remove from Linear View
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
return menuItems;
|
||||
}, [
|
||||
fieldName,
|
||||
handleExposeField,
|
||||
handleUnexposeField,
|
||||
isExposed,
|
||||
mayExpose,
|
||||
nodeId,
|
||||
]);
|
||||
|
||||
return (
|
||||
<ContextMenu<HTMLDivElement>
|
||||
<IAIContextMenu<HTMLDivElement>
|
||||
menuProps={{
|
||||
size: 'sm',
|
||||
isLazy: true,
|
||||
@ -29,19 +114,23 @@ const FieldContextMenu = (props: Props) => {
|
||||
bg: 'transparent',
|
||||
_hover: { bg: 'transparent' },
|
||||
}}
|
||||
renderMenu={() => (
|
||||
<MenuList
|
||||
sx={{ visibility: 'visible !important' }}
|
||||
motionProps={menuListMotionProps}
|
||||
onContextMenu={skipEvent}
|
||||
>
|
||||
<MenuItem>Test</MenuItem>
|
||||
</MenuList>
|
||||
)}
|
||||
renderMenu={() =>
|
||||
!menuItems.length ? null : (
|
||||
<MenuList
|
||||
sx={{ visibility: 'visible !important' }}
|
||||
motionProps={menuListMotionProps}
|
||||
onContextMenu={skipEvent}
|
||||
>
|
||||
<MenuGroup title={label || fieldTemplateTitle || 'Unknown Field'}>
|
||||
{menuItems}
|
||||
</MenuGroup>
|
||||
</MenuList>
|
||||
)
|
||||
}
|
||||
>
|
||||
{props.children}
|
||||
</ContextMenu>
|
||||
{children}
|
||||
</IAIContextMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default FieldContextMenu;
|
||||
export default memo(FieldContextMenu);
|
||||
|
@ -3,63 +3,42 @@ import {
|
||||
EditableInput,
|
||||
EditablePreview,
|
||||
Flex,
|
||||
forwardRef,
|
||||
useEditableControls,
|
||||
} from '@chakra-ui/react';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import IAIDraggable from 'common/components/IAIDraggable';
|
||||
import { NodeFieldDraggableData } from 'features/dnd/types';
|
||||
import {
|
||||
useFieldData,
|
||||
useFieldTemplate,
|
||||
useFieldLabel,
|
||||
useFieldTemplateTitle,
|
||||
} from 'features/nodes/hooks/useNodeData';
|
||||
import { fieldLabelChanged } from 'features/nodes/store/nodesSlice';
|
||||
import {
|
||||
MouseEvent,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { MouseEvent, memo, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
interface Props {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
isDraggable?: boolean;
|
||||
kind: 'input' | 'output';
|
||||
}
|
||||
|
||||
const FieldTitle = (props: Props) => {
|
||||
const { nodeId, fieldName, isDraggable = false, kind } = props;
|
||||
const fieldTemplate = useFieldTemplate(nodeId, fieldName, kind);
|
||||
const field = useFieldData(nodeId, fieldName);
|
||||
const FieldTitle = forwardRef((props: Props, ref) => {
|
||||
const { nodeId, fieldName, kind } = props;
|
||||
const label = useFieldLabel(nodeId, fieldName);
|
||||
const fieldTemplateTitle = useFieldTemplateTitle(nodeId, fieldName, kind);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const [localTitle, setLocalTitle] = useState(
|
||||
field?.label || fieldTemplate?.title || 'Unknown Field'
|
||||
);
|
||||
|
||||
const draggableData: NodeFieldDraggableData | undefined = useMemo(
|
||||
() =>
|
||||
field &&
|
||||
fieldTemplate?.fieldKind === 'input' &&
|
||||
fieldTemplate?.input !== 'connection' &&
|
||||
isDraggable
|
||||
? {
|
||||
id: `${nodeId}-${fieldName}`,
|
||||
payloadType: 'NODE_FIELD',
|
||||
payload: { nodeId, field, fieldTemplate },
|
||||
}
|
||||
: undefined,
|
||||
[field, fieldName, fieldTemplate, isDraggable, nodeId]
|
||||
label || fieldTemplateTitle || 'Unknown Field'
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (newTitle: string) => {
|
||||
if (newTitle === label || newTitle === fieldTemplateTitle) {
|
||||
return;
|
||||
}
|
||||
setLocalTitle(newTitle || fieldTemplateTitle || 'Unknown Field');
|
||||
dispatch(fieldLabelChanged({ nodeId, fieldName, label: newTitle }));
|
||||
setLocalTitle(newTitle || fieldTemplate?.title || 'Unknown Field');
|
||||
},
|
||||
[dispatch, nodeId, fieldName, fieldTemplate?.title]
|
||||
[label, fieldTemplateTitle, dispatch, nodeId, fieldName]
|
||||
);
|
||||
|
||||
const handleChange = useCallback((newTitle: string) => {
|
||||
@ -68,27 +47,33 @@ const FieldTitle = (props: Props) => {
|
||||
|
||||
useEffect(() => {
|
||||
// Another component may change the title; sync local title with global state
|
||||
setLocalTitle(field?.label || fieldTemplate?.title || 'Unknown Field');
|
||||
}, [field?.label, fieldTemplate?.title]);
|
||||
setLocalTitle(label || fieldTemplateTitle || 'Unknown Field');
|
||||
}, [label, fieldTemplateTitle]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
ref={ref}
|
||||
className="nopan"
|
||||
sx={{
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
h: 'full',
|
||||
alignItems: 'flex-start',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
gap: 1,
|
||||
h: 'full',
|
||||
w: 'full',
|
||||
}}
|
||||
>
|
||||
<Editable
|
||||
value={localTitle}
|
||||
onChange={handleChange}
|
||||
onSubmit={handleSubmit}
|
||||
as={Flex}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
alignItems: 'center',
|
||||
h: 'full',
|
||||
w: 'full',
|
||||
}}
|
||||
>
|
||||
<EditablePreview
|
||||
@ -101,6 +86,11 @@ const FieldTitle = (props: Props) => {
|
||||
<EditableInput
|
||||
sx={{
|
||||
p: 0,
|
||||
fontWeight: 600,
|
||||
color: 'base.900',
|
||||
_dark: {
|
||||
color: 'base.100',
|
||||
},
|
||||
_focusVisible: {
|
||||
p: 0,
|
||||
textAlign: 'left',
|
||||
@ -108,27 +98,24 @@ const FieldTitle = (props: Props) => {
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<EditableControls draggableData={draggableData} />
|
||||
<EditableControls />
|
||||
</Editable>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default memo(FieldTitle);
|
||||
|
||||
type EditableControlsProps = {
|
||||
draggableData?: NodeFieldDraggableData;
|
||||
};
|
||||
|
||||
const EditableControls = memo((props: EditableControlsProps) => {
|
||||
const EditableControls = memo(() => {
|
||||
const { isEditing, getEditButtonProps } = useEditableControls();
|
||||
const handleDoubleClick = useCallback(
|
||||
const handleClick = useCallback(
|
||||
(e: MouseEvent<HTMLDivElement>) => {
|
||||
const { onClick } = getEditButtonProps();
|
||||
if (!onClick) {
|
||||
return;
|
||||
}
|
||||
onClick(e);
|
||||
e.preventDefault();
|
||||
},
|
||||
[getEditButtonProps]
|
||||
);
|
||||
@ -137,19 +124,9 @@ const EditableControls = memo((props: EditableControlsProps) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (props.draggableData) {
|
||||
return (
|
||||
<IAIDraggable
|
||||
data={props.draggableData}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
cursor={props.draggableData ? 'grab' : 'text'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex
|
||||
onDoubleClick={handleDoubleClick}
|
||||
onClick={handleClick}
|
||||
position="absolute"
|
||||
w="full"
|
||||
h="full"
|
||||
|
@ -23,7 +23,8 @@ const FieldTooltipContent = ({ nodeId, fieldName, kind }: Props) => {
|
||||
const isInputTemplate = isInputFieldTemplate(fieldTemplate);
|
||||
const fieldTitle = useMemo(() => {
|
||||
if (isInputFieldValue(field)) {
|
||||
if (field.label && fieldTemplate) {
|
||||
console.log(field, fieldTemplate);
|
||||
if (field.label && fieldTemplate?.title) {
|
||||
return `${field.label} (${fieldTemplate.title})`;
|
||||
}
|
||||
|
||||
|
@ -3,13 +3,16 @@ import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
|
||||
import {
|
||||
useDoesInputHaveValue,
|
||||
useFieldTemplate,
|
||||
useIsMouseOverField,
|
||||
} from 'features/nodes/hooks/useNodeData';
|
||||
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
|
||||
import { PropsWithChildren, memo, useMemo } from 'react';
|
||||
import FieldContextMenu from './FieldContextMenu';
|
||||
import FieldHandle from './FieldHandle';
|
||||
import FieldTitle from './FieldTitle';
|
||||
import FieldTooltipContent from './FieldTooltipContent';
|
||||
import InputFieldRenderer from './InputFieldRenderer';
|
||||
import SelectionOverlay from 'common/components/SelectionOverlay';
|
||||
|
||||
interface Props {
|
||||
nodeId: string;
|
||||
@ -48,7 +51,11 @@ const InputField = ({ nodeId, fieldName }: Props) => {
|
||||
|
||||
if (fieldTemplate?.fieldKind !== 'input') {
|
||||
return (
|
||||
<InputFieldWrapper shouldDim={shouldDim}>
|
||||
<InputFieldWrapper
|
||||
nodeId={nodeId}
|
||||
fieldName={fieldName}
|
||||
shouldDim={shouldDim}
|
||||
>
|
||||
<FormControl
|
||||
sx={{ color: 'error.400', textAlign: 'left', fontSize: 'sm' }}
|
||||
>
|
||||
@ -59,40 +66,48 @@ const InputField = ({ nodeId, fieldName }: Props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<InputFieldWrapper shouldDim={shouldDim}>
|
||||
<InputFieldWrapper
|
||||
nodeId={nodeId}
|
||||
fieldName={fieldName}
|
||||
shouldDim={shouldDim}
|
||||
>
|
||||
<FormControl
|
||||
as={Flex}
|
||||
isInvalid={isMissingInput}
|
||||
isDisabled={isConnected}
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
alignItems: 'stretch',
|
||||
justifyContent: 'space-between',
|
||||
ps: 2,
|
||||
gap: 2,
|
||||
h: 'full',
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
label={
|
||||
<FieldTooltipContent
|
||||
nodeId={nodeId}
|
||||
fieldName={fieldName}
|
||||
kind="input"
|
||||
/>
|
||||
}
|
||||
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
|
||||
placement="top"
|
||||
shouldWrapChildren
|
||||
hasArrow
|
||||
>
|
||||
<FormLabel sx={{ mb: 0 }}>
|
||||
<FieldTitle
|
||||
nodeId={nodeId}
|
||||
fieldName={fieldName}
|
||||
kind="input"
|
||||
isDraggable
|
||||
/>
|
||||
</FormLabel>
|
||||
</Tooltip>
|
||||
<FieldContextMenu nodeId={nodeId} fieldName={fieldName} kind="input">
|
||||
{(ref) => (
|
||||
<Tooltip
|
||||
label={
|
||||
<FieldTooltipContent
|
||||
nodeId={nodeId}
|
||||
fieldName={fieldName}
|
||||
kind="input"
|
||||
/>
|
||||
}
|
||||
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
|
||||
placement="top"
|
||||
hasArrow
|
||||
>
|
||||
<FormLabel sx={{ mb: 0 }}>
|
||||
<FieldTitle
|
||||
ref={ref}
|
||||
nodeId={nodeId}
|
||||
fieldName={fieldName}
|
||||
kind="input"
|
||||
/>
|
||||
</FormLabel>
|
||||
</Tooltip>
|
||||
)}
|
||||
</FieldContextMenu>
|
||||
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
|
||||
</FormControl>
|
||||
|
||||
@ -113,27 +128,37 @@ export default InputField;
|
||||
|
||||
type InputFieldWrapperProps = PropsWithChildren<{
|
||||
shouldDim: boolean;
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
}>;
|
||||
|
||||
const InputFieldWrapper = memo(
|
||||
({ shouldDim, children }: InputFieldWrapperProps) => (
|
||||
<Flex
|
||||
className="nopan"
|
||||
sx={{
|
||||
position: 'relative',
|
||||
minH: 8,
|
||||
py: 0.5,
|
||||
alignItems: 'center',
|
||||
opacity: shouldDim ? 0.5 : 1,
|
||||
transitionProperty: 'opacity',
|
||||
transitionDuration: '0.1s',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Flex>
|
||||
)
|
||||
({ shouldDim, nodeId, fieldName, children }: InputFieldWrapperProps) => {
|
||||
const { isMouseOverField, handleMouseOver, handleMouseOut } =
|
||||
useIsMouseOverField(nodeId, fieldName);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
onMouseOver={handleMouseOver}
|
||||
onMouseOut={handleMouseOut}
|
||||
className="nopan"
|
||||
sx={{
|
||||
position: 'relative',
|
||||
minH: 8,
|
||||
py: 0.5,
|
||||
alignItems: 'center',
|
||||
opacity: shouldDim ? 0.5 : 1,
|
||||
transitionProperty: 'opacity',
|
||||
transitionDuration: '0.1s',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<SelectionOverlay isSelected={false} isHovered={isMouseOverField} />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
InputFieldWrapper.displayName = 'InputFieldWrapper';
|
||||
|
@ -1,6 +1,12 @@
|
||||
import { Flex, FormControl, FormLabel, Tooltip } from '@chakra-ui/react';
|
||||
import { Flex, FormControl, FormLabel, Icon, Tooltip } from '@chakra-ui/react';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import SelectionOverlay from 'common/components/SelectionOverlay';
|
||||
import { useIsMouseOverField } from 'features/nodes/hooks/useNodeData';
|
||||
import { workflowExposedFieldRemoved } from 'features/nodes/store/nodesSlice';
|
||||
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
|
||||
import { memo } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { FaInfoCircle, FaTrash } from 'react-icons/fa';
|
||||
import FieldTitle from './FieldTitle';
|
||||
import FieldTooltipContent from './FieldTooltipContent';
|
||||
import InputFieldRenderer from './InputFieldRenderer';
|
||||
@ -11,8 +17,18 @@ type Props = {
|
||||
};
|
||||
|
||||
const LinearViewField = ({ nodeId, fieldName }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { isMouseOverField, handleMouseOut, handleMouseOver } =
|
||||
useIsMouseOverField(nodeId, fieldName);
|
||||
|
||||
const handleRemoveField = useCallback(() => {
|
||||
dispatch(workflowExposedFieldRemoved({ nodeId, fieldName }));
|
||||
}, [dispatch, fieldName, nodeId]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
onMouseOver={handleMouseOver}
|
||||
onMouseOut={handleMouseOut}
|
||||
layerStyle="second"
|
||||
sx={{
|
||||
position: 'relative',
|
||||
@ -22,31 +38,43 @@ const LinearViewField = ({ nodeId, fieldName }: Props) => {
|
||||
}}
|
||||
>
|
||||
<FormControl as={Flex} sx={{ flexDir: 'column', gap: 1, flexShrink: 1 }}>
|
||||
<Tooltip
|
||||
label={
|
||||
<FieldTooltipContent
|
||||
nodeId={nodeId}
|
||||
fieldName={fieldName}
|
||||
kind="input"
|
||||
/>
|
||||
}
|
||||
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
|
||||
placement="top"
|
||||
shouldWrapChildren
|
||||
hasArrow
|
||||
<FormLabel
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
mb: 0,
|
||||
}}
|
||||
>
|
||||
<FormLabel
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
mb: 0,
|
||||
}}
|
||||
<FieldTitle nodeId={nodeId} fieldName={fieldName} kind="input" />
|
||||
<Tooltip
|
||||
label={
|
||||
<FieldTooltipContent
|
||||
nodeId={nodeId}
|
||||
fieldName={fieldName}
|
||||
kind="input"
|
||||
/>
|
||||
}
|
||||
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
|
||||
placement="top"
|
||||
hasArrow
|
||||
>
|
||||
<FieldTitle nodeId={nodeId} fieldName={fieldName} kind="input" />
|
||||
</FormLabel>
|
||||
</Tooltip>
|
||||
<Flex h="full" alignItems="center">
|
||||
<Icon as={FaInfoCircle} />
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
<IAIIconButton
|
||||
aria-label="Remove from Linear View"
|
||||
tooltip="Remove from Linear View"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleRemoveField}
|
||||
icon={<FaTrash />}
|
||||
/>
|
||||
</FormLabel>
|
||||
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
|
||||
</FormControl>
|
||||
<SelectionOverlay isSelected={false} isHovered={isMouseOverField} />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { Flex, Text } from '@chakra-ui/react';
|
||||
import { skipToken } from '@reduxjs/toolkit/dist/query';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import IAIDndImage from 'common/components/IAIDndImage';
|
||||
@ -81,6 +81,9 @@ const ImageInputFieldComponent = (
|
||||
draggableData={draggableData}
|
||||
postUploadAction={postUploadAction}
|
||||
useThumbailFallback
|
||||
uploadElement={<UploadElement />}
|
||||
dropLabel={<DropLabel />}
|
||||
minSize={8}
|
||||
>
|
||||
<IAIDndImageIcon
|
||||
onClick={handleReset}
|
||||
@ -93,3 +96,14 @@ const ImageInputFieldComponent = (
|
||||
};
|
||||
|
||||
export default memo(ImageInputFieldComponent);
|
||||
|
||||
const UploadElement = () => (
|
||||
<Text fontSize={16} fontWeight={600}>
|
||||
Drop or Upload
|
||||
</Text>
|
||||
);
|
||||
const DropLabel = () => (
|
||||
<Text fontSize={16} fontWeight={600}>
|
||||
Drop
|
||||
</Text>
|
||||
);
|
||||
|
@ -3,9 +3,7 @@ import { createSelector } from '@reduxjs/toolkit';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import IAIDroppable from 'common/components/IAIDroppable';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import { AddFieldToLinearViewDropData } from 'features/dnd/types';
|
||||
import { memo } from 'react';
|
||||
import LinearViewField from '../../fields/LinearViewField';
|
||||
import ScrollableContent from '../ScrollableContent';
|
||||
@ -20,11 +18,6 @@ const selector = createSelector(
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
const droppableData: AddFieldToLinearViewDropData = {
|
||||
id: 'add-field-to-linear-view',
|
||||
actionType: 'ADD_FIELD_TO_LINEAR',
|
||||
};
|
||||
|
||||
const LinearTabContent = () => {
|
||||
const { fields } = useAppSelector(selector);
|
||||
|
||||
@ -42,6 +35,7 @@ const LinearTabContent = () => {
|
||||
position: 'relative',
|
||||
flexDir: 'column',
|
||||
alignItems: 'flex-start',
|
||||
p: 1,
|
||||
gap: 2,
|
||||
h: 'full',
|
||||
w: 'full',
|
||||
@ -50,7 +44,7 @@ const LinearTabContent = () => {
|
||||
{fields.length ? (
|
||||
fields.map(({ nodeId, fieldName }) => (
|
||||
<LinearViewField
|
||||
key={`${nodeId}-${fieldName}`}
|
||||
key={`${nodeId}.${fieldName}`}
|
||||
nodeId={nodeId}
|
||||
fieldName={fieldName}
|
||||
/>
|
||||
@ -63,7 +57,6 @@ const LinearTabContent = () => {
|
||||
)}
|
||||
</Flex>
|
||||
</ScrollableContent>
|
||||
<IAIDroppable data={droppableData} dropLabel="Add Field to Linear View" />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { map, some } from 'lodash-es';
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { mouseOverFieldChanged } from '../store/nodesSlice';
|
||||
import { FOOTER_FIELDS, IMAGE_FIELDS } from '../types/constants';
|
||||
import { isInvocationNode } from '../types/types';
|
||||
|
||||
@ -51,6 +52,28 @@ export const useNodeData = (nodeId: string) => {
|
||||
return nodeData;
|
||||
};
|
||||
|
||||
export const useFieldLabel = (nodeId: string, fieldName: string) => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(
|
||||
stateSelector,
|
||||
({ nodes }) => {
|
||||
const node = nodes.nodes.find((node) => node.id === nodeId);
|
||||
if (!isInvocationNode(node)) {
|
||||
return;
|
||||
}
|
||||
return node?.data.inputs[fieldName]?.label;
|
||||
},
|
||||
defaultSelectorOptions
|
||||
),
|
||||
[fieldName, nodeId]
|
||||
);
|
||||
|
||||
const label = useAppSelector(selector);
|
||||
|
||||
return label;
|
||||
};
|
||||
|
||||
export const useFieldData = (nodeId: string, fieldName: string) => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
@ -73,6 +96,30 @@ export const useFieldData = (nodeId: string, fieldName: string) => {
|
||||
return fieldData;
|
||||
};
|
||||
|
||||
export const useFieldInputKind = (nodeId: string, fieldName: string) => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(
|
||||
stateSelector,
|
||||
({ nodes }) => {
|
||||
const node = nodes.nodes.find((node) => node.id === nodeId);
|
||||
if (!isInvocationNode(node)) {
|
||||
return;
|
||||
}
|
||||
const nodeTemplate = nodes.nodeTemplates[node?.data.type ?? ''];
|
||||
const fieldTemplate = nodeTemplate?.inputs[fieldName];
|
||||
return fieldTemplate?.input;
|
||||
},
|
||||
defaultSelectorOptions
|
||||
),
|
||||
[fieldName, nodeId]
|
||||
);
|
||||
|
||||
const fieldType = useAppSelector(selector);
|
||||
|
||||
return fieldType;
|
||||
};
|
||||
|
||||
export const useFieldType = (
|
||||
nodeId: string,
|
||||
fieldName: string,
|
||||
@ -236,6 +283,33 @@ export const useNodeTemplateTitle = (nodeId: string) => {
|
||||
return title;
|
||||
};
|
||||
|
||||
export const useFieldTemplateTitle = (
|
||||
nodeId: string,
|
||||
fieldName: string,
|
||||
kind: 'input' | 'output'
|
||||
) => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(
|
||||
stateSelector,
|
||||
({ nodes }) => {
|
||||
const node = nodes.nodes.find((node) => node.id === nodeId);
|
||||
if (!isInvocationNode(node)) {
|
||||
return;
|
||||
}
|
||||
const nodeTemplate = nodes.nodeTemplates[node?.data.type ?? ''];
|
||||
return nodeTemplate?.[KIND_MAP[kind]][fieldName]?.title;
|
||||
},
|
||||
defaultSelectorOptions
|
||||
),
|
||||
[fieldName, kind, nodeId]
|
||||
);
|
||||
|
||||
const fieldTemplate = useAppSelector(selector);
|
||||
|
||||
return fieldTemplate;
|
||||
};
|
||||
|
||||
export const useFieldTemplate = (
|
||||
nodeId: string,
|
||||
fieldName: string,
|
||||
@ -284,3 +358,30 @@ export const useDoesInputHaveValue = (nodeId: string, fieldName: string) => {
|
||||
|
||||
return doesFieldHaveValue;
|
||||
};
|
||||
|
||||
export const useIsMouseOverField = (nodeId: string, fieldName: string) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(
|
||||
stateSelector,
|
||||
({ nodes }) =>
|
||||
nodes.mouseOverField?.nodeId === nodeId &&
|
||||
nodes.mouseOverField?.fieldName === fieldName,
|
||||
defaultSelectorOptions
|
||||
),
|
||||
[fieldName, nodeId]
|
||||
);
|
||||
|
||||
const isMouseOverField = useAppSelector(selector);
|
||||
|
||||
const handleMouseOver = useCallback(() => {
|
||||
dispatch(mouseOverFieldChanged({ nodeId, fieldName }));
|
||||
}, [dispatch, fieldName, nodeId]);
|
||||
|
||||
const handleMouseOut = useCallback(() => {
|
||||
dispatch(mouseOverFieldChanged(null));
|
||||
}, [dispatch]);
|
||||
|
||||
return { isMouseOverField, handleMouseOver, handleMouseOut };
|
||||
};
|
||||
|
@ -81,6 +81,7 @@ export const initialNodesState: NodesState = {
|
||||
},
|
||||
nodeExecutionStates: {},
|
||||
viewport: { x: 0, y: 0, zoom: 1 },
|
||||
mouseOverField: null,
|
||||
};
|
||||
|
||||
type FieldValueAction<T extends InputFieldValue> = PayloadAction<{
|
||||
@ -594,6 +595,12 @@ const nodesSlice = createSlice({
|
||||
viewportChanged: (state, action: PayloadAction<Viewport>) => {
|
||||
state.viewport = action.payload;
|
||||
},
|
||||
mouseOverFieldChanged: (
|
||||
state,
|
||||
action: PayloadAction<FieldIdentifier | null>
|
||||
) => {
|
||||
state.mouseOverField = action.payload;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(receivedOpenAPISchema.pending, (state) => {
|
||||
@ -701,6 +708,7 @@ export const {
|
||||
workflowExposedFieldRemoved,
|
||||
fieldLabelChanged,
|
||||
viewportChanged,
|
||||
mouseOverFieldChanged,
|
||||
} = nodesSlice.actions;
|
||||
|
||||
export default nodesSlice.reducer;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { OpenAPIV3 } from 'openapi-types';
|
||||
import { Edge, Node, OnConnectStartParams, Viewport } from 'reactflow';
|
||||
import {
|
||||
FieldIdentifier,
|
||||
FieldType,
|
||||
InvocationEdgeExtra,
|
||||
InvocationTemplate,
|
||||
@ -29,4 +30,5 @@ export type NodesState = {
|
||||
nodeExecutionStates: Record<string, NodeExecutionState>;
|
||||
viewport: Viewport;
|
||||
isReady: boolean;
|
||||
mouseOverField: FieldIdentifier | null;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user