From e629aa6b739470c20c27dd85a930d4719970e7e2 Mon Sep 17 00:00:00 2001 From: alberto-abarzua Date: Mon, 6 Nov 2023 15:14:38 -0300 Subject: [PATCH] feat: actions, actions set, collapsable actionset --- frontend/package-lock.json | 62 +++++++++ frontend/package.json | 1 + .../components/actions/ActionContainer.jsx | 55 +++++++- frontend/src/components/actions/ActionSet.jsx | 57 ++++++-- .../src/components/actions/ActionsPanel.jsx | 55 ++++---- .../src/components/actions/BaseAction.jsx | 32 +++-- .../src/components/actions/MoveAction.jsx | 33 +++-- .../src/components/actions/SleepAction.jsx | 19 ++- .../src/components/actions/ToolAction.jsx | 24 ++-- frontend/src/components/actions/ToolBar.jsx | 91 +++---------- .../src/components/actions/ToolBarElement.jsx | 67 ++++++++++ frontend/src/components/ui/accordion.jsx | 45 +++++++ frontend/src/redux/ActionListSlice.js | 123 +++++++++++++----- frontend/src/utils/actions.jsx | 8 +- frontend/src/utils/byIdContext.js | 5 + frontend/src/utils/dragndrop.js | 41 +++--- 16 files changed, 505 insertions(+), 213 deletions(-) create mode 100644 frontend/src/components/actions/ToolBarElement.jsx create mode 100644 frontend/src/components/ui/accordion.jsx create mode 100644 frontend/src/utils/byIdContext.js diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 176f8d71..2f0ae148 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,6 +11,7 @@ "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.14.7", + "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-slot": "^1.0.2", @@ -1092,6 +1093,37 @@ "@babel/runtime": "^7.13.10" } }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.1.2.tgz", + "integrity": "sha512-fDG7jcoNKVjSK6yfmuAs0EnPDro0WMXIhMtXdTBWqEioVW206ku+4Lw07e+13lUkFkpoEQ2PdeMIAGpdqEAmDg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collapsible": "1.0.3", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.0.3", "license": "MIT", @@ -1114,6 +1146,36 @@ } } }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.0.3.tgz", + "integrity": "sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.0.3", "license": "MIT", diff --git a/frontend/package.json b/frontend/package.json index 3a72d48f..151a8580 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.14.7", + "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-slot": "^1.0.2", diff --git a/frontend/src/components/actions/ActionContainer.jsx b/frontend/src/components/actions/ActionContainer.jsx index 26236fa1..ea4c1f47 100644 --- a/frontend/src/components/actions/ActionContainer.jsx +++ b/frontend/src/components/actions/ActionContainer.jsx @@ -1,12 +1,57 @@ +import { actionListActions } from '@/redux/ActionListSlice'; import { renderAction } from '@/utils/actions'; +import byIdContext from '@/utils/byIdContext'; +import { ItemTypes } from '@/utils/ItemTypes'; +import AddIcon from '@mui/icons-material/Add'; import PropTypes from 'prop-types'; +import { useContext, useState } from 'react'; +import { useDrop } from 'react-dnd'; +import { useDispatch } from 'react-redux'; const ActionContainer = ({ actionList }) => { - return ( -
- {actionList && actionList.map(action => renderAction(action))} -
- ); + const dispatch = useDispatch(); + const byId = useContext(byIdContext); + const [isOver, setIsOver] = useState(false); + + actionList = actionList.map(action => byId[action.id]); + const [, drop] = useDrop({ + accept: ItemTypes.ACTION, + collect(monitor) { + setIsOver(monitor.isOver()); + return { + handlerId: monitor.getHandlerId(), + }; + }, + drop(item) { + dispatch( + actionListActions.pushActionToValue({ + actionId: null, + actionToAddId: item.id, + type: item.type, + value: item.value, + }) + ); + }, + }); + + const dropAreaStyles = isOver ? 'bg-blue-300 ' : ''; + + if (actionList.length > 0) { + return ( +
+ {actionList && actionList.map(action => renderAction(action))} +
+ ); + } else { + return ( +
+ +
+ ); + } }; ActionContainer.propTypes = { diff --git a/frontend/src/components/actions/ActionSet.jsx b/frontend/src/components/actions/ActionSet.jsx index 6775a22e..d2ce9b36 100644 --- a/frontend/src/components/actions/ActionSet.jsx +++ b/frontend/src/components/actions/ActionSet.jsx @@ -1,51 +1,76 @@ import ActionContainer from '@/components/actions/ActionContainer'; import BaseAction from '@/components/actions/BaseAction'; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from '@/components/ui/accordion'; import { actionListActions } from '@/redux/ActionListSlice'; import { ItemTypes } from '@/utils/ItemTypes'; +import AddIcon from '@mui/icons-material/Add'; import DashboardCustomizeIcon from '@mui/icons-material/DashboardCustomize'; import PropTypes from 'prop-types'; +import { useState } from 'react'; import { useDrop } from 'react-dnd'; -import { useSelector, useDispatch } from 'react-redux'; -const ActionSet = ({ ...props }) => { +import { useDispatch } from 'react-redux'; + +const ActionSet = ({ action, ...props }) => { const dispatch = useDispatch(); - const action = useSelector(state => state.actionList.byId[props.id]); + const id = action.id; + const [isOver, setIsOver] = useState(false); const actionList = action.value; const [, drop] = useDrop({ accept: ItemTypes.ACTION, collect(monitor) { + setIsOver(monitor.isOver()); return { handlerId: monitor.getHandlerId(), }; }, drop(item) { - if (item.id === props.id) { + if (item.id === id) { return; } - dispatch( - actionListActions.pushActionToValue({ actionId: props.id, actionToAddId: item.id }) - ); + dispatch(actionListActions.pushActionToValue({ actionId: id, actionToAddId: item.id })); }, }); + const dropAreaStyles = isOver ? 'bg-blue-300 ' : ''; + const body = actionList.length === 0 ? ( -
Drag here
+
+ +
) : ( - +
+ + + Sub Actions + + + + + +
); - return ( } + action={action} {...props} > -
-
-
+
+
+
{body}
@@ -55,7 +80,11 @@ const ActionSet = ({ ...props }) => { }; ActionSet.propTypes = { - id: PropTypes.number.isRequired, + action: PropTypes.shape({ + type: PropTypes.string.isRequired, + id: PropTypes.number.isRequired, + value: PropTypes.arrayOf(PropTypes.any).isRequired, + }).isRequired, }; export default ActionSet; diff --git a/frontend/src/components/actions/ActionsPanel.jsx b/frontend/src/components/actions/ActionsPanel.jsx index 43f9a555..61fb22c2 100644 --- a/frontend/src/components/actions/ActionsPanel.jsx +++ b/frontend/src/components/actions/ActionsPanel.jsx @@ -2,7 +2,8 @@ import ActionContainer from '@/components/actions/ActionContainer'; import ToolBar from '@/components/actions/ToolBar'; import { Button } from '@/components/ui/button'; import { actionListActions } from '@/redux/ActionListSlice'; -import { ActionTypes, runAction } from '@/utils/actions'; +import { runAction } from '@/utils/actions'; +import byIdContext from '@/utils/byIdContext'; import ErrorIcon from '@mui/icons-material/Error'; import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import StopIcon from '@mui/icons-material/Stop'; @@ -11,10 +12,11 @@ import { useSelector, useDispatch } from 'react-redux'; const ActionPanel = () => { const dispatch = useDispatch(); - const actionList = useSelector(state => state.actionList.actions); - const byId = useSelector(state => state.actionList.byId); - console.log('byId', byId); - console.log('actionList', actionList); + + const actionSlice = useSelector(state => state.actionList); + + const actionList = actionSlice.actions; + const byId = actionSlice.byId; const [running, setRunning] = useState(false); const runningRef = useRef(false); @@ -28,21 +30,16 @@ const ActionPanel = () => { const runActions = async () => { for (let action of actionList) { if (!runningRef.current) return; + + action = byId[action.id]; dispatch(actionListActions.setRunningStatus({ actionId: action.id, running: true })); - console.log('running action', action); - if (action.type === ActionTypes.ACTIONSET) { - action = byId[action.id]; - } await runAction(action, dispatch); - console.log('finished running action', action); dispatch(actionListActions.setRunningStatus({ actionId: action.id, running: false })); } - console.log('finished running all actions'); setRunning(false); }; - console.log('rendering ActionPanel'); const handleClickPlayStop = () => { setRunning(prev => !prev); @@ -79,23 +76,25 @@ const ActionPanel = () => { } return ( -
- -
- -
-
- + +
+ +
+ +
+
+ +
-
+ ); }; diff --git a/frontend/src/components/actions/BaseAction.jsx b/frontend/src/components/actions/BaseAction.jsx index dbc6ab1d..fc7f29e1 100644 --- a/frontend/src/components/actions/BaseAction.jsx +++ b/frontend/src/components/actions/BaseAction.jsx @@ -9,11 +9,11 @@ import ErrorIcon from '@mui/icons-material/Error'; import PropTypes from 'prop-types'; import { useRef } from 'react'; import { useDrag, useDrop } from 'react-dnd'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; -const BaseAction = ({ icon, children, className, id, ...props }) => { +const BaseAction = ({ icon, children, className, action, ...props }) => { + const id = action.id; const dispatch = useDispatch(); - const action = useSelector(state => state.actionList.byId[id]); const running = action.running; const valid = action.valid; @@ -29,15 +29,22 @@ const BaseAction = ({ icon, children, className, id, ...props }) => { }, hover(item, monitor) { const dragPos = dragLocation(ref, monitor); - if (id === item.id || item.id == action.parentId || dragPos === PositionTypes.OUT) + + if ( + id == item.id || + item.id == action.parentId || + dragPos === PositionTypes.OUT || + dragPos === PositionTypes.MIDDLE + ) return; - const before = dragPos === PositionTypes.TOP; dispatch( actionListActions.moveAction({ refActionId: item.id, targetActionId: id, - before: before, + before: dragPos === PositionTypes.TOP, + type: item.type, + value: item.value, }) ); }, @@ -74,15 +81,12 @@ const BaseAction = ({ icon, children, className, id, ...props }) => { ); - console.log('isDragging action ', id % 1000, isDragging); - if (!isDragging) { return (
{icon}
@@ -100,13 +104,12 @@ const BaseAction = ({ icon, children, className, id, ...props }) => { >
-

{id % 1000}

); } else { return (
{ - const dispatch = useDispatch(); - const id = props.id; +const MoveAction = ({ action, ...props }) => { + const id = action.id; - const action = useSelector(state => state.actionList.byId[id]); + const dispatch = useDispatch(); const [currentPose, setCurrentPose] = useState(action.value); @@ -44,11 +43,12 @@ const MoveAction = ({ ...props }) => { return ( } + action={action} className="h-30 bg-action-move" {...props} > - <> -
+
+
{
-
+
{ />
- +
); }; MoveAction.propTypes = { - id: PropTypes.number.isRequired, + action: PropTypes.shape({ + id: PropTypes.number.isRequired, + parentId: PropTypes.number, + running: PropTypes.bool.isRequired, + valid: PropTypes.bool.isRequired, + value: PropTypes.shape({ + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired, + z: PropTypes.number.isRequired, + roll: PropTypes.number.isRequired, + pitch: PropTypes.number.isRequired, + yaw: PropTypes.number.isRequired, + }).isRequired, + }).isRequired, }; export default MoveAction; diff --git a/frontend/src/components/actions/SleepAction.jsx b/frontend/src/components/actions/SleepAction.jsx index 3b613c9b..8b0dc2ae 100644 --- a/frontend/src/components/actions/SleepAction.jsx +++ b/frontend/src/components/actions/SleepAction.jsx @@ -4,21 +4,22 @@ import { actionListActions } from '@/redux/ActionListSlice'; import BedtimeIcon from '@mui/icons-material/Bedtime'; import PropTypes from 'prop-types'; import { useState, useEffect } from 'react'; -import { useSelector, useDispatch } from 'react-redux'; +import { useDispatch } from 'react-redux'; -const SleepAction = ({ ...props }) => { +const SleepAction = ({ action, ...props }) => { + const id = action.id; const dispatch = useDispatch(); - const action = useSelector(state => state.actionList.byId[props.id]); const [sleepValue, setsleepValue] = useState(action.value); useEffect(() => { - dispatch(actionListActions.setActionValue({ actionId: props.id, value: sleepValue })); - }, [sleepValue, dispatch, props.id]); + dispatch(actionListActions.setActionValue({ actionId: id, value: sleepValue })); + }, [sleepValue, dispatch, id]); return ( } + action={action} {...props} >
@@ -40,6 +41,12 @@ const SleepAction = ({ ...props }) => { }; SleepAction.propTypes = { - id: PropTypes.number.isRequired, + action: PropTypes.shape({ + type: PropTypes.string.isRequired, + id: PropTypes.number.isRequired, + value: PropTypes.shape({ + duration: PropTypes.number.isRequired, + }).isRequired, + }).isRequired, }; export default SleepAction; diff --git a/frontend/src/components/actions/ToolAction.jsx b/frontend/src/components/actions/ToolAction.jsx index d77b4a7a..2340d91b 100644 --- a/frontend/src/components/actions/ToolAction.jsx +++ b/frontend/src/components/actions/ToolAction.jsx @@ -1,25 +1,27 @@ -import BaseAction from './BaseAction'; -import TextVariable from '../general/text/TextVariable'; +import BaseAction from '@/components/actions/BaseAction'; +import TextVariable from '@/components/general/text/TextVariable'; import { actionListActions } from '@/redux/ActionListSlice'; import BuildIcon from '@mui/icons-material/Build'; import PropTypes from 'prop-types'; import { useState, useEffect } from 'react'; -import { useSelector, useDispatch } from 'react-redux'; +import { useDispatch } from 'react-redux'; + +const ToolAction = ({ action, ...props }) => { + const id = action.id; -const ToolAction = ({ ...props }) => { const dispatch = useDispatch(); - const action = useSelector(state => state.actionList.byId[props.id]); const [toolValue, setToolValue] = useState(action.value); useEffect(() => { - dispatch(actionListActions.setActionValue({ actionId: props.id, value: toolValue })); - }, [toolValue, dispatch, props.id]); + dispatch(actionListActions.setActionValue({ actionId: id, value: toolValue })); + }, [toolValue, dispatch, id]); return ( } + action={action} {...props} >
@@ -41,7 +43,13 @@ const ToolAction = ({ ...props }) => { }; ToolAction.propTypes = { - id: PropTypes.number.isRequired, + action: PropTypes.shape({ + type: PropTypes.string.isRequired, + id: PropTypes.number.isRequired, + value: PropTypes.shape({ + toolValue: PropTypes.number.isRequired, + }).isRequired, + }).isRequired, }; export default ToolAction; diff --git a/frontend/src/components/actions/ToolBar.jsx b/frontend/src/components/actions/ToolBar.jsx index aea6fc09..8ed98d11 100644 --- a/frontend/src/components/actions/ToolBar.jsx +++ b/frontend/src/components/actions/ToolBar.jsx @@ -1,3 +1,4 @@ +import ToolBarElement from './ToolBarElement'; import { DropdownMenu, DropdownMenuContent, @@ -6,7 +7,6 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { actionListActions } from '@/redux/ActionListSlice'; import { ActionTypes } from '@/utils/actions'; import BedtimeIcon from '@mui/icons-material/Bedtime'; @@ -22,83 +22,53 @@ const ToolBar = () => { const currentPose = useSelector(state => state.armPose); - const addMoveAction = () => { - let value = { - x: currentPose.x, - y: currentPose.y, - z: currentPose.z, - roll: currentPose.roll, - pitch: currentPose.pitch, - yaw: currentPose.yaw, - }; - dispatch( - actionListActions.addAction({ type: ActionTypes.MOVE, value: value, parentId: null }) - ); - }; - - const addSleepAction = () => { - let value = { - duration: 2, - }; - dispatch( - actionListActions.addAction({ type: ActionTypes.SLEEP, value: value, parentId: null }) - ); - }; - - const addToolAction = () => { - let value = { - toolValue: currentPose.toolValue, - }; - dispatch( - actionListActions.addAction({ type: ActionTypes.TOOL, value: value, parentId: null }) - ); - }; - - const addActionSet = () => { - let value = []; - dispatch( - actionListActions.addAction({ - type: ActionTypes.ACTIONSET, - value: value, - parentId: null, - }) - ); - }; - const clearActionList = () => { dispatch(actionListActions.clearActionList()); }; + const moveValue = { + x: currentPose.x, + y: currentPose.y, + z: currentPose.z, + roll: currentPose.roll, + pitch: currentPose.pitch, + yaw: currentPose.yaw, + }; + const elements = [ { name: 'Move', icon: GamesIcon, - onClick: addMoveAction, bgColor: 'bg-action-move', + type: ActionTypes.MOVE, + value: moveValue, hoverColor: 'hover:bg-action-move-hover', - helpText: 'Move Action: Move to a pose', + helpText: 'Move Addction: Move to a pose', }, { name: 'Sleep', icon: BedtimeIcon, - onClick: addSleepAction, bgColor: 'bg-action-sleep', + type: ActionTypes.SLEEP, + value: { duration: 2 }, hoverColor: 'hover:bg-action-sleep-hover', helpText: 'Sleep Action: Pause movement', }, { name: 'Tool', icon: BuildIcon, - onClick: addToolAction, bgColor: 'bg-action-tool', + type: ActionTypes.TOOL, + value: { toolValue: 0 }, hoverColor: 'hover:bg-action-tool-hover', helpText: 'Tool Action: Change tool value', }, { name: 'Custom', icon: DashboardCustomizeIcon, - onClick: addActionSet, bgColor: 'bg-action-set', + type: ActionTypes.ACTIONSET, + value: [], hoverColor: 'hover:bg-action-set-hover', helpText: 'Action Set: Set of actions', }, @@ -106,27 +76,8 @@ const ToolBar = () => { return (
- {elements.map((action, index) => ( -
- - - -
-
- -
-
-
- - -

{action.helpText}

-
-
-
-
+ {elements.map((element, index) => ( + ))} diff --git a/frontend/src/components/actions/ToolBarElement.jsx b/frontend/src/components/actions/ToolBarElement.jsx new file mode 100644 index 00000000..3665d952 --- /dev/null +++ b/frontend/src/components/actions/ToolBarElement.jsx @@ -0,0 +1,67 @@ +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; +import { actionListActions } from '@/redux/ActionListSlice'; +import { ItemTypes } from '@/utils/ItemTypes'; +import PropTypes from 'prop-types'; +import { useDrag } from 'react-dnd'; +import { useDispatch } from 'react-redux'; + +const ToolBarElement = ({ element }) => { + const dispatch = useDispatch(); + + const [, dragRef] = useDrag({ + type: ItemTypes.ACTION, + item: () => { + return { id: Date.now(), type: element.type, value: element.value }; + }, + collect: monitor => ({ + isDragging: monitor.isDragging(), + }), + }); + + const addToActionList = () => { + dispatch( + actionListActions.addAction({ + type: element.type, + value: element.value, + parentId: null, + }) + ); + }; + + return ( +
+ + + +
+
+ +
+
+
+ + +

{element.helpText}

+
+
+
+
+ ); +}; + +ToolBarElement.propTypes = { + element: PropTypes.shape({ + name: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, + icon: PropTypes.object.isRequired, + bgColor: PropTypes.string.isRequired, + value: PropTypes.any.isRequired, + hoverColor: PropTypes.string.isRequired, + helpText: PropTypes.string.isRequired, + }).isRequired, +}; +export default ToolBarElement; diff --git a/frontend/src/components/ui/accordion.jsx b/frontend/src/components/ui/accordion.jsx new file mode 100644 index 00000000..6633fe60 --- /dev/null +++ b/frontend/src/components/ui/accordion.jsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import * as AccordionPrimitive from '@radix-ui/react-accordion'; +import { ChevronDown } from 'lucide-react'; + +import { cn } from '@/lib/utils'; + +const Accordion = AccordionPrimitive.Root; + +const AccordionItem = React.forwardRef(({ className, ...props }, ref) => ( + +)); +AccordionItem.displayName = 'AccordionItem'; + +const AccordionTrigger = React.forwardRef(({ className, children, ...props }, ref) => ( + + svg]:rotate-180', + className + )} + {...props} + > + {children} + + + +)); +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; + +const AccordionContent = React.forwardRef(({ className, children, ...props }, ref) => ( + +
{children}
+
+)); +AccordionContent.displayName = AccordionPrimitive.Content.displayName; + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/frontend/src/redux/ActionListSlice.js b/frontend/src/redux/ActionListSlice.js index 8ba8d0ff..aac2fef6 100644 --- a/frontend/src/redux/ActionListSlice.js +++ b/frontend/src/redux/ActionListSlice.js @@ -29,21 +29,32 @@ const actionListSlice = createSlice({ initialState, reducers: { moveAction: (state, action) => { - const { refActionId, targetActionId, before } = action.payload; - console.log( - 'Moving action ', - refActionId % 1000, - ' to ', - targetActionId % 1000, - ' before ', - before - ); + let { refActionId, targetActionId, before } = action.payload; + // refaction is the action being moved // targetAction is the action that refAction is being moved to - const refAction = state.byId[refActionId]; + let refAction = state.byId[refActionId]; const targetAction = state.byId[targetActionId]; + if (!refAction) { + const { type, value } = action.payload; + const newAction = { + id: refActionId, + type, + parentId: targetAction.parentId, + value, + valid: true, + running: false, + }; + state.byId[newAction.id] = newAction; + refAction = newAction; + refActionId = newAction.id; + //updte the parent list + const parentList = getParentList(state, refAction); + parentList.push(refAction); + } + const refActionList = getParentList(state, refAction); const targetActionList = getParentList(state, targetAction); @@ -73,7 +84,7 @@ const actionListSlice = createSlice({ addAction: (state, action) => { const { type, parentId, value } = action.payload; - // if parentId is undefined + const newAction = { id: Date.now(), type, @@ -82,6 +93,7 @@ const actionListSlice = createSlice({ valid: true, running: false, }; + state.byId[newAction.id] = newAction; if (parentId !== null) { @@ -93,9 +105,12 @@ const actionListSlice = createSlice({ deleteAction: (state, action) => { const { actionId } = action.payload; + const actionToDelete = state.byId[actionId]; const actionList = getParentList(state, actionToDelete); + const actionIndex = actionList.findIndex(action => action.id === actionId); + if (actionIndex !== -1) { actionList.splice(actionIndex, 1); } @@ -105,16 +120,35 @@ const actionListSlice = createSlice({ duplicateAction: (state, action) => { const { actionId } = action.payload; const actionToDuplicate = state.byId[actionId]; - const actionList = getParentList(state, actionToDuplicate); - const actionIndex = actionList.findIndex(action => action.id === actionId); - const newAction = { - ...actionToDuplicate, - id: Date.now(), + + // Deep clone the action + let newAction = JSON.parse(JSON.stringify(actionToDuplicate)); + + // Helper function to generate a unique ID + + // Recursively update id, parentId, and state.byId + let id_offset = 0; + const updateHelper = (actionToUpdate, newParentId) => { + const newId = Date.now() + id_offset; + id_offset++; + actionToUpdate.id = newId; + actionToUpdate.parentId = newParentId; + state.byId[newId] = actionToUpdate; + if (actionToUpdate.type === ActionTypes.ACTIONSET) { + console.log(actionToUpdate); + for (let subactionToUpdate of actionToUpdate.value) { + updateHelper(subactionToUpdate, newId); + } + } }; + + updateHelper(newAction, actionToDuplicate.parentId); + console.log(newAction); + console.log(actionToDuplicate); + actionList.splice(actionIndex + 1, 0, newAction); - state.byId[newAction.id] = newAction; }, setValidStatus: (state, action) => { @@ -132,25 +166,48 @@ const actionListSlice = createSlice({ state.byId[actionId].value = value; }, pushActionToValue: (state, action) => { - const { actionId, actionToAddId } = action.payload; - // make sure the action type is actionset - const targetAction = state.byId[actionId]; - if (targetAction.type !== ActionTypes.ACTIONSET) { - console.error('pushActionToValue: action type is not actionset'); - return; - } - const ActionToAdd = state.byId[actionToAddId]; + let { actionId, actionToAddId } = action.payload; - targetAction.value.push(ActionToAdd); - // delete actionToAdd from its old location - const actionList = getParentList(state, ActionToAdd); - const actionIndex = actionList.findIndex(action => action.id === actionToAddId); - if (actionIndex !== -1) { - actionList.splice(actionIndex, 1); + let targetList = state.actions; + let targetActionId = null; + if (actionId) { + targetList = state.byId[actionId].value; + targetActionId = actionId; } - // update parentId - ActionToAdd.parentId = targetAction.id; + let actionToAdd = state.byId[actionToAddId]; + + // check if actionToAdd exits + + if (!actionToAdd) { + const { type, value } = action.payload; + const newAction = { + id: actionToAddId, + type, + parentId: targetActionId, + value, + valid: true, + running: false, + }; + + state.byId[newAction.id] = newAction; + + actionToAdd = newAction; + actionToAddId = newAction.id; + targetList.push(actionToAdd); + } else { + targetList.push(actionToAdd); + + // delete actionToAdd from its old location + const actionList = getParentList(state, actionToAdd); + const actionIndex = actionList.findIndex(action => action.id === actionToAddId); + if (actionIndex !== -1) { + actionList.splice(actionIndex, 1); + } + // update parentId + + actionToAdd.parentId = targetActionId; + } }, clearActionList: state => { diff --git a/frontend/src/utils/actions.jsx b/frontend/src/utils/actions.jsx index 37f288a9..b08258dd 100644 --- a/frontend/src/utils/actions.jsx +++ b/frontend/src/utils/actions.jsx @@ -39,11 +39,9 @@ const actionHandlers = { await api.post('/move/tool/move/', target); }, [ActionTypes.ACTIONSET]: async (action, dispatch) => { - console.log('running actionset', action); for (let subAction of action.value) { - console.log('running subaction', subAction); dispatch(actionListActions.setRunningStatus({ actionId: subAction.id, running: true })); - await runAction(subAction); + await runAction(subAction, dispatch); dispatch( actionListActions.setRunningStatus({ actionId: subAction.id, running: false }) ); @@ -70,7 +68,9 @@ const renderAction = action => { const Component = components[action.type]; - return Component ? : null; + return Component ? ( + + ) : null; }; export { ActionTypes, runAction, renderAction }; diff --git a/frontend/src/utils/byIdContext.js b/frontend/src/utils/byIdContext.js new file mode 100644 index 00000000..1fe446a4 --- /dev/null +++ b/frontend/src/utils/byIdContext.js @@ -0,0 +1,5 @@ +import { createContext } from 'react'; + +const byIdContext = createContext(); + +export default byIdContext; diff --git a/frontend/src/utils/dragndrop.js b/frontend/src/utils/dragndrop.js index b0ff5060..5f76cc62 100644 --- a/frontend/src/utils/dragndrop.js +++ b/frontend/src/utils/dragndrop.js @@ -2,6 +2,7 @@ const PositionTypes = { TOP: 'top', BOTTOM: 'bottom', OUT: 'out', + MIDDLE: 'middle', }; const dragLocation = (ref, monitor) => { @@ -10,37 +11,31 @@ const dragLocation = (ref, monitor) => { } const hoverBoundingRect = ref.current.getBoundingClientRect(); + const toleranceDistance = 20; - // Get vertical middle of the element - const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2; - - // Determine mouse position const clientOffset = monitor.getClientOffset(); + const top = hoverBoundingRect.top; + const bottom = hoverBoundingRect.bottom; + const currentY = clientOffset.y; - // If getClientOffset is null, consider it as OUT - if (!clientOffset) { - return PositionTypes.OUT; - } + const distFromTop = Math.abs(currentY - top); + const distFromBottom = Math.abs(currentY - bottom); + const innerTopBoundary = top + toleranceDistance; + const innerBottomBoundary = bottom - toleranceDistance; - const hoverClientY = clientOffset.y - hoverBoundingRect.top; - const hoverClientX = clientOffset.x - hoverBoundingRect.left; - - // Check if the mouse is outside the element on either axis - if ( - hoverClientX < 0 || - hoverClientX > hoverBoundingRect.right - hoverBoundingRect.left || - hoverClientY < 0 || - hoverClientY > hoverBoundingRect.bottom - hoverBoundingRect.top - ) { - return PositionTypes.OUT; + if (distFromTop <= toleranceDistance) { + return PositionTypes.TOP; } - // Corrected logic for top and bottom checks - if (hoverClientY < hoverMiddleY) { - return PositionTypes.TOP; - } else { + if (distFromBottom <= toleranceDistance) { return PositionTypes.BOTTOM; } + + if (currentY > innerTopBoundary && currentY < innerBottomBoundary) { + return PositionTypes.MIDDLE; + } + + return PositionTypes.OUT; }; export { PositionTypes, dragLocation };