diff --git a/.vscode/settings.json b/.vscode/settings.json index 7229cda57..519424e1f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,6 +13,7 @@ "RSSI", "serialutil", "SITL", - "statustext" + "statustext", + "SUAS" ] } diff --git a/gcs/package.json b/gcs/package.json index 252c734c6..823f1e1ed 100644 --- a/gcs/package.json +++ b/gcs/package.json @@ -27,14 +27,19 @@ "dependencies": { "@headlessui/react": "2.1.4", "@mantine/code-highlight": "^7.17.1", - "@mantine/core": "^7.3.2", - "@mantine/hooks": "^7.3.2", + "@mantine/core": "^7.17.3", + "@mantine/hooks": "^7.17.3", "@mantine/notifications": "^7.4.0", "@mantine/spotlight": "^7.15.3", + "@mantine/tiptap": "^7.17.3", "@reduxjs/toolkit": "^2.2.7", "@robloche/chartjs-plugin-streaming": "^3.1.0", "@tabler/icons-react": "^2.44.0", "@tailwindcss/container-queries": "^0.1.1", + "@tiptap/extension-link": "^2.11.6", + "@tiptap/pm": "^2.11.6", + "@tiptap/react": "^2.11.6", + "@tiptap/starter-kit": "^2.11.6", "@tremor/react": "^3.12.1", "@turf/turf": "^7.2.0", "chart.js": "^4.4.2", diff --git a/gcs/src/components/customMantineTheme.jsx b/gcs/src/components/customMantineTheme.jsx index d44efe320..ee32fa6c1 100644 --- a/gcs/src/components/customMantineTheme.jsx +++ b/gcs/src/components/customMantineTheme.jsx @@ -27,4 +27,5 @@ export const CustomMantineTheme = createTheme({ tailwindColors.falcongrey[950], ], }, + cursorType: "pointer", }) diff --git a/gcs/src/components/dashboard/preFlightChecklist/checkListArea.jsx b/gcs/src/components/dashboard/preFlightChecklist/checkListArea.jsx new file mode 100644 index 000000000..51b33c977 --- /dev/null +++ b/gcs/src/components/dashboard/preFlightChecklist/checkListArea.jsx @@ -0,0 +1,203 @@ +/* + The Checklist area, this can be edited. +*/ + +// Native imports +import { useEffect, useState } from "react" + +// 3rd Party Imports +import { ActionIcon, Button, Checkbox, Modal, Tooltip } from "@mantine/core" + +// Local Imports +import EditCheckList from "./checkListEdit.jsx" + +// Styling imports +import resolveConfig from "tailwindcss/resolveConfig" +import tailwindConfig from "../../../../tailwind.config.js" +import { IconCheckbox, IconEdit, IconTrashX } from "@tabler/icons-react" +const tailwindColors = resolveConfig(tailwindConfig).theme.colors + +export default function CheckListArea({ + name, + items, + saveItems, + deleteChecklist, + setName, +}) { + const [showDeleteModal, setDeleteModal] = useState(false) + const [editCheckListModal, setEditCheckListModal] = useState(false) + const [checkListName, setChecklistName] = useState(name) + const [checkBoxList, setCheckboxList] = useState(items) + const [checkBoxListString, setCheckboxListString] = useState( + generateCheckboxListString(), + ) + const [mappedItems, setMappedItems] = useState(generateMappedItems()) + const [lastToggleCheck, setLastToggleCheck] = useState(false) // false = uncheck, true = check + + function generateCheckboxListString(set = false) { + // Go from list to string, returns0 + var final = "" + if (set) { + setCheckboxListString(final) + } + + return final + } + + function generateCheckboxList(defaultCheck = false) { + // Go from string to list, does not return + console.log(checkBoxListString) + var final = [] + checkBoxListString + .split("
  • ") + .splice(1) + .map((element) => { + var text = element.split("

    ")[0].trim() + if (text !== "") { + final.push({ + checked: defaultCheck, + name: element.split("

    ")[0].trim(), + }) + } + }) + setCheckboxList(final) + } + + function toggleCheck() { + generateCheckboxList(lastToggleCheck) + setLastToggleCheck(!lastToggleCheck) + } + + function setChecked(name, value) { + var final = [] + checkBoxListString + .split("
  • ") + .splice(1) + .map((element) => { + var elementName = element.split("

    ")[0].trim() + final.push({ + checked: + elementName == name + ? value + : checkBoxList.find((e) => e.name == elementName).checked, + name: elementName, + }) + }) + setCheckboxList(final) + } + + function generateMappedItems() { + return checkBoxList.map((element) => { + return ( + setChecked(element.name, !element.checked)} + /> + ) + }) + } + + useEffect(() => { + setMappedItems(generateMappedItems()) + saveItems(checkBoxList) + }, [checkBoxList]) + + return ( + <> + {/* Checkbox area */} +
    +
    +
    + + toggleCheck()} + > + + + + + setEditCheckListModal(true)} + > + + + +
    + + + setDeleteModal(true)} + > + + + +
    + + {mappedItems} +
    + + {/* Edit mode */} + setEditCheckListModal(false)} + nameSet={[checkListName, setChecklistName, (e) => setName(e)]} + checkListSet={[checkBoxListString, setCheckboxListString]} + generateCheckboxListString={generateCheckboxListString} + generateCheckboxList={generateCheckboxList} + /> + + {/* Generic "are you sure" modal */} + setDeleteModal(false)} + title="Are you sure you want to delete this checklist?" + centered + styles={{ + content: { + borderRadius: "0.5rem", + }, + }} + withCloseButton={false} + > +
    + + +
    +
    + + ) +} diff --git a/gcs/src/components/dashboard/preFlightChecklist/checkListEdit.jsx b/gcs/src/components/dashboard/preFlightChecklist/checkListEdit.jsx new file mode 100644 index 000000000..ec1f97d71 --- /dev/null +++ b/gcs/src/components/dashboard/preFlightChecklist/checkListEdit.jsx @@ -0,0 +1,136 @@ +/* + The modal to edit a checklist +*/ + +// 3rd Party Imports +import { Button, Modal, TextInput } from "@mantine/core" +import { useEditor } from "@tiptap/react" +import BulletList from "@tiptap/extension-bullet-list" +import ListItem from "@tiptap/extension-list-item" +import { RichTextEditor } from "@mantine/tiptap" +import { Node } from "@tiptap/core" + +// Styling imports +import resolveConfig from "tailwindcss/resolveConfig" +import tailwindConfig from "../../../../tailwind.config.js" +const tailwindColors = resolveConfig(tailwindConfig).theme.colors + +export default function EditCheckList({ + opened, + close, + nameSet, + checkListSet, + generateCheckboxListString, + generateCheckboxList, +}) { + const [name, setName, finaliseName] = nameSet // Finalise changes it in the selected accordion (ik annoying...) + const [checkboxList, setCheckboxList] = checkListSet + + const Document = Node.create({ + name: "doc", + topNode: true, + content: "list+", + }) + + const Paragraph = Node.create({ + name: "paragraph", + group: "block", + content: "inline*", + parseHTML() { + return [{ tag: "p" }] + }, + renderHTML({ HTMLAttributes }) { + return ["p", HTMLAttributes, 0] + }, + }) + + const Text = Node.create({ + name: "text", + group: "inline", + }) + + const editor = useEditor({ + extensions: [Document, Text, Paragraph, BulletList, ListItem], + content: checkboxList, + onUpdate: ({ editor }) => { + setCheckboxList(editor.getHTML()) + }, + autofocus: "end", + }) + + return ( + close()} + styles={{ + content: { + borderRadius: "0.5rem", + }, + }} + size={"xl"} + centered + > +
    { + e.preventDefault() + finaliseName(name) + generateCheckboxList() + close() + }} + > +
    + {/* Inputs */} +

    Name

    + setName(event.currentTarget.value)} + /> + +
    +

    Items

    +

    + Bullet point list of items +

    +
    + + {/* + Going to keep this for future use with code blocks, no need to delete. + + + + + + */} + + + + + {/* Controls */} +
    + + +
    +
    +
    +
    + ) +} diff --git a/gcs/src/components/dashboard/resizableInfoBox.jsx b/gcs/src/components/dashboard/resizableInfoBox.jsx index 24e2c7b77..a22d34c5c 100644 --- a/gcs/src/components/dashboard/resizableInfoBox.jsx +++ b/gcs/src/components/dashboard/resizableInfoBox.jsx @@ -34,7 +34,7 @@ export default function ResizableInfoBox(props) { }} className="h-full" > -
    +
    {props.children}
    diff --git a/gcs/src/components/dashboard/tabsSection.jsx b/gcs/src/components/dashboard/tabsSection.jsx index 002a971b3..6940e071d 100644 --- a/gcs/src/components/dashboard/tabsSection.jsx +++ b/gcs/src/components/dashboard/tabsSection.jsx @@ -11,6 +11,7 @@ import CameraTabsSection from "./tabsSectionTabs/cameraTabsSection" import ActionTabsSection from "./tabsSectionTabs/actionTabsSection" import MissionTabsSection from "./tabsSectionTabs/missionTabsSection" import DataTabsSection from "./tabsSectionTabs/dataTabsSection" +import PreFlightChecklistTab from "./tabsSectionTabs/preFlightChecklistSection" export default function TabsSection({ connected, @@ -22,16 +23,18 @@ export default function TabsSection({ displayedData, setDisplayedData, }) { - const tabPadding = "pt-6" + const tabPadding = "pt-6 pb-4" return ( - + Data Actions Mission Camera + Pre-Flight Checklist + {/* Data */} + + {/* Pre Flight Checklist */} + ) } diff --git a/gcs/src/components/dashboard/tabsSectionTabs/preFlightChecklistSection.jsx b/gcs/src/components/dashboard/tabsSectionTabs/preFlightChecklistSection.jsx new file mode 100644 index 000000000..288790f53 --- /dev/null +++ b/gcs/src/components/dashboard/tabsSectionTabs/preFlightChecklistSection.jsx @@ -0,0 +1,160 @@ +/** + * PreFlightCheckListSection + * Contains the preflight checklist + */ + +// Native imports +import { useState } from "react" + +// 3rd Party Imports +import { Tabs, Accordion, Button, Modal, TextInput } from "@mantine/core" +import { useLocalStorage } from "@mantine/hooks" + +// Local imports +import CheckListArea from "../preFlightChecklist/checkListArea.jsx" +import { showErrorNotification } from "../../../helpers/notification.js" + +// Styling imports +import resolveConfig from "tailwindcss/resolveConfig" +import tailwindConfig from "../../../../tailwind.config.js" +import { AddCommand } from "../../spotlight/commandHandler.js" +const tailwindColors = resolveConfig(tailwindConfig).theme.colors + +export default function PreFlightChecklistTab({ tabPadding }) { + const [preFlightChecklistItems, setPreFlightChecklistItems] = useLocalStorage( + { key: "preFlightCheckList", defaultValue: [] }, + ) + const [openChecklist, setOpenChecklist] = useLocalStorage({ + key: "lastOpenedPreFlightCheckList", + defaultValue: "", + }) + + // New checklist + const [showNewChecklistModal, setNewChecklistModal] = useState(false) + const [newChecklistName, setNewChecklistName] = useState("") + + function deleteChecklist(toDelete) { + var final = [] + preFlightChecklistItems.map((element) => { + if (element != toDelete) { + final.push(element) + } + }) + setPreFlightChecklistItems(final) + } + + function createNewChecklist() { + if (newChecklistName !== "") { + preFlightChecklistItems.push({ + name: newChecklistName, + value: [ + { + checked: false, + name: "Your first item, press edit to add more!", + }, + ], + }) + setPreFlightChecklistItems(preFlightChecklistItems) + setOpenChecklist(newChecklistName) + setNewChecklistModal(false) + setNewChecklistName("") + return + } + + // Show error message + showErrorNotification("Name cannot be empty") + } + + // Add create new checklist as a spotlight command + AddCommand("new_preflight_checklist", () => setNewChecklistModal(true)) + + const items = preFlightChecklistItems.map((item) => ( + setOpenChecklist(item.name)} + > + {item.name} + + { + item.value = e + setPreFlightChecklistItems(preFlightChecklistItems) + }} + deleteChecklist={() => deleteChecklist(item)} + name={item.name} + setName={(e) => { + item.name = e + setOpenChecklist(e) + }} + /> + + + )) + + return ( + +
    + {/* List, known issue of not opening the same list if name was changed but it's not worth it */} + + {items} + + {/* Controls */} + + + {/* New checklist modal */} + setNewChecklistModal(false)} + title="New Checklist" + centered + styles={{ + content: { + borderRadius: "0.5rem", + }, + }} + > +
    { + e.preventDefault() + createNewChecklist() + }} + > +
    + + setNewChecklistName(event.currentTarget.value) + } + data-autofocus + /> +
    + +
    + + +
    +
    +
    +
    +
    + ) +} diff --git a/gcs/src/components/dashboard/telemetry.jsx b/gcs/src/components/dashboard/telemetry.jsx index af7a30f65..3fc86de24 100644 --- a/gcs/src/components/dashboard/telemetry.jsx +++ b/gcs/src/components/dashboard/telemetry.jsx @@ -164,14 +164,13 @@ export default function TelemetrySection({ - {batteryData.map(battery => ( + {batteryData.map((battery) => (
    BATTERY{battery.id} - {(battery.voltages - ? battery.voltages[0] / 1000 - : 0 - ).toFixed(2)} + {(battery.voltages ? battery.voltages[0] / 1000 : 0).toFixed( + 2, + )} V diff --git a/gcs/src/components/error/errorBoundary.jsx b/gcs/src/components/error/errorBoundary.jsx index ffffa8c33..fdaa9c33d 100644 --- a/gcs/src/components/error/errorBoundary.jsx +++ b/gcs/src/components/error/errorBoundary.jsx @@ -7,7 +7,7 @@ import React from "react" // 3rd Party Imports import { Button } from "@mantine/core" -import { CodeHighlight } from '@mantine/code-highlight' +import { CodeHighlight } from "@mantine/code-highlight" export default function ErrorBoundaryFallback({ error }) { return ( diff --git a/gcs/src/components/navbar.jsx b/gcs/src/components/navbar.jsx index ab6d7bab7..b4b9a7012 100644 --- a/gcs/src/components/navbar.jsx +++ b/gcs/src/components/navbar.jsx @@ -259,12 +259,12 @@ export default function Navbar({ currentPage }) { backgroundOpacity: 0.55, blur: 3, }} - withCloseButton={false} styles={{ content: { borderRadius: "0.5rem", }, }} + withCloseButton={false} >
    { diff --git a/gcs/src/components/noDroneConnected.jsx b/gcs/src/components/noDroneConnected.jsx index ee82ea2f3..c836339fa 100644 --- a/gcs/src/components/noDroneConnected.jsx +++ b/gcs/src/components/noDroneConnected.jsx @@ -1,4 +1,4 @@ -export default function NoDroneConnected({tab}) { +export default function NoDroneConnected({ tab }) { return (

    diff --git a/gcs/src/components/spotlight/actions.jsx b/gcs/src/components/spotlight/actions.jsx index 9e7b863f2..f43df52a6 100644 --- a/gcs/src/components/spotlight/actions.jsx +++ b/gcs/src/components/spotlight/actions.jsx @@ -12,7 +12,7 @@ import tailwindConfig from "../../../tailwind.config" const tailwindColors = resolveConfig(tailwindConfig).theme.colors const badgeColor = tailwindColors.falcongrey[600] -let actions = [] +export let actions = [] function AddSpotlightAction( id, label, @@ -125,3 +125,11 @@ AddSpotlightAction( kbdBadge("Ctrl + ,", badgeColor), kbdBadge("⌘ + ,", badgeColor), ) +AddSpotlightAction( + "new_preflight_checklist", + "New Pre-flight Checklist", + "command", + () => { + RunCommand("new_preflight_checklist") + }, +) diff --git a/gcs/src/css/index.css b/gcs/src/css/index.css index 3eab1a70e..1e82cb715 100644 --- a/gcs/src/css/index.css +++ b/gcs/src/css/index.css @@ -14,6 +14,11 @@ -webkit-text-size-adjust: 100%; @apply bg-falcongrey-800 text-neutral-50 h-full max-h-full min-h-screen; + + ul, + ol { + list-style: revert; + } } :root[data-mantine-color-scheme="dark"] { @@ -53,10 +58,10 @@ body { /* Handle */ ::-webkit-scrollbar-thumb { - background: #d36565; + background: #727c7e; } /* Handle on hover */ ::-webkit-scrollbar-thumb:hover { - background: #b43e31; + background: #535d5f; } diff --git a/gcs/src/css/resizable.css b/gcs/src/css/resizable.css index 8e508a7fd..8f688a929 100644 --- a/gcs/src/css/resizable.css +++ b/gcs/src/css/resizable.css @@ -79,7 +79,7 @@ .custom-handle-e { top: 50%; - right: 1%; + right: 10px; transform: rotate(135deg); cursor: e-resize; position: absolute; diff --git a/gcs/src/dashboard.jsx b/gcs/src/dashboard.jsx index 443754b36..696274606 100644 --- a/gcs/src/dashboard.jsx +++ b/gcs/src/dashboard.jsx @@ -152,7 +152,9 @@ export default function Dashboard() { () => ({ VFR_HUD: (msg) => setTelemetryData(msg), BATTERY_STATUS: (msg) => { - const battery = localBatteryData.filter(battery => battery.id == msg.id)[0] + const battery = localBatteryData.filter( + (battery) => battery.id == msg.id, + )[0] if (battery) { Object.assign(battery, msg) } else { diff --git a/gcs/src/main.jsx b/gcs/src/main.jsx index 4112bcd93..5dcd8da21 100644 --- a/gcs/src/main.jsx +++ b/gcs/src/main.jsx @@ -5,7 +5,8 @@ import "./css/resizable.css" import "@mantine/core/styles.css" import "@mantine/notifications/styles.css" import "@mantine/spotlight/styles.css" -import '@mantine/code-highlight/styles.css' +import "@mantine/code-highlight/styles.css" +import "@mantine/tiptap/styles.css" // React imports import { HashRouter } from "react-router-dom" diff --git a/run.bash b/run.bash index 9feb29c69..a9771429d 100755 --- a/run.bash +++ b/run.bash @@ -8,6 +8,8 @@ if [ -z "$2" ]; then else cd radio pip install -r requirements.txt + cd ../gcs + yarn cd ../ concurrently "python radio/app.py" "cd gcs && yarn dev" -n "backend,frontend" -c "red,blue" fi \ No newline at end of file