From c7ecd2422a08771d525d1ef7b4483942d01eb4fb Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Thu, 11 Apr 2024 17:29:23 +0200 Subject: [PATCH] Started working on unit control panel, typed props --- frontend/react/src/types/types.ts | 4 +- .../react/src/ui/components/olaccordion.tsx | 8 +- .../react/src/ui/components/olcheckbox.tsx | 7 +- .../src/ui/components/olcoalitiontoggle.tsx | 20 ++- .../react/src/ui/components/oldropdown.tsx | 9 +- .../react/src/ui/components/ollabeltoggle.tsx | 19 ++- .../react/src/ui/components/olnumberinput.tsx | 17 +- .../react/src/ui/components/olrangeslider.tsx | 16 +- .../react/src/ui/components/olsearchbar.tsx | 10 +- .../react/src/ui/components/olstatebutton.tsx | 15 +- .../src/ui/components/olunitlistentry.tsx | 10 +- .../react/src/ui/components/olunitsummary.tsx | 8 +- .../react/src/ui/panels/components/menu.tsx | 31 ++-- frontend/react/src/ui/panels/header.tsx | 8 +- frontend/react/src/ui/panels/mainmenu.tsx | 13 +- frontend/react/src/ui/panels/sidebar.tsx | 4 +- frontend/react/src/ui/panels/spawnmenu.tsx | 127 ++++++++------ .../react/src/ui/panels/unitcontrolmenu.tsx | 158 ++++++++++++------ .../react/src/ui/panels/unitspawnmenu.tsx | 45 ++++- frontend/react/src/ui/ui.tsx | 8 +- frontend/react/src/unit/unitsmanager.ts | 2 +- 21 files changed, 368 insertions(+), 171 deletions(-) diff --git a/frontend/react/src/types/types.ts b/frontend/react/src/types/types.ts index 3d4e8dcc..d0daebf7 100644 --- a/frontend/react/src/types/types.ts +++ b/frontend/react/src/types/types.ts @@ -47,4 +47,6 @@ export type MGRS = { rowLetter: string, string: string, zoneNumber: string -} \ No newline at end of file +} + +export type Coalition = "blue" | "neutral" | "red"; \ No newline at end of file diff --git a/frontend/react/src/ui/components/olaccordion.tsx b/frontend/react/src/ui/components/olaccordion.tsx index 9ab4aea2..95d6d154 100644 --- a/frontend/react/src/ui/components/olaccordion.tsx +++ b/frontend/react/src/ui/components/olaccordion.tsx @@ -1,8 +1,12 @@ -import React, { useId, useEffect, useRef, useState } from "react" +import React, { useEffect, useRef, useState } from "react" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faArrowCircleDown } from "@fortawesome/free-solid-svg-icons"; -export function OlAccordion(props) { +export function OlAccordion(props: { + title: string, + children?: JSX.Element | JSX.Element[], + showArrows?: boolean +}) { var [open, setOpen] = useState(false); var [scrolledUp, setScrolledUp] = useState(true); var [scrolledDown, setScrolledDown] = useState(false); diff --git a/frontend/react/src/ui/components/olcheckbox.tsx b/frontend/react/src/ui/components/olcheckbox.tsx index 64fe7d24..b3a23abb 100644 --- a/frontend/react/src/ui/components/olcheckbox.tsx +++ b/frontend/react/src/ui/components/olcheckbox.tsx @@ -1,6 +1,9 @@ -import React from "react"; +import React, { ChangeEvent } from "react"; -export function OlCheckbox(props) { +export function OlCheckbox(props: { + checked: boolean, + onChange: (e: ChangeEvent) => void +}) { return - -
- Large toggle +export function OlCoalitionToggle(props: { + coalition: Coalition, + onClick: () => void +}) { + return
+
} \ No newline at end of file diff --git a/frontend/react/src/ui/components/oldropdown.tsx b/frontend/react/src/ui/components/oldropdown.tsx index 6582c822..75c22868 100644 --- a/frontend/react/src/ui/components/oldropdown.tsx +++ b/frontend/react/src/ui/components/oldropdown.tsx @@ -1,8 +1,15 @@ import React, { useState, useEffect, useRef } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { IconProp } from "@fortawesome/fontawesome-svg-core"; -export function OlDropdown(props) { +export function OlDropdown(props: { + className: string, + leftIcon?: IconProp, + rightIcon?: IconProp, + label: string, + children?: JSX.Element | JSX.Element[] +}) { var [open, setOpen] = useState(false); var contentRef = useRef(null); var buttonRef = useRef(null); diff --git a/frontend/react/src/ui/components/ollabeltoggle.tsx b/frontend/react/src/ui/components/ollabeltoggle.tsx index f461e4fa..79ecce24 100644 --- a/frontend/react/src/ui/components/ollabeltoggle.tsx +++ b/frontend/react/src/ui/components/ollabeltoggle.tsx @@ -1,11 +1,14 @@ -import React, { useState } from "react"; +import React from "react"; -export function OlLabelToggle(props) { - var [toggled, setToggled] = useState(false); - - return } \ No newline at end of file diff --git a/frontend/react/src/ui/components/olnumberinput.tsx b/frontend/react/src/ui/components/olnumberinput.tsx index 0ed2e4cb..c9101452 100644 --- a/frontend/react/src/ui/components/olnumberinput.tsx +++ b/frontend/react/src/ui/components/olnumberinput.tsx @@ -1,15 +1,22 @@ -import React, {useEffect, useId} from "react"; +import React, {ChangeEvent, useEffect, useId} from "react"; -export function OlNumberInput(props) { +export function OlNumberInput(props: { + value: number, + min: number, + max: number, + onDecrease: () => void, + onIncrease: () => void, + onChange: (e: ChangeEvent) => void +}) { return
- - - } -export function OlRoundStateButton(props) { +export function OlRoundStateButton(props: { + className?: string, + checked: boolean, + icon: IconProp, + onClick: () => void +}) { const className = (props.className ?? '') + ` h-8 w-8 flex-none m-auto border border-gray-900 font-medium rounded-full text-sm dark:bg-[transparent] dark:data-[checked='true']:bg-white dark:text-white dark:data-[checked='true']:text-gray-900 dark:border-gray-600 `; return - - {props.children} -
+export function Menu(props: { + title: string, + open: boolean, + onClose: () => void, + onBack?: () => void, + showBackButton?: boolean, + children?: JSX.Element | JSX.Element[], +}) { + return
+
+ {props.showBackButton && { })} icon={faArrowLeft} className="mr-4 cursor-pointer dark:hover:bg-gray-600 p-2 rounded-md" />} {props.title} + +
+ {props.children} +
} \ No newline at end of file diff --git a/frontend/react/src/ui/panels/header.tsx b/frontend/react/src/ui/panels/header.tsx index 05155f98..4b61e21f 100644 --- a/frontend/react/src/ui/panels/header.tsx +++ b/frontend/react/src/ui/panels/header.tsx @@ -7,7 +7,7 @@ import { OlDropdownItem, OlDropdown } from '../components/oldropdown'; import { OlLabelToggle } from '../components/ollabeltoggle'; import { getApp } from '../../olympusapp'; -export function Header(props) { +export function Header() { return {(appState) => @@ -18,7 +18,7 @@ export function Header(props) {
- + {}}/>
{ @@ -65,8 +65,8 @@ export function Header(props) { checked={!appState.mapHiddenTypes['neutral']} icon={faShield} className={"!text-gray-500"} />
- - + {}}> + {}} /> DCS Sat DCS Alt diff --git a/frontend/react/src/ui/panels/mainmenu.tsx b/frontend/react/src/ui/panels/mainmenu.tsx index 8f641a9d..a8c17bc3 100644 --- a/frontend/react/src/ui/panels/mainmenu.tsx +++ b/frontend/react/src/ui/panels/mainmenu.tsx @@ -5,8 +5,17 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { VERSION } from "../../olympusapp"; import { faGithub } from "@fortawesome/free-brands-svg-icons"; -export function MainMenu(props) { - return +export function MainMenu(props: { + open: boolean, + onClose: () => void, + children?: JSX.Element | JSX.Element[], +}) { + return
Version {VERSION}
Overview
diff --git a/frontend/react/src/ui/panels/sidebar.tsx b/frontend/react/src/ui/panels/sidebar.tsx index f97f7116..b5d04dad 100644 --- a/frontend/react/src/ui/panels/sidebar.tsx +++ b/frontend/react/src/ui/panels/sidebar.tsx @@ -4,7 +4,7 @@ import { faPlus, faGamepad, faRuler, faPencil, faListDots } from '@fortawesome/f import { EventsConsumer } from '../../eventscontext'; import { StateConsumer } from '../../statecontext'; -export function SideBar(props) { +export function SideBar() { return {(appState) => @@ -14,7 +14,7 @@ export function SideBar(props) {
- +
diff --git a/frontend/react/src/ui/panels/spawnmenu.tsx b/frontend/react/src/ui/panels/spawnmenu.tsx index 7baaa202..ff5e08c3 100644 --- a/frontend/react/src/ui/panels/spawnmenu.tsx +++ b/frontend/react/src/ui/panels/spawnmenu.tsx @@ -7,16 +7,35 @@ import { OlAccordion } from "../components/olaccordion"; import { getApp } from "../../olympusapp"; import { OlUnitEntryList } from "../components/olunitlistentry"; import { UnitSpawnMenu } from "./unitspawnmenu"; +import { UnitBlueprint } from "../../interfaces"; library.add(faPlus); -export function SpawnMenu(props) { - var [blueprint, setBlueprint] = useState(null); +function filterUnits(blueprints: {[key: string]: UnitBlueprint}, filterString: string) { + var filteredUnits = {}; + if (blueprints) { + Object.entries(blueprints).forEach(([key, value]) => { + if (value.enabled && (filterString === "" || value.label.includes(filterString))) + filteredUnits[key] = value; + }); + } + return filteredUnits; +} - const filteredAircraft = getApp()?.getAircraftDatabase()?.blueprints ?? {}; - const filteredHelicopters = getApp()?.getHelicopterDatabase()?.blueprints ?? {}; - const filteredNavyUnits = getApp()?.getNavyUnitDatabase()?.blueprints ?? {}; +export function SpawnMenu(props: { + open: boolean, + onClose: () => void, + children?: JSX.Element | JSX.Element[], +}) { + var [blueprint, setBlueprint] = useState(null as (null | UnitBlueprint)); + var [filterString, setFilterString] = useState(""); + /* Filter aircrafts, helicopters, and navyunits */ + const filteredAircraft = filterUnits(getApp()?.getAircraftDatabase()?.blueprints, filterString); + const filteredHelicopters = filterUnits(getApp()?.getHelicopterDatabase()?.blueprints, filterString); + const filteredNavyUnits = filterUnits(getApp()?.getNavyUnitDatabase()?.blueprints, filterString); + + /* Split ground units between air defence and all others */ var filteredAirDefense = {}; var filteredGroundUnits = {}; Object.keys(getApp()?.getGroundUnitDatabase()?.blueprints ?? {}).forEach((key) => { @@ -28,58 +47,64 @@ export function SpawnMenu(props) { filteredGroundUnits[key] = blueprint; } }); + filteredAirDefense = filterUnits(filteredAirDefense, filterString); + filteredGroundUnits = filterUnits(filteredGroundUnits, filterString); return setBlueprint(null)} + onBack={() => setBlueprint(null)} > - {!blueprint &&
- - -
- {Object.keys(filteredAircraft).map((key) => { - const blueprint = getApp().getAircraftDatabase().blueprints[key]; - return setBlueprint(blueprint)} /> - })} -
-
- -
- {Object.keys(filteredHelicopters).map((key) => { - return - })} -
-
- -
- {Object.keys(filteredAirDefense).map((key) => { - return - })} -
-
- -
- {Object.keys(filteredGroundUnits).map((key) => { - const blueprint = getApp().getGroundUnitDatabase().blueprints[key]; - return setBlueprint(blueprint)} /> - })} -
-
- -
- {Object.keys(filteredNavyUnits).map((key) => { - return - })} -
-
- + <> + {(blueprint === null) &&
+ setFilterString(ev.target.value)}/> + +
+ {Object.keys(filteredAircraft).map((key) => { + const blueprint = getApp().getAircraftDatabase().blueprints[key]; + return setBlueprint(blueprint)} /> + })} +
+
+ +
+ {Object.keys(filteredHelicopters).map((key) => { + const blueprint = getApp().getHelicopterDatabase().blueprints[key]; + return setBlueprint(blueprint)} /> + })} +
+
+ +
+ {Object.keys(filteredAirDefense).map((key) => { + const blueprint = getApp().getGroundUnitDatabase().blueprints[key]; + return setBlueprint(blueprint)} /> + })} +
+
+ +
+ {Object.keys(filteredGroundUnits).map((key) => { + const blueprint = getApp().getGroundUnitDatabase().blueprints[key]; + return setBlueprint(blueprint)} /> + })} +
+
+ +
+ {Object.keys(filteredNavyUnits).map((key) => { + const blueprint = getApp().getNavyUnitDatabase().blueprints[key]; + return setBlueprint(blueprint)} /> + })} +
+
+ - -
- } +
+
+ } - {blueprint && } + {!(blueprint === null) && } +
} \ No newline at end of file diff --git a/frontend/react/src/ui/panels/unitcontrolmenu.tsx b/frontend/react/src/ui/panels/unitcontrolmenu.tsx index 7df5c9a3..54afe927 100644 --- a/frontend/react/src/ui/panels/unitcontrolmenu.tsx +++ b/frontend/react/src/ui/panels/unitcontrolmenu.tsx @@ -3,61 +3,125 @@ import { Menu } from "./components/menu"; import { faGamepad } from '@fortawesome/free-solid-svg-icons'; import { library } from '@fortawesome/fontawesome-svg-core' import { Unit } from "../../unit/unit"; +import { OlLabelToggle } from "../components/ollabeltoggle"; +import { OlRangeSlider } from "../components/olrangeslider"; +import { getApp } from "../../olympusapp"; -library.add(faGamepad); +const defaultUnitControlPanelData = { + desiredAltitude: undefined as undefined | number, + desiredAltitudeType: undefined as undefined | boolean +} -export function UnitControlMenu(props) { - var [open, setOpen] = useState(false); - var [selectedUnits, setSelectedUnits] = useState([] as Unit[]); +export function UnitControlMenu() { + var [open, setOpen] = useState(false); + var [selectedUnits, setSelectedUnits] = useState([] as Unit[]); - document.addEventListener("unitsSelection", (ev: CustomEventInit) => { - setOpen(true); - setSelectedUnits(ev.detail as Unit[]) - }) + var [selectedUnitsData, setSelectedUnitsData] = useState(defaultUnitControlPanelData); + var [selectedUnitsRequestedData, setSelectedUnitsRequestedData] = useState(defaultUnitControlPanelData); - document.addEventListener("unitDeselection", (ev: CustomEventInit) => { + /* */ + const minAltitude = 0; + const maxAltitude = 60000; + const altitudeStep = 500; - }) + /* When a unit is selected, open the menu */ + document.addEventListener("unitsSelection", (ev: CustomEventInit) => { + setOpen(true); + setSelectedUnits(ev.detail as Unit[]) + }) - document.addEventListener("clearSelection", () => { - setOpen(false); - setSelectedUnits([]) - }) + /* When a unit is deselected, refresh the view */ + document.addEventListener("unitDeselection", (ev: CustomEventInit) => { - var unitOccurences = { - blue: {}, - red: {}, - neutral: {} - } + }) - selectedUnits.forEach((unit) => { - if (!(unit.getName() in unitOccurences[unit.getCoalition()])) - unitOccurences[unit.getCoalition()][unit.getName()] = 1; - else - unitOccurences[unit.getCoalition()][unit.getName()]++; - }) + /* When all units are selected clean the view */ + document.addEventListener("clearSelection", () => { + setOpen(false); + setSelectedUnits([]) + }) - return -
- { - <> - { - ['blue', 'red', 'neutral'].map((coalition) => { - return Object.keys(unitOccurences[coalition]).map((name) => { - return
- - {name} - - - x{unitOccurences[coalition][name]} - -
- }) - }) - } - - } -
+ document.addEventListener("unitUpdated", () => { -
+ }) + + /* Count how many units are selected of each type, divided by coalition */ + var unitOccurences = { + blue: {}, + red: {}, + neutral: {} + } + selectedUnits.forEach((unit) => { + if (!(unit.getName() in unitOccurences[unit.getCoalition()])) + unitOccurences[unit.getCoalition()][unit.getName()] = 1; + else + unitOccurences[unit.getCoalition()][unit.getName()]++; + }) + + return { }} + > +
+
+ { + <> + { + ['blue', 'red', 'neutral'].map((coalition) => { + return Object.keys(unitOccurences[coalition]).map((name) => { + return
+ + {name} + + + x{unitOccurences[coalition][name]} + +
+ }) + }) + } + + } +
+
+
+
+
+
+ Altitude + {`${selectedUnitsRequestedData.desiredAltitude} FT`} +
+ { + selectedUnits.forEach((unit) => { + unit.setAltitudeType((!selectedUnitsRequestedData.desiredAltitudeType) ? "AGL" : "ASL"); + setSelectedUnitsRequestedData({ + ...selectedUnitsRequestedData, + desiredAltitudeType: !selectedUnitsRequestedData.desiredAltitudeType + }) + }) + }} /> +
+ { + selectedUnits.forEach((unit) => { + unit.setAltitude(Number(ev.target.value)); + setSelectedUnitsRequestedData({ + ...selectedUnitsRequestedData, + desiredAltitude: Number(ev.target.value) + }) + }) + }} + value={selectedUnitsRequestedData.desiredAltitude} + min={minAltitude} + max={maxAltitude} + step={altitudeStep} /> +
+
+ +
} \ No newline at end of file diff --git a/frontend/react/src/ui/panels/unitspawnmenu.tsx b/frontend/react/src/ui/panels/unitspawnmenu.tsx index 7a1e8e0a..9a4e06eb 100644 --- a/frontend/react/src/ui/panels/unitspawnmenu.tsx +++ b/frontend/react/src/ui/panels/unitspawnmenu.tsx @@ -6,11 +6,25 @@ import { OlLabelToggle } from "../components/ollabeltoggle"; import { OlRangeSlider } from "../components/olrangeslider"; import { OlDropdownItem, OlDropdown } from '../components/oldropdown'; import { LoadoutBlueprint, UnitBlueprint } from "../../interfaces"; +import { Coalition } from "../../types/types"; -export function UnitSpawnMenu(props) { +export function UnitSpawnMenu(props: { + blueprint: UnitBlueprint +}) { + /* Compute the min and max values depending on the unit type */ + const minNumber = 1; + const maxNumber = 4; + const minAltitude = 0; + const maxAltitude = 30000; + const altitudeStep = 500; + + /* State initialization */ + var [spawnCoalition, setSpawnCoalition] = useState("blue" as Coalition); + var [spawnNumber, setSpawnNumber] = useState(1); var [spawnRole, setSpawnRole] = useState(""); var [spawnLoadoutName, setSpawnLoadout] = useState(""); - var [spawnAltitude, setSpawnAltitude] = useState(1000); + var [spawnAltitude, setSpawnAltitude] = useState((maxAltitude - minAltitude) / 2); + var [spawnAltitudeType, setSpawnAltitudeType] = useState(false); /* Get a list of all the roles */ const roles: string[] = []; @@ -31,15 +45,28 @@ export function UnitSpawnMenu(props) { /* Initialize the loadout */ spawnLoadoutName === "" && loadouts.length > 0 && setSpawnLoadout(loadouts[0].name) - - const spawnLoadout = props.blueprint.loadouts.find((loadout) => { return loadout.name === spawnLoadoutName; }) + const spawnLoadout = props.blueprint.loadouts?.find((loadout) => { return loadout.name === spawnLoadoutName; }) return
- +
- - + { + spawnCoalition === 'blue' && setSpawnCoalition('neutral'); + spawnCoalition === 'neutral' && setSpawnCoalition('red'); + spawnCoalition === 'red' && setSpawnCoalition('blue'); + }} + /> + { setSpawnNumber(Math.max(minNumber, spawnNumber - 1)) }} + onIncrease={() => { setSpawnNumber(Math.min(maxNumber, spawnNumber + 1)) }} + onChange={(ev) => { !isNaN(Number(ev.target.value)) && setSpawnNumber(Math.max(minNumber, Math.min(maxNumber, Number(ev.target.value)))) }} + />
@@ -47,9 +74,9 @@ export function UnitSpawnMenu(props) { Altitude {`${spawnAltitude} FT`}
- + setSpawnAltitudeType(!spawnAltitudeType)} />
- + setSpawnAltitude(Number(ev.target.value))} value={spawnAltitude} min={minAltitude} max={maxAltitude} step={altitudeStep} />
diff --git a/frontend/react/src/ui/ui.tsx b/frontend/react/src/ui/ui.tsx index c144020e..7a6692e2 100644 --- a/frontend/react/src/ui/ui.tsx +++ b/frontend/react/src/ui/ui.tsx @@ -23,7 +23,7 @@ export type OlympusState = { mapOptions: MapOptions; } -export function UI(props) { +export function UI() { var [mainMenuVisible, setMainMenuVisible] = useState(false); var [spawnMenuVisible, setSpawnMenuVisible] = useState(false); var [unitControlMenuVisible, setUnitControlMenuVisible] = useState(false); @@ -78,9 +78,9 @@ export function UI(props) {
- setMainMenuVisible(false)} /> - setSpawnMenuVisible(false)} /> - setUnitControlMenuVisible(false)} /> + setMainMenuVisible(false)} /> + setSpawnMenuVisible(false)} /> +
diff --git a/frontend/react/src/unit/unitsmanager.ts b/frontend/react/src/unit/unitsmanager.ts index b908cfec..4fcba3b9 100644 --- a/frontend/react/src/unit/unitsmanager.ts +++ b/frontend/react/src/unit/unitsmanager.ts @@ -1041,7 +1041,7 @@ export class UnitsManager { * @param variableGetter CallableFunction that returns the requested variable. Example: getUnitsVariable((unit: Unit) => unit.getName(), foo) will return a string value if all the units have the same name, otherwise it will return undefined. * @returns The value of the variable if all units have the same value, else undefined */ - getSelectedUnitsVariable(variableGetter: CallableFunction) { + getSelectedUnitsVariable(variableGetter: (unit: Unit) => any) { return this.getUnitsVariable(variableGetter, this.getSelectedUnits()); };