diff --git a/client/src/pages/presentationEditor/components/RndComponent.tsx b/client/src/pages/presentationEditor/components/RndComponent.tsx index 8f2324c93a36d8e246043a4b5adeaec990f88dae..a134ee14c9ac268159b6ae0013350012e638dc65 100644 --- a/client/src/pages/presentationEditor/components/RndComponent.tsx +++ b/client/src/pages/presentationEditor/components/RndComponent.tsx @@ -1,14 +1,17 @@ -import { Card, IconButton, Tooltip } from '@material-ui/core' +import { Card, IconButton, Menu, MenuItem, Tooltip } from '@material-ui/core' import axios from 'axios' import React, { useEffect, useState } from 'react' import { Rnd } from 'react-rnd' +import { getEditorCompetition } from '../../../actions/editor' import { ComponentTypes } from '../../../enum/ComponentTypes' -import { useAppSelector } from '../../../hooks' +import { useAppDispatch, useAppSelector } from '../../../hooks' import { Component, ImageComponent, TextComponent } from '../../../interfaces/ApiModels' import { Position, Size } from '../../../interfaces/Components' +import { RemoveMenuItem } from '../../admin/styledComp' import ImageComponentDisplay from './ImageComponentDisplay' import { HoverContainer } from './styled' import TextComponentDisplay from './TextComponentDisplay' +//import NestedMenuItem from 'material-ui-nested-menu-item' type RndComponentProps = { component: Component @@ -17,6 +20,8 @@ type RndComponentProps = { scale: number } +const initialMenuState = { menuIsOpen: false, mouseX: null, mouseY: null, componentId: null } + const RndComponent = ({ component, width, height, scale }: RndComponentProps) => { const [hover, setHover] = useState(false) const [currentPos, setCurrentPos] = useState<Position>({ x: component.x, y: component.y }) @@ -27,6 +32,14 @@ const RndComponent = ({ component, width, height, scale }: RndComponentProps) => const typeName = useAppSelector( (state) => state.types.componentTypes.find((componentType) => componentType.id === component.type_id)?.name ) + const [menuState, setMenuState] = useState<{ + menuIsOpen: boolean + mouseX: null | number + mouseY: null | number + componentId: null | number + }>(initialMenuState) + const dispatch = useAppDispatch() + const handleUpdatePos = (pos: Position) => { axios.put(`/api/competitions/${competitionId}/slides/${slideId}/components/${component.id}`, { x: pos.x, @@ -49,6 +62,37 @@ const RndComponent = ({ component, width, height, scale }: RndComponentProps) => setCurrentPos({ x: currentPos.x, y: centerY }) handleUpdatePos({ x: currentPos.x, y: centerY }) } + const handleRightClick = (event: React.MouseEvent<HTMLDivElement>, componentId: number) => { + event.preventDefault() + setMenuState({ + menuIsOpen: true, + mouseX: event.clientX - 2, + mouseY: event.clientY - 4, + componentId: componentId, + }) + } + const handleCloseMenu = () => { + setMenuState(initialMenuState) + } + const handleDuplicateComponent = async (viewTypeId: number) => { + console.log('Duplicate') + await axios + .post( + `/api/competitions/${competitionId}/slides/${slideId}/components/${menuState.componentId}/copy/${viewTypeId}` + ) + .then(() => dispatch(getEditorCompetition(competitionId.toString()))) + .catch(console.log) + setMenuState(initialMenuState) + } + const handleRemoveComponent = async () => { + console.log('Remove') + await axios + .delete(`/api/competitions/${competitionId}/slides/${slideId}/components/${menuState.componentId}`) + .then(() => dispatch(getEditorCompetition(competitionId.toString()))) + .catch(console.log) + setMenuState(initialMenuState) + } + useEffect(() => { const downHandler = (ev: KeyboardEvent) => { if (ev.key === 'Shift') setShiftPressed(true) @@ -102,6 +146,8 @@ const RndComponent = ({ component, width, height, scale }: RndComponentProps) => lockAspectRatio={shiftPressed} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} + //Right click to open menu + onContextMenu={(event: React.MouseEvent<HTMLDivElement>) => handleRightClick(event, component.id)} //Multiply by scale to show components correctly for current screen size size={{ width: currentSize.w * scale, height: currentSize.h * scale }} position={{ x: currentPos.x * scale, y: currentPos.y * scale }} @@ -129,6 +175,23 @@ const RndComponent = ({ component, width, height, scale }: RndComponentProps) => </Card> )} {renderInnerComponent()} + <Menu + keepMounted + open={menuState.menuIsOpen} + onClose={handleCloseMenu} + anchorReference="anchorPosition" + anchorPosition={ + menuState.mouseY !== null && menuState.mouseX !== null + ? { top: menuState.mouseY, left: menuState.mouseX } + : undefined + } + > + {/* <NestedMenuItem label="Duplicera"> */} + <MenuItem onClick={() => handleDuplicateComponent(3)}>Duplicera till åskådarvy</MenuItem> + <MenuItem onClick={() => handleDuplicateComponent(1)}>Duplicera till deltagarvy</MenuItem> + {/* </NestedMenuItem> */} + <RemoveMenuItem onClick={handleRemoveComponent}>Ta bort</RemoveMenuItem> + </Menu> </Rnd> ) } diff --git a/server/app/apis/components.py b/server/app/apis/components.py index a1982763a5ec37b8bb02bb8e6a73e164355417a8..38227a46d90fe2afb5ea4a2fc7924412661c5259 100644 --- a/server/app/apis/components.py +++ b/server/app/apis/components.py @@ -54,6 +54,16 @@ class ComponentByID(Resource): return {}, codes.NO_CONTENT +@api.route("/<component_id>/copy/<view_type_id>") +@api.param("competition_id, slide_id, component_id, view_type_id") +class ComponentList(Resource): + @protect_route(allowed_roles=["*"]) + def post(self, competition_id, slide_id, component_id, view_type_id): + item_component = dbc.get.component(competition_id, slide_id, component_id) + item = dbc.copy.component(item_component, slide_id, view_type_id) + return item_response(schema.dump(item)) + + @api.route("") @api.param("competition_id, slide_id") class ComponentList(Resource): diff --git a/server/app/database/controller/copy.py b/server/app/database/controller/copy.py index 08d743472035034b3b45eda3bdc6553f26377f60..48dab2db79c33cb1ca819a59f0e934ab23dcc191 100644 --- a/server/app/database/controller/copy.py +++ b/server/app/database/controller/copy.py @@ -9,6 +9,7 @@ from app.database.types import ID_IMAGE_COMPONENT, ID_QUESTION_COMPONENT, ID_TEX def _alternative(item_old, question_id): """Internal function. Makes a copy of the provided question alternative""" + return add.question_alternative(item_old.text, item_old.value, question_id) @@ -39,6 +40,16 @@ def _component(item_component, item_slide_new): Internal function. Makes a copy of the provided component item to the specified slide. """ + + component(item_component, item_slide_new.id, item_component.view_type_id) + + +def component(item_component, slide_id_new, view_type_id): + """ + Makes a copy of the provided component item + to the specified slide and view_type. + """ + data = {} if item_component.type_id == ID_TEXT_COMPONENT: data["text"] = item_component.text @@ -46,10 +57,11 @@ def _component(item_component, item_slide_new): data["media_id"] = item_component.media_id elif item_component.type_id == ID_QUESTION_COMPONENT: data["question_id"] = item_component.question_id - add.component( + + return add.component( item_component.type_id, - item_slide_new.id, - item_component.view_type_id, + slide_id_new, + view_type_id, item_component.x, item_component.y, item_component.w, diff --git a/server/tests/test_app.py b/server/tests/test_app.py index 3deb6f295f330a5bda2cb9842de4fb06f6d152c7..2152a6d4c7236f57d099f50dccec3a090ce94c5d 100644 --- a/server/tests/test_app.py +++ b/server/tests/test_app.py @@ -112,7 +112,7 @@ def test_competition_api(client): assert response.status_code == codes.OK # Copies competition - for _ in range(10): + for _ in range(3): response, _ = post(client, f"/api/competitions/{competition_id}/copy", headers=headers) assert response.status_code == codes.OK @@ -330,6 +330,35 @@ def test_slide_api(client): assert response.status_code == codes.OK """ + # Get a specific component + CID = 2 + SID = 3 + COMID = 2 + response, c1 = get(client, f"/api/competitions/{CID}/slides/{SID}/components/{COMID}", headers=headers) + assert response.status_code == codes.OK + + # Copy the component to another view + view_type_id = 3 + response, c2 = post( + client, f"/api/competitions/{CID}/slides/{SID}/components/{COMID}/copy/{view_type_id}", headers=headers + ) + # Check that the components metch + assert response.status_code == codes.OK + assert c1 != c2 + assert c1["x"] == c2["x"] + assert c1["y"] == c2["y"] + assert c1["w"] == c2["w"] + assert c1["h"] == c2["h"] + assert c1["slide_id"] == SID + assert c2["slide_id"] == SID + assert c1["type_id"] == c2["type_id"] + if c1["type_id"] == 1: + assert c1["text"] == c2["text"] + elif c1["type_id"] == 2: + assert c1["image_id"] == c2["image_id"] + assert c1["view_type_id"] == 1 + assert c2["view_type_id"] == 3 + def test_question_api(client): add_default_values()