feat(ui): field context menu, add/remove from linear ui

This commit is contained in:
psychedelicious 2023-08-17 17:44:34 +10:00
parent 64a6aa0293
commit 84cf8bdc08
12 changed files with 400 additions and 160 deletions

View File

@ -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'

View File

@ -79,6 +79,7 @@ const NodeTitle = ({ nodeId, title }: Props) => {
fontSize="sm"
sx={{
p: 0,
fontWeight: 'initial',
_focusVisible: {
p: 0,
boxShadow: 'none',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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