feat(ui): add selection mode toggle

This commit is contained in:
psychedelicious 2023-08-22 22:19:52 +10:00
parent 5cf9b75d77
commit 6d10e40c9b
4 changed files with 64 additions and 7 deletions

View File

@ -1,5 +1,8 @@
import { useToken } from '@chakra-ui/react'; import { useToken } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { contextMenusClosed } from 'features/ui/store/uiSlice'; import { contextMenusClosed } from 'features/ui/store/uiSlice';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
@ -61,14 +64,24 @@ const nodeTypes = {
// TODO: can we support reactflow? if not, we could style the attribution so it matches the app // TODO: can we support reactflow? if not, we could style the attribution so it matches the app
const proOptions: ProOptions = { hideAttribution: true }; const proOptions: ProOptions = { hideAttribution: true };
const selector = createSelector(
stateSelector,
({ nodes }) => {
const { shouldSnapToGrid, selectionMode } = nodes;
return {
shouldSnapToGrid,
selectionMode,
};
},
defaultSelectorOptions
);
export const Flow = () => { export const Flow = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const nodes = useAppSelector((state) => state.nodes.nodes); const nodes = useAppSelector((state) => state.nodes.nodes);
const edges = useAppSelector((state) => state.nodes.edges); const edges = useAppSelector((state) => state.nodes.edges);
const viewport = useAppSelector((state) => state.nodes.viewport); const viewport = useAppSelector((state) => state.nodes.viewport);
const shouldSnapToGrid = useAppSelector( const { shouldSnapToGrid, selectionMode } = useAppSelector(selector);
(state) => state.nodes.shouldSnapToGrid
);
const isValidConnection = useIsValidConnection(); const isValidConnection = useIsValidConnection();
@ -181,6 +194,7 @@ export const Flow = () => {
style={{ borderRadius }} style={{ borderRadius }}
onPaneClick={handlePaneClick} onPaneClick={handlePaneClick}
deleteKeyCode={DELETE_KEYS} deleteKeyCode={DELETE_KEYS}
selectionMode={selectionMode}
> >
<TopLeftPanel /> <TopLeftPanel />
<TopCenterPanel /> <TopCenterPanel />

View File

@ -1,6 +1,7 @@
import { import {
Divider, Divider,
Flex, Flex,
FormLabelProps,
Heading, Heading,
Modal, Modal,
ModalBody, ModalBody,
@ -13,17 +14,19 @@ import {
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store'; import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIIconButton from 'common/components/IAIIconButton'; import IAIIconButton from 'common/components/IAIIconButton';
import IAISwitch from 'common/components/IAISwitch'; import IAISwitch from 'common/components/IAISwitch';
import { ChangeEvent, memo, useCallback } from 'react';
import { FaCog } from 'react-icons/fa';
import { import {
selectionModeChanged,
shouldAnimateEdgesChanged, shouldAnimateEdgesChanged,
shouldColorEdgesChanged, shouldColorEdgesChanged,
shouldSnapToGridChanged, shouldSnapToGridChanged,
shouldValidateGraphChanged, shouldValidateGraphChanged,
} from 'features/nodes/store/nodesSlice'; } from 'features/nodes/store/nodesSlice';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { ChangeEvent, memo, useCallback } from 'react';
import { FaCog } from 'react-icons/fa';
import { SelectionMode } from 'reactflow';
const selector = createSelector( const selector = createSelector(
stateSelector, stateSelector,
@ -33,12 +36,14 @@ const selector = createSelector(
shouldValidateGraph, shouldValidateGraph,
shouldSnapToGrid, shouldSnapToGrid,
shouldColorEdges, shouldColorEdges,
selectionMode,
} = nodes; } = nodes;
return { return {
shouldAnimateEdges, shouldAnimateEdges,
shouldValidateGraph, shouldValidateGraph,
shouldSnapToGrid, shouldSnapToGrid,
shouldColorEdges, shouldColorEdges,
selectionModeIsChecked: selectionMode === SelectionMode.Full,
}; };
}, },
defaultSelectorOptions defaultSelectorOptions
@ -52,6 +57,7 @@ const NodeEditorSettings = () => {
shouldValidateGraph, shouldValidateGraph,
shouldSnapToGrid, shouldSnapToGrid,
shouldColorEdges, shouldColorEdges,
selectionModeIsChecked,
} = useAppSelector(selector); } = useAppSelector(selector);
const handleChangeShouldValidate = useCallback( const handleChangeShouldValidate = useCallback(
@ -82,6 +88,13 @@ const NodeEditorSettings = () => {
[dispatch] [dispatch]
); );
const handleChangeSelectionMode = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
dispatch(selectionModeChanged(e.target.checked));
},
[dispatch]
);
return ( return (
<> <>
<IAIIconButton <IAIIconButton
@ -105,6 +118,7 @@ const NodeEditorSettings = () => {
> >
<Heading size="sm">General</Heading> <Heading size="sm">General</Heading>
<IAISwitch <IAISwitch
formLabelProps={formLabelProps}
onChange={handleChangeShouldAnimate} onChange={handleChangeShouldAnimate}
isChecked={shouldAnimateEdges} isChecked={shouldAnimateEdges}
label="Animated Edges" label="Animated Edges"
@ -112,6 +126,7 @@ const NodeEditorSettings = () => {
/> />
<Divider /> <Divider />
<IAISwitch <IAISwitch
formLabelProps={formLabelProps}
isChecked={shouldSnapToGrid} isChecked={shouldSnapToGrid}
onChange={handleChangeShouldSnap} onChange={handleChangeShouldSnap}
label="Snap to Grid" label="Snap to Grid"
@ -119,15 +134,24 @@ const NodeEditorSettings = () => {
/> />
<Divider /> <Divider />
<IAISwitch <IAISwitch
formLabelProps={formLabelProps}
isChecked={shouldColorEdges} isChecked={shouldColorEdges}
onChange={handleChangeShouldColor} onChange={handleChangeShouldColor}
label="Color-Code Edges" label="Color-Code Edges"
helperText="Color-code edges according to their connected fields" helperText="Color-code edges according to their connected fields"
/> />
<IAISwitch
formLabelProps={formLabelProps}
isChecked={selectionModeIsChecked}
onChange={handleChangeSelectionMode}
label="Fully Contain Nodes to Select"
helperText="Nodes must be fully inside the selection box to be selected"
/>
<Heading size="sm" pt={4}> <Heading size="sm" pt={4}>
Advanced Advanced
</Heading> </Heading>
<IAISwitch <IAISwitch
formLabelProps={formLabelProps}
isChecked={shouldValidateGraph} isChecked={shouldValidateGraph}
onChange={handleChangeShouldValidate} onChange={handleChangeShouldValidate}
label="Validate Connections and Graph" label="Validate Connections and Graph"
@ -142,3 +166,7 @@ const NodeEditorSettings = () => {
}; };
export default memo(NodeEditorSettings); export default memo(NodeEditorSettings);
const formLabelProps: FormLabelProps = {
fontWeight: 600,
};

View File

@ -14,6 +14,7 @@ import {
Node, Node,
NodeChange, NodeChange,
OnConnectStartParams, OnConnectStartParams,
SelectionMode,
Viewport, Viewport,
} from 'reactflow'; } from 'reactflow';
import { receivedOpenAPISchema } from 'services/api/thunks/schema'; import { receivedOpenAPISchema } from 'services/api/thunks/schema';
@ -103,6 +104,7 @@ export const initialNodesState: NodesState = {
mouseOverField: null, mouseOverField: null,
nodesToCopy: [], nodesToCopy: [],
edgesToCopy: [], edgesToCopy: [],
selectionMode: SelectionMode.Partial,
}; };
type FieldValueAction<T extends InputFieldValue> = PayloadAction<{ type FieldValueAction<T extends InputFieldValue> = PayloadAction<{
@ -721,6 +723,11 @@ const nodesSlice = createSlice({
addNodePopoverToggled: (state) => { addNodePopoverToggled: (state) => {
state.isAddNodePopoverOpen = !state.isAddNodePopoverOpen; state.isAddNodePopoverOpen = !state.isAddNodePopoverOpen;
}, },
selectionModeChanged: (state, action: PayloadAction<boolean>) => {
state.selectionMode = action.payload
? SelectionMode.Full
: SelectionMode.Partial;
},
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addCase(receivedOpenAPISchema.pending, (state) => { builder.addCase(receivedOpenAPISchema.pending, (state) => {
@ -832,6 +839,7 @@ export const {
addNodePopoverOpened, addNodePopoverOpened,
addNodePopoverClosed, addNodePopoverClosed,
addNodePopoverToggled, addNodePopoverToggled,
selectionModeChanged,
} = nodesSlice.actions; } = nodesSlice.actions;
export default nodesSlice.reducer; export default nodesSlice.reducer;

View File

@ -1,4 +1,10 @@
import { Edge, Node, OnConnectStartParams, Viewport } from 'reactflow'; import {
Edge,
Node,
OnConnectStartParams,
SelectionMode,
Viewport,
} from 'reactflow';
import { import {
FieldIdentifier, FieldIdentifier,
FieldType, FieldType,
@ -32,4 +38,5 @@ export type NodesState = {
nodesToCopy: Node<NodeData>[]; nodesToCopy: Node<NodeData>[];
edgesToCopy: Edge<InvocationEdgeExtra>[]; edgesToCopy: Edge<InvocationEdgeExtra>[];
isAddNodePopoverOpen: boolean; isAddNodePopoverOpen: boolean;
selectionMode: SelectionMode;
}; };