diff --git a/src/backend/app/projects/project_logic.py b/src/backend/app/projects/project_logic.py index 2a5e6492..01a36390 100644 --- a/src/backend/app/projects/project_logic.py +++ b/src/backend/app/projects/project_logic.py @@ -200,12 +200,12 @@ async def get_project_info_from_s3(project_id: uuid.UUID, task_id: uuid.UUID): # Generate a presigned URL for the assets ZIP file try: # Check if the object exists - assets_path = f"processed/{project_id}/{task_id}/assets.zip" + assets_path = f"projects/{project_id}/{task_id}/assets.zip" get_object_metadata(settings.S3_BUCKET_NAME, assets_path) # If it exists, generate the presigned URL presigned_url = get_presigned_url( - settings.S3_BUCKET_NAME, assets_path, expires=3600 + settings.S3_BUCKET_NAME, assets_path, expires=2 ) except S3Error as e: if e.code == "NoSuchKey": diff --git a/src/backend/app/s3.py b/src/backend/app/s3.py index 68aaf6d7..7120ccba 100644 --- a/src/backend/app/s3.py +++ b/src/backend/app/s3.py @@ -3,6 +3,7 @@ from minio import Minio from io import BytesIO from typing import Any +from datetime import timedelta def s3_client(): @@ -182,20 +183,22 @@ def list_objects_from_bucket(bucket_name: str, prefix: str): return objects -def get_presigned_url(bucket_name: str, object_name: str, expires: int = 3600): +def get_presigned_url(bucket_name: str, object_name: str, expires: int = 2): """Generate a presigned URL for an object in an S3 bucket. Args: bucket_name (str): The name of the S3 bucket. object_name (str): The name of the object in the bucket. - expires (int, optional): The time in seconds until the URL expires. - Defaults to 3600. + expires (int, optional): The time in hours until the URL expires. + Defaults to 2 hour. Returns: str: The presigned URL to access the object. """ client = s3_client() - return client.presigned_get_object(bucket_name, object_name, expires=expires) + return client.presigned_get_object( + bucket_name, object_name, expires=timedelta(hours=expires) + ) def get_object_metadata(bucket_name: str, object_name: str): diff --git a/src/frontend/package.json b/src/frontend/package.json index 613f7b14..f07f9e6a 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -15,6 +15,7 @@ "@turf/area": "^7.0.0", "@turf/bbox": "^7.0.0", "@turf/centroid": "^7.0.0", + "@turf/helpers": "^7.0.0", "@turf/meta": "^7.0.0", "@turf/flatten": "^7.0.0", "@turf/length": "^7.0.0", diff --git a/src/frontend/src/components/DroneOperatorTask/DescriptionSection/PopoverBox/ImageBox/ImageCard/index.tsx b/src/frontend/src/components/DroneOperatorTask/DescriptionSection/PopoverBox/ImageBox/ImageCard/index.tsx index c9bb0557..fb23e168 100644 --- a/src/frontend/src/components/DroneOperatorTask/DescriptionSection/PopoverBox/ImageBox/ImageCard/index.tsx +++ b/src/frontend/src/components/DroneOperatorTask/DescriptionSection/PopoverBox/ImageBox/ImageCard/index.tsx @@ -21,7 +21,7 @@ const ImageCard = ({ return ( <>
dispatch(setSelectedImage(image))} > diff --git a/src/frontend/src/components/DroneOperatorTask/DescriptionSection/PopoverBox/ImageBox/index.tsx b/src/frontend/src/components/DroneOperatorTask/DescriptionSection/PopoverBox/ImageBox/index.tsx index 9c5bf842..6b0bf2b7 100644 --- a/src/frontend/src/components/DroneOperatorTask/DescriptionSection/PopoverBox/ImageBox/index.tsx +++ b/src/frontend/src/components/DroneOperatorTask/DescriptionSection/PopoverBox/ImageBox/index.tsx @@ -3,27 +3,24 @@ /* eslint-disable no-await-in-loop */ /* eslint-disable no-console */ /* eslint-disable no-unused-vars */ -import { useEffect, useState, useRef } from 'react'; -import { motion } from 'framer-motion'; -import { useParams } from 'react-router-dom'; import { useMutation } from '@tanstack/react-query'; +import { motion } from 'framer-motion'; +import { useEffect, useRef, useState } from 'react'; -import { useTypedDispatch, useTypedSelector } from '@Store/hooks'; +import { Button } from '@Components/RadixComponents/Button'; +import { getImageUploadLink } from '@Services/droneOperator'; import { + checkAllImages, setCheckedImages, - showPopover, unCheckAllImages, - checkAllImages, } from '@Store/actions/droneOperatorTask'; -import Icon from '@Components/common/Icon'; -import { Button } from '@Components/RadixComponents/Button'; -import { getImageUploadLink } from '@Services/droneOperator'; -import delay from '@Utils/createDelay'; -import chunkArray from '@Utils/createChunksOfArray'; +import { useTypedDispatch, useTypedSelector } from '@Store/hooks'; import callApiSimultaneously from '@Utils/callApiSimultaneously'; +import chunkArray from '@Utils/createChunksOfArray'; +import delay from '@Utils/createDelay'; import widthCalulator from '@Utils/percentageCalculator'; -import ImageCard from './ImageCard'; import FilesUploadingPopOver from '../LoadingBox'; +import ImageCard from './ImageCard'; import PreviewImage from './PreviewImage'; // interface IImageBoxPopOverProps { @@ -151,7 +148,7 @@ const ImageBoxPopOver = () => { className={`naxatw-grid naxatw-gap-4 ${clickedImage ? 'naxatw-grid-cols-[70%_auto]' : 'naxatw-grid-cols-1'}`} >
{imageObject?.map((image, index) => ( { - const navigate = useNavigate(); + const dispatch = useDispatch(); + // const navigate = useNavigate(); - // function to redirect to dashboard after 2 seconds - function redirectToDashboard() { + // function to close modal + function closeModal() { setTimeout(() => { - navigate('/dashboard'); + // navigate('/dashboard'); + dispatch(toggleModal()); }, 2000); return null; } @@ -49,8 +52,8 @@ const FilesUploadingPopOver = ({

{uploadedFiles === filesLength && uploadedFiles !== 0 ? ( <> -

Redirecting to Dashboard ...

- {redirectToDashboard()} + {/*

Redirecting to Dashboard ...

*/} + {closeModal()} ) : ( `${uploadedFiles} / ${filesLength} Files Uploaded` diff --git a/src/frontend/src/components/DroneOperatorTask/DescriptionSection/index.tsx b/src/frontend/src/components/DroneOperatorTask/DescriptionSection/index.tsx index 9e205376..f49d37d3 100644 --- a/src/frontend/src/components/DroneOperatorTask/DescriptionSection/index.tsx +++ b/src/frontend/src/components/DroneOperatorTask/DescriptionSection/index.tsx @@ -1,16 +1,16 @@ /* eslint-disable no-nested-ternary */ -import { useState, useEffect } from 'react'; -import { useParams } from 'react-router-dom'; -import { motion } from 'framer-motion'; -import { Button } from '@Components/RadixComponents/Button'; -import Tab from '@Components/common/Tabs'; import { useGetIndividualTaskQuery, useGetTaskWaypointQuery } from '@Api/tasks'; -import { useTypedDispatch, useTypedSelector } from '@Store/hooks'; -import { setSecondPageState } from '@Store/actions/droneOperatorTask'; +import { Button } from '@Components/RadixComponents/Button'; +import useWindowDimensions from '@Hooks/useWindowDimensions'; +import { useTypedSelector } from '@Store/hooks'; import hasErrorBoundary from '@Utils/hasErrorBoundary'; +import { motion } from 'framer-motion'; +import { useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; import { toast } from 'react-toastify'; -import UploadsBox from './UploadsBox'; +import MapSection from '../MapSection'; import DescriptionBox from './DescriptionBox'; +import UploadsBox from './UploadsBox'; const { BASE_URL } = process.env; @@ -19,6 +19,9 @@ const DroneOperatorDescriptionBox = () => { const secondPageStates = useTypedSelector(state => state.droneOperatorTask); const { secondPageState, secondPage } = secondPageStates; const [animated, setAnimated] = useState(false); + const [showDownloadOptions, setShowDownloadOptions] = + useState(false); + const { width } = useWindowDimensions(); const { data: taskDescription }: Record = useGetIndividualTaskQuery(taskId as string); @@ -78,20 +81,6 @@ const DroneOperatorDescriptionBox = () => { ); } }; - const dispatch = useTypedDispatch(); - - const headerTabOptions = [ - { - id: 1, - label: 'Description', - value: 'description', - }, - { - id: 2, - label: 'Uploads', - value: 'uploads', - }, - ]; const handleDownloadFlightPlan = () => { fetch( @@ -147,38 +136,48 @@ const DroneOperatorDescriptionBox = () => {

Task #{taskDescription?.project_task_index}

-
- + +
+ {showDownloadOptions && ( +
+
handleDownloadFlightPlan()} + onClick={() => { + handleDownloadFlightPlan(); + setShowDownloadOptions(false); + }} + > + Download flight plan +
+
downloadGeojson()} + onClick={() => { + downloadGeojson(); + setShowDownloadOptions(false); + }} + > + Download geojson +
+
+ )}
- { - dispatch(setSecondPageState(value)); - }} - tabOptions={headerTabOptions} - activeTab={secondPageState} - orientation="row" - className={`naxatw-h-[3rem] naxatw-border-b naxatw-bg-transparent hover:naxatw-border-b-2 hover:naxatw-border-red ${!secondPage ? 'naxatw-hidden' : 'naxatw-block'}`} - activeClassName="naxatw-border-b-2 naxatw-bg-transparent naxatw-border-red" - clickable - /> + {width < 640 && } {renderComponent(secondPageState)}
diff --git a/src/frontend/src/components/DroneOperatorTask/MapSection/GetCoordinatesOnClick.tsx b/src/frontend/src/components/DroneOperatorTask/MapSection/GetCoordinatesOnClick.tsx new file mode 100644 index 00000000..66659ad5 --- /dev/null +++ b/src/frontend/src/components/DroneOperatorTask/MapSection/GetCoordinatesOnClick.tsx @@ -0,0 +1,31 @@ +/* eslint-disable no-param-reassign */ +import { MapInstanceType } from '@Components/common/MapLibreComponents/types'; +import { useEffect } from 'react'; + +interface IGetCoordinatesOnClick { + map?: MapInstanceType; + isMapLoaded?: Boolean; + getCoordinates: any; +} + +const GetCoordinatesOnClick = ({ + map, + isMapLoaded, + getCoordinates, +}: IGetCoordinatesOnClick) => { + useEffect(() => { + if (!map || !isMapLoaded) return () => {}; + map.getCanvas().style.cursor = 'crosshair'; + map.on('click', e => { + const latLng = e.lngLat; + getCoordinates(latLng); + }); + + return () => { + map.getCanvas().style.cursor = ''; + }; + }, [map, isMapLoaded, getCoordinates]); + return null; +}; + +export default GetCoordinatesOnClick; diff --git a/src/frontend/src/components/DroneOperatorTask/MapSection/ShowInfo.tsx b/src/frontend/src/components/DroneOperatorTask/MapSection/ShowInfo.tsx new file mode 100644 index 00000000..e16aa1a5 --- /dev/null +++ b/src/frontend/src/components/DroneOperatorTask/MapSection/ShowInfo.tsx @@ -0,0 +1,31 @@ +interface IShowInfo { + heading?: string; + message: string; + wrapperClassName?: string; + className?: string; +} + +const ShowInfo = ({ + message, + className, + heading, + wrapperClassName, +}: IShowInfo) => { + return ( +
+
+ info{' '} +
+ {heading} +
+
+
+ {message} +
+
+ ); +}; + +export default ShowInfo; diff --git a/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx b/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx index 5ddd3954..5666af81 100644 --- a/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx +++ b/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx @@ -1,24 +1,37 @@ /* eslint-disable react/no-array-index-key */ -import { useCallback, useEffect, useState } from 'react'; -import { LngLatBoundsLike, Map } from 'maplibre-gl'; -import { useParams } from 'react-router-dom'; -import { FeatureCollection } from 'geojson'; import { useGetTaskWaypointQuery } from '@Api/tasks'; -import getBbox from '@turf/bbox'; -import { coordAll } from '@turf/meta'; +import marker from '@Assets/images/marker.png'; +import right from '@Assets/images/rightArrow.png'; +import BaseLayerSwitcherUI from '@Components/common/BaseLayerSwitcher'; import { useMapLibreGLMap } from '@Components/common/MapLibreComponents'; +import AsyncPopup from '@Components/common/MapLibreComponents/AsyncPopup'; import VectorLayer from '@Components/common/MapLibreComponents/Layers/VectorLayer'; +import LocateUser from '@Components/common/MapLibreComponents/LocateUser'; import MapContainer from '@Components/common/MapLibreComponents/MapContainer'; import { GeojsonType } from '@Components/common/MapLibreComponents/types'; -import right from '@Assets/images/rightArrow.png'; -import marker from '@Assets/images/marker.png'; +import { Button } from '@Components/RadixComponents/Button'; +import { postTaskWaypoint } from '@Services/tasks'; +import { toggleModal } from '@Store/actions/common'; +import { setSelectedTakeOffPoint } from '@Store/actions/droneOperatorTask'; +import { useTypedSelector } from '@Store/hooks'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import getBbox from '@turf/bbox'; +import { point } from '@turf/helpers'; +import { coordAll } from '@turf/meta'; import hasErrorBoundary from '@Utils/hasErrorBoundary'; -import AsyncPopup from '@Components/common/MapLibreComponents/AsyncPopup'; -import BaseLayerSwitcherUI from '@Components/common/BaseLayerSwitcher'; -import LocateUser from '@Components/common/MapLibreComponents/LocateUser'; +import { FeatureCollection } from 'geojson'; +import { LngLatBoundsLike, Map } from 'maplibre-gl'; +import { useCallback, useEffect, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { useParams } from 'react-router-dom'; +import { toast } from 'react-toastify'; +import GetCoordinatesOnClick from './GetCoordinatesOnClick'; +import ShowInfo from './ShowInfo'; -const MapSection = () => { +const MapSection = ({ className }: { className?: string }) => { + const dispatch = useDispatch(); const { projectId, taskId } = useParams(); + const queryClient = useQueryClient(); const [popupData, setPopupData] = useState>({}); const { map, isMapLoaded } = useMapLibreGLMap({ containerId: 'dashboard-map', @@ -29,6 +42,9 @@ const MapSection = () => { }, disableRotation: true, }); + const newTakeOffPoint = useTypedSelector( + state => state.droneOperatorTask.selectedTakeOffPoint, + ); const { data: taskWayPoints }: any = useGetTaskWaypointQuery( projectId as string, @@ -56,13 +72,42 @@ const MapSection = () => { }, ); - // zoom to task + const { mutate: postWaypoint, isLoading: isUpdatingTakeOffPoint } = + useMutation({ + mutationFn: postTaskWaypoint, + onSuccess: async data => { + // update task cached waypoint data with response + queryClient.setQueryData(['task-waypoints'], () => { + return data; + }); + dispatch(setSelectedTakeOffPoint(null)); + }, + onError: (err: any) => { + toast.error(err?.response?.data?.detail || err.message); + }, + }); + + // zoom to task (waypoint) useEffect(() => { - if (!taskWayPoints?.geojsonAsLineString) return; + if (!taskWayPoints?.geojsonAsLineString || !isMapLoaded || !map) return; const { geojsonAsLineString } = taskWayPoints; - const bbox = getBbox(geojsonAsLineString as FeatureCollection); + let bbox = null; + // calculate bbox with with updated take-off point + if (newTakeOffPoint && newTakeOffPoint !== 'place_on_map') { + const combinedFeatures: FeatureCollection = { + type: 'FeatureCollection', + features: [ + ...geojsonAsLineString.features, + // @ts-ignore + newTakeOffPoint, + ], + }; + bbox = getBbox(combinedFeatures); + } else { + bbox = getBbox(geojsonAsLineString as FeatureCollection); + } map?.fitBounds(bbox as LngLatBoundsLike, { padding: 25, duration: 500 }); - }, [map, taskWayPoints]); + }, [map, taskWayPoints, newTakeOffPoint, isMapLoaded]); const getPopupUI = useCallback(() => { return ( @@ -70,15 +115,20 @@ const MapSection = () => {

{popupData?.index}

- {popupData?.coordinates?.lat?.toFixed(8)}, {popupData?.coordinates?.lng?.toFixed(8)}{' '} + {popupData?.coordinates?.lat?.toFixed(8)},  + {popupData?.coordinates?.lng?.toFixed(8)}{' '}

Speed: {popupData?.speed} m/s

- {popupData?.elevation && -

Elevation (Sea Level): {popupData?.elevation} meter

- } -

Take Photo: {popupData?.take_photo ? "True" : "False"}

+ {popupData?.elevation && ( +

+ Elevation (Sea Level): {popupData?.elevation} meter{' '} +

+ )} +

+ Take Photo: {popupData?.take_photo ? 'True' : 'False'} +

Gimble angle: {popupData?.gimbal_angle} degree

@@ -92,9 +142,31 @@ const MapSection = () => { ); }, [popupData]); + const handleSaveStartingPoint = () => { + const { geometry } = newTakeOffPoint as Record; + const [lng, lat] = geometry.coordinates; + postWaypoint({ + projectId, + taskId, + data: { + longitude: lng, + latitude: lat, + }, + }); + }; + + useEffect( + () => () => { + dispatch(setSelectedTakeOffPoint(null)); + }, + [dispatch], + ); + return ( <> -
+
{ )} +
+ +
+ + {newTakeOffPoint && ( + + )} + + {newTakeOffPoint === 'place_on_map' && ( + ) => + dispatch( + setSelectedTakeOffPoint( + point([coordinates.lng, coordinates?.lat]), + ), + ) + } + /> + )} + + {newTakeOffPoint === 'place_on_map' && ( + + )} + ) => diff --git a/src/frontend/src/components/DroneOperatorTask/ModalContent/ChooseTakeOffPointOptions.tsx b/src/frontend/src/components/DroneOperatorTask/ModalContent/ChooseTakeOffPointOptions.tsx new file mode 100644 index 00000000..1cb16b92 --- /dev/null +++ b/src/frontend/src/components/DroneOperatorTask/ModalContent/ChooseTakeOffPointOptions.tsx @@ -0,0 +1,61 @@ +import RadioButton from '@Components/common/RadioButton'; +import { Button } from '@Components/RadixComponents/Button'; +import { takeOffPointOptions } from '@Constants/taskDescription'; +import { toggleModal } from '@Store/actions/common'; +import { + setSelectedTakeOffPoint, + setSelectedTakeOffPointOption, +} from '@Store/actions/droneOperatorTask'; +import { useTypedDispatch, useTypedSelector } from '@Store/hooks'; +import { point } from '@turf/helpers'; + +const ChooseTakeOffPointOptions = () => { + const dispatch = useTypedDispatch(); + const selectedTakeOffPointOption = useTypedSelector( + state => state.droneOperatorTask.selectedTakeOffPointOption, + ); + + const handleNextClick = () => { + if (selectedTakeOffPointOption === 'current_location') { + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition(latLng => + dispatch( + setSelectedTakeOffPoint( + point([latLng?.coords?.longitude, latLng?.coords?.latitude]), + ), + ), + ); + } + } else { + dispatch(setSelectedTakeOffPoint(selectedTakeOffPointOption)); + } + dispatch(toggleModal()); + }; + return ( +
+

+ Please select the take-off point for your drone. +

+
+ dispatch(setSelectedTakeOffPointOption(value))} + value={selectedTakeOffPointOption} + /> +
+
+ +
+
+ ); +}; + +export default ChooseTakeOffPointOptions; diff --git a/src/frontend/src/constants/modalContents.tsx b/src/frontend/src/constants/modalContents.tsx index a5eb0a79..caecbacd 100644 --- a/src/frontend/src/constants/modalContents.tsx +++ b/src/frontend/src/constants/modalContents.tsx @@ -1,11 +1,13 @@ -import { ReactElement } from 'react'; import ExitCreateProjectModal from '@Components/CreateProject/ExitCreateProjectModal'; import ImageBoxPopOver from '@Components/DroneOperatorTask/DescriptionSection/PopoverBox/ImageBox'; +import ChooseTakeOffPointOptions from '@Components/DroneOperatorTask/ModalContent/ChooseTakeOffPointOptions'; +import { ReactElement } from 'react'; export type ModalContentsType = | 'sign-up-success' | 'quit-create-project' | 'raw-image-preview' + | 'update-flight-take-off-point' | null; export type PromptDialogContentsType = 'delete-layer' | null; @@ -31,10 +33,16 @@ export function getModalContent(content: ModalContentsType): ModalReturnType { case 'raw-image-preview': return { - className: '!naxatw-w-[60vw]', + className: '!naxatw-w-[95vw] md:!naxatw-w-[60vw]', title: 'Upload Raw Image', content: , }; + case 'update-flight-take-off-point': + return { + className: 'naxatw-w-[92vw] naxatw-max-w-[25rem]', + title: 'Take-off Point', + content: , + }; default: return { diff --git a/src/frontend/src/constants/taskDescription.ts b/src/frontend/src/constants/taskDescription.ts new file mode 100644 index 00000000..afeebfb4 --- /dev/null +++ b/src/frontend/src/constants/taskDescription.ts @@ -0,0 +1,14 @@ +/* eslint-disable import/prefer-default-export */ + +export const takeOffPointOptions = [ + { + label: 'My current location', + value: 'current_location', + name: 'take_off_point', + }, + { + label: 'Place on map', + value: 'place_on_map', + name: 'take_off_point', + }, +]; diff --git a/src/frontend/src/hooks/useWindowDimensions.tsx b/src/frontend/src/hooks/useWindowDimensions.tsx new file mode 100644 index 00000000..ee0a0de8 --- /dev/null +++ b/src/frontend/src/hooks/useWindowDimensions.tsx @@ -0,0 +1,36 @@ +import { useState, useEffect } from 'react'; + +function debounce(func: Function, timeout = 300) { + let timer: any; + return (arg: any) => { + clearTimeout(timer); + timer = setTimeout(() => { + func(arg); + }, timeout); + }; +} + +const useWindowDimensions = () => { + const [dimensions, setDimensions] = useState({ + width: window.innerWidth, + height: window.innerHeight, + }); + + useEffect(() => { + const handleResize = debounce(() => { + setDimensions({ + width: window.innerWidth, + height: window.innerHeight, + }); + }, 100); + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); + + return dimensions; +}; + +export default useWindowDimensions; diff --git a/src/frontend/src/services/tasks.ts b/src/frontend/src/services/tasks.ts index 67561a69..47a7f4d1 100644 --- a/src/frontend/src/services/tasks.ts +++ b/src/frontend/src/services/tasks.ts @@ -8,5 +8,15 @@ export const getTaskWaypoint = (projectId: string, taskId: string) => export const getIndividualTask = (taskId: string) => authenticated(api).get(`/tasks/${taskId}`); +export const postTaskWaypoint = (payload: Record) => { + const { taskId, projectId, data } = payload; + return authenticated(api).post( + `/waypoint/task/${taskId}/?project_id=${projectId}&download=false`, + data, + { + headers: { 'Content-Type': 'application/json' }, + }, + ); +}; export const getTaskAssetsInfo = (projectId: string, taskId: string) => authenticated(api).get(`/projects/assets/${projectId}/${taskId}/`); diff --git a/src/frontend/src/store/actions/droneOperatorTask.ts b/src/frontend/src/store/actions/droneOperatorTask.ts index 5564edd5..19e24a36 100644 --- a/src/frontend/src/store/actions/droneOperatorTask.ts +++ b/src/frontend/src/store/actions/droneOperatorTask.ts @@ -10,4 +10,6 @@ export const { unCheckAllImages, checkAllImages, setFiles, + setSelectedTakeOffPointOption, + setSelectedTakeOffPoint, } = droneOperatorTaskSlice.actions; diff --git a/src/frontend/src/store/slices/droneOperartorTask.ts b/src/frontend/src/store/slices/droneOperartorTask.ts index 5bb88bbc..85f33d4a 100644 --- a/src/frontend/src/store/slices/droneOperartorTask.ts +++ b/src/frontend/src/store/slices/droneOperartorTask.ts @@ -8,6 +8,8 @@ export interface IDroneOperatorTaskState { checkedImages: Record; popOver: boolean; files: any[]; + selectedTakeOffPointOption: string; + selectedTakeOffPoint: any[] | string | null; } const initialState: IDroneOperatorTaskState = { @@ -17,6 +19,8 @@ const initialState: IDroneOperatorTaskState = { checkedImages: {}, popOver: false, files: [], + selectedTakeOffPointOption: 'current_location', + selectedTakeOffPoint: null, }; export const droneOperatorTaskSlice = createSlice({ @@ -55,6 +59,12 @@ export const droneOperatorTaskSlice = createSlice({ setFiles: (state, action) => { state.files = action.payload; }, + setSelectedTakeOffPointOption: (state, action) => { + state.selectedTakeOffPointOption = action.payload; + }, + setSelectedTakeOffPoint: (state, action) => { + state.selectedTakeOffPoint = action.payload; + }, }, }); diff --git a/src/frontend/src/views/TaskDescription/index.tsx b/src/frontend/src/views/TaskDescription/index.tsx index daa5753e..fa7b0088 100644 --- a/src/frontend/src/views/TaskDescription/index.tsx +++ b/src/frontend/src/views/TaskDescription/index.tsx @@ -1,18 +1,21 @@ import DroneOperatorDescriptionBox from '@Components/DroneOperatorTask/DescriptionSection'; import DroneOperatorTaskHeader from '@Components/DroneOperatorTask/Header'; import MapSection from '@Components/DroneOperatorTask/MapSection'; +import useWindowDimensions from '@Hooks/useWindowDimensions'; import hasErrorBoundary from '@Utils/hasErrorBoundary'; const TaskDescription = () => { + const { width } = useWindowDimensions(); + return ( <>
-
+
-
+
- + {width >= 640 && }