diff --git a/backend/src/middleware/validScenarioId.js b/backend/src/middleware/validScenarioId.js new file mode 100644 index 00000000..d210d68a --- /dev/null +++ b/backend/src/middleware/validScenarioId.js @@ -0,0 +1,18 @@ +import mongoose from "mongoose"; + +const HTTP_BAD_REQUEST = 400; + +/** + * Checks if the scenarioId is valid + */ +export default async function validScenarioId(req, res, next) { + if ( + req.params?.scenarioId && + !mongoose.isValidObjectId(req.params.scenarioId) + ) { + res.status(HTTP_BAD_REQUEST).json({ error: "Invalid scenario ID." }); + return; + } + + next(); +} diff --git a/backend/src/routes/api/group.js b/backend/src/routes/api/group.js index fc828fc5..80fd0d80 100644 --- a/backend/src/routes/api/group.js +++ b/backend/src/routes/api/group.js @@ -1,21 +1,23 @@ import { Router } from "express"; - import { + createGroup, getCurrentScene, getGroup, - createGroup, getGroupByScenarioId, } from "../../db/daos/groupDao.js"; import { retrieveRoleList, updateRoleList } from "../../db/daos/scenarioDao.js"; import Group from "../../db/models/group.js"; +import validScenarioId from "../../middleware/validScenarioId.js"; + const router = Router(); const HTTP_OK = 200; const HTTP_CONFLICT = 409; const HTTP_NO_CONTENT = 204; const HTTP_NOT_FOUND = 404; +const HTTP_BAD_REQUEST = 400; // get the groups assigned to a scenario router.get("/scenario/:scenarioId", async (req, res) => { @@ -39,6 +41,19 @@ router.get("/path/:groupId", async (req, res) => { } }); +// get a group by its id +router.get("/retrieve/:groupId", async (req, res) => { + const { groupId } = req.params; + const group = await getGroup(groupId); + if (!group) { + return res.status(HTTP_NOT_FOUND).json({ error: "Group not found" }); + } + return res.status(HTTP_OK).json(group); +}); + +export default router; + +router.use("/:scenarioId", validScenarioId); // create a new group router.post("/:scenarioId", async (req, res) => { const { groupList, roleList } = req.body; @@ -98,20 +113,7 @@ router.post("/:scenarioId", async (req, res) => { router.get("/:scenarioId/roleList", async (req, res) => { const { scenarioId } = req.params; - const roleList = await retrieveRoleList(scenarioId); res.status(HTTP_OK).json(roleList); }); - -// get a group by its id -router.get("/retrieve/:groupId", async (req, res) => { - const { groupId } = req.params; - const group = await getGroup(groupId); - if (!group) { - return res.status(HTTP_NOT_FOUND).json({ error: "Group not found" }); - } - return res.status(HTTP_OK).json(group); -}); - -export default router; diff --git a/backend/src/routes/api/scenario.js b/backend/src/routes/api/scenario.js index 0bc6ee6a..4186559e 100644 --- a/backend/src/routes/api/scenario.js +++ b/backend/src/routes/api/scenario.js @@ -1,13 +1,15 @@ import { Router } from "express"; import auth from "../../middleware/firebaseAuth.js"; import scenarioAuth from "../../middleware/scenarioAuth.js"; +import validScenarioId from "../../middleware/validScenarioId.js"; import { createScenario, - retrieveScenarioList, - updateScenario, deleteScenario, + retrieveScenario, + retrieveScenarioList, updateDurations, + updateScenario, } from "../../db/daos/scenarioDao.js"; import { retrieveAssignedScenarioList } from "../../db/daos/userDao.js"; @@ -48,8 +50,15 @@ router.post("/", async (req, res) => { }); // Apply scenario auth middleware +router.use("/:scenarioId", validScenarioId); router.use("/:scenarioId", scenarioAuth); +// Get a scenario by id. +router.get("/:scenarioId", async (req, res) => { + const scenario = await retrieveScenario(req.params.scenarioId); + res.status(HTTP_OK).json(scenario); +}); + // Update a scenario by a user router.put("/:scenarioId", async (req, res) => { const { name, duration } = req.body; diff --git a/backend/src/routes/api/scene.js b/backend/src/routes/api/scene.js index 85e5f0f1..02bed3ed 100644 --- a/backend/src/routes/api/scene.js +++ b/backend/src/routes/api/scene.js @@ -2,15 +2,16 @@ import { Router } from "express"; import { createScene, - retrieveSceneList, - retrieveScene, - updateScene, deleteScene, duplicateScene, incrementVisisted, + retrieveScene, + retrieveSceneList, + updateScene, } from "../../db/daos/sceneDao.js"; import auth from "../../middleware/firebaseAuth.js"; import scenarioAuth from "../../middleware/scenarioAuth.js"; +import validScenarioId from "../../middleware/validScenarioId.js"; const router = Router({ mergeParams: true }); @@ -20,6 +21,7 @@ const HTTP_NOT_FOUND = 404; // Apply auth middleware to all routes below this point router.use(auth); // Apply scenario auth middleware +router.use(validScenarioId); router.use(scenarioAuth); // Get scene infromation diff --git a/frontend/index.html b/frontend/index.html index b9e3f512..74ac41ea 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,10 +1,18 @@ - - + + + + Virtual Patient System - UoA diff --git a/frontend/src/components/DashedCard.jsx b/frontend/src/components/DashedCard.jsx index fdce33d9..1d8148ce 100644 --- a/frontend/src/components/DashedCard.jsx +++ b/frontend/src/components/DashedCard.jsx @@ -1,5 +1,6 @@ import { Box } from "@material-ui/core"; -import styles from "./ListContainer/ListContainer.module.scss"; + +import AddRoundedIcon from "@mui/icons-material/AddRounded"; /** * Component used to represent a card with a dashed border, used to indicate that a new card can be created. @@ -16,27 +17,27 @@ import styles from "./ListContainer/ListContainer.module.scss"; */ export default function DashedCard({ onClick }) { return ( -
+
-
-
+ className="cursor-pointer flex justify-center items-center overflow-hidden rounded-xl border-2 border-dashed border-slate-400 bg-slate-100" + > + +
-

Create New Scene

); } diff --git a/frontend/src/components/DeleteModal.jsx b/frontend/src/components/DeleteModal.jsx index 7f4654eb..7da9a243 100644 --- a/frontend/src/components/DeleteModal.jsx +++ b/frontend/src/components/DeleteModal.jsx @@ -1,5 +1,4 @@ -import { useState } from "react"; -import DeleteButton from "./DeleteButton"; +import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded"; function DeleteModal({ onDelete, currentScenario }) { const handleClickOpen = () => { @@ -18,10 +17,11 @@ function DeleteModal({ onDelete, currentScenario }) { return (
diff --git a/frontend/src/components/HelpButton.jsx b/frontend/src/components/HelpButton.jsx index 5a814777..445afb38 100644 --- a/frontend/src/components/HelpButton.jsx +++ b/frontend/src/components/HelpButton.jsx @@ -1,4 +1,3 @@ -import Button from "@material-ui/core/Button"; import HelpIcon from "@material-ui/icons/Help"; import { useState } from "react"; diff --git a/frontend/src/components/ListContainer/ListContainer.jsx b/frontend/src/components/ListContainer/ListContainer.jsx deleted file mode 100644 index 979969ef..00000000 --- a/frontend/src/components/ListContainer/ListContainer.jsx +++ /dev/null @@ -1,232 +0,0 @@ -import { useState } from "react"; - -import { Box } from "@material-ui/core"; -import ImageList from "@material-ui/core/ImageList"; -import ImageListItem from "@material-ui/core/ImageListItem"; - -import Thumbnail from "features/authoring/components/Thumbnail"; -import DashedCard from "../DashedCard"; - -import styles from "./ListContainer.module.scss"; -import useStyles from "./component.styles"; - -/** - * Component used to display cards in a list format for scenario and scene selection. - * - * @component - * @example - * const data = [ ... ] - * const wide = true - * const sceneSelectionPage = false - * const scenarioId = "1ef4cD1wsd676dS" - * function onItemSelected() { - * console.log("Selected.") - * } - * function onItemDoubleClick() { - * console.log("Double clicked.") - * } - * function addCard() { - * console.log("Card Added.") - * } - * function onItemBlur() { - * console.log("Item Blurred.") - * } - * return ( - * - * ) - */ -export default function ListContainer({ - data, // could be scenarios or scenes data - assignedScenarios, - onItemSelected, - onItemDoubleClick, - wide, - addCard, - onItemBlur, - sceneSelectionPage, - scenarioId, - invalidNameId, -}) { - const classes = useStyles(); - const [selected, setSelected] = useState(); - const columns = wide ? 5 : 4; - - /** Function which executes when an image in the image list is clicked. */ - const onItemClick = (event, item) => { - if (event.detail === 2) { - onItemDoubleClick(item); - } else { - setSelected(item._id); - onItemSelected(item); - } - }; - - /** Function which executes when an image in the image list is right-clicked. Select item. */ - const onItemRightClick = (item) => { - setSelected(item._id); - onItemSelected(item); - }; - - return ( - <> -
- {!sceneSelectionPage && ( -

Created scenarios

- )} - - - {addCard ? ( - - - - ) : null} - {data && data.length > 0 - ? data.map((item) => ( - onItemClick(event, item)} - onContextMenu={() => onItemRightClick(item)} - > -
- - {sceneSelectionPage ? ( - - ) : ( - - )} - - -
- {invalidNameId === item._id && ( - invalid null name - )} -
- )) - : null} -
- - {assignedScenarios && assignedScenarios.length ? ( - <> - {!sceneSelectionPage && ( -

Assigned scenarios

- )} - - - {addCard ? ( - - - - ) : null} - {assignedScenarios && assignedScenarios.length > 0 - ? assignedScenarios.map((item) => ( - { - window.open(`/play/${item._id}`, "_blank"); - }} - > -
- - - - -
- {invalidNameId === item._id && ( - invalid null name - )} -
- )) - : null} -
- - ) : null} -
- - ); -} diff --git a/frontend/src/components/ListContainer/ThumbnailList.jsx b/frontend/src/components/ListContainer/ThumbnailList.jsx new file mode 100644 index 00000000..1ff3ece9 --- /dev/null +++ b/frontend/src/components/ListContainer/ThumbnailList.jsx @@ -0,0 +1,140 @@ +import { useState } from "react"; + +import { Box } from "@material-ui/core"; +import ImageList from "@material-ui/core/ImageList"; +import ImageListItem from "@material-ui/core/ImageListItem"; + +import Thumbnail from "features/authoring/components/Thumbnail"; +import DashedCard from "../DashedCard"; + +import styles from "./ThumbnailList.module.scss"; +import useStyles from "./component.styles"; + +/** + * Component used to display cards in a list format for scenario and scene selection. + * + * @component + * @example + * const data = [ ... ] + * function onItemSelected() { + * console.log("Selected.") + * } + * function onItemDoubleClick() { + * console.log("Double clicked.") + * } + * function addCard() { + * console.log("Card Added.") + * } + * function onItemBlur() { + * console.log("Item Blurred.") + * } + * return ( + * + * ) + */ +export default function ThumbnailList({ + data, // could be scenarios or scenes data, but expects components. + invalidNameId, + highlightOnSelect = true, // Whether or not to highlight the card border on select. + addCard, + onItemBlur, + onItemSelected = () => {}, + onItemDoubleClick = () => {}, +}) { + const classes = useStyles(); + const [selected, setSelected] = useState(); + + /** Function which executes when an image in the image list is clicked. */ + const onItemClick = (event, item) => { + if (event.detail === 2) { + onItemDoubleClick(item); + } else { + if (highlightOnSelect) { + setSelected(item._id); + } + onItemSelected(item); + } + }; + + /** Function which executes when an image in the image list is right-clicked. Select item. */ + const onItemRightClick = (item) => { + setSelected(item._id); + onItemSelected(item); + }; + + return ( + <> +
+ + {addCard ? ( + + + + ) : null} + {data && data.length > 0 + ? data.map((item) => ( + onItemClick(event, item)} + onContextMenu={() => onItemRightClick(item)} + > +
+ + + +
+ {onItemBlur ? ( + + ) : ( +

+ {item.name} +

+ )} +
+
+ {invalidNameId === item._id && ( + invalid null name + )} +
+ )) + : null} +
+
+ + ); +} diff --git a/frontend/src/components/ListContainer/ListContainer.module.scss b/frontend/src/components/ListContainer/ThumbnailList.module.scss similarity index 76% rename from frontend/src/components/ListContainer/ListContainer.module.scss rename to frontend/src/components/ListContainer/ThumbnailList.module.scss index 09450471..a06d11fd 100644 --- a/frontend/src/components/ListContainer/ListContainer.module.scss +++ b/frontend/src/components/ListContainer/ThumbnailList.module.scss @@ -1,20 +1,11 @@ -.scenarioListContainer { - width: 85vw; - height: 100vh; +.listContainer { + width: 100%; overflow-y: scroll; overflow-x: hidden; padding-right: 30px; padding-left: 30px; } -.scenarioListContainerWide { - flex-grow: 1; - width: 100vw; - height: 90vh; - overflow-y: scroll; - overflow-x: hidden; -} - .imageListItem { margin-top: 20px; margin-left: 3px; @@ -22,13 +13,6 @@ cursor: pointer; } -.imageListItemWide { - padding-top: 20px; - padding-left: 20px; - padding-right: 20px; - cursor: pointer; -} - .crossHorizontalLine { position: absolute; top: calc(50% - 5px); diff --git a/frontend/src/components/ListContainer/component.styles.js b/frontend/src/components/ListContainer/component.styles.js index 0800ef87..65566aee 100644 --- a/frontend/src/components/ListContainer/component.styles.js +++ b/frontend/src/components/ListContainer/component.styles.js @@ -4,12 +4,6 @@ import { makeStyles } from "@material-ui/core"; * This file contains all the styles used to override material-ui components which are being used within a component. */ -const useStyles = makeStyles({ - listContainerItem: { - height: "250px !important", - paddingBottom: "0 !important", - textAlign: "center", - }, -}); +const useStyles = makeStyles({}); export default useStyles; diff --git a/frontend/src/components/ScreenContainer/ScreenContainer.module.scss b/frontend/src/components/ScreenContainer/ScreenContainer.module.scss index 348c68b2..79ec91fb 100644 --- a/frontend/src/components/ScreenContainer/ScreenContainer.module.scss +++ b/frontend/src/components/ScreenContainer/ScreenContainer.module.scss @@ -1,6 +1,7 @@ .rowContainer { display: flex; flex-direction: row; + height: 100%; } .colContainer { diff --git a/frontend/src/components/ShareModal/ShareModal.jsx b/frontend/src/components/ShareModal/ShareModal.jsx index 597a96a7..026572d8 100644 --- a/frontend/src/components/ShareModal/ShareModal.jsx +++ b/frontend/src/components/ShareModal/ShareModal.jsx @@ -1,7 +1,5 @@ -import Button from "@material-ui/core/Button"; import { useContext, useState } from "react"; import ScenarioContext from "../../context/ScenarioContext"; -import styles from "./ShareModal.module.scss"; /** * Component used to a display a share model on the screen, conisting of a copiable link and a button. @@ -19,7 +17,7 @@ import styles from "./ShareModal.module.scss"; export default function ShareModal({ isOpen, handleClose }) { const { currentScenario } = useContext(ScenarioContext); const [copySuccess, setCopySuccess] = useState(false); - const url = `${window.location.origin}/play/${currentScenario._id}`; + const url = `${window.location.origin}/play/${currentScenario?._id}`; /** Function which executes when the modal is closed. */ function onClose() { diff --git a/frontend/src/components/SideBar/SideBar.jsx b/frontend/src/components/SideBar/SideBar.jsx index 5dbd2283..9f87169d 100644 --- a/frontend/src/components/SideBar/SideBar.jsx +++ b/frontend/src/components/SideBar/SideBar.jsx @@ -1,14 +1,17 @@ -import Button from "@material-ui/core/Button"; import { useContext, useState } from "react"; -import { Link, Router, useHistory } from "react-router-dom"; +import { useHistory } from "react-router-dom"; import AuthenticationContext from "../../context/AuthenticationContext"; import ScenarioContext from "../../context/ScenarioContext"; -import AccessLevel from "../../enums/route.access.level"; import { useDelete, usePost } from "../../hooks/crudHooks"; -import styles from "./SideBar.module.scss"; -import HelpButton from "../HelpButton"; import CreateScenerioCard from "../CreateScenarioCard/CreateScenarioCard"; import DeleteModal from "../DeleteModal"; +import HelpButton from "../HelpButton"; +import styles from "./SideBar.module.scss"; + +import AddCircleOutlineRoundedIcon from "@mui/icons-material/AddCircleOutlineRounded"; +import EditRoundedIcon from "@mui/icons-material/EditRounded"; +import LogoutIcon from "@mui/icons-material/Logout"; +import PlayArrowRoundedIcon from "@mui/icons-material/PlayArrowRounded"; /** * Component used for navigation and executing actions located at the left side of the screen. @@ -48,9 +51,6 @@ export default function SideBar() { history.push(`/scenario/${newScenario._id}`); } - /** Calls backend end point to switch to the lecturer's dashboard */ - function openDashboard() {} - /** Calls backend end point to delete a scenario. */ async function deleteScenario() { await useDelete(`/api/scenario/${currentScenario._id}`, getUserIdToken); @@ -81,74 +81,74 @@ export default function SideBar() { onClose={handleCloseCard} /> )} -
- University of Auckland Logo -
    -
  • - -
  • - {VpsUser.role === AccessLevel.STAFF ? ( + + {/* Main sidebar */} +
    + {/* UoA logo container */} +
    + University of Auckland Logo +
    + + {/* Button containers */} +
    +
    • +
    • +
    • + +
    • +
    • +
    • - ) : ( - "" - )} -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    +
  • + +
  • +
+ +
    +
  • + +
  • +
  • + +
  • +
+
); diff --git a/frontend/src/components/SideBar/SideBar.module.scss b/frontend/src/components/SideBar/SideBar.module.scss index 16b56d57..92dd93ea 100644 --- a/frontend/src/components/SideBar/SideBar.module.scss +++ b/frontend/src/components/SideBar/SideBar.module.scss @@ -1,36 +1,30 @@ .sideBar { - min-width: 250px; - width: 15vw; - height: 100vh; - background-color: #035084; - text-align: center; + min-width: 16rem /* 240px */; + width: 16rem /* 240px */; + height: 100%; display: flex; flex-direction: column; align-items: center; - --tooltip-color: black; - - .logo { - margin: 10px; - width: 230px; - } .sideBarList { list-style: none; - padding: 0; - width: 12.5rem; - - li:not(:last-child) { - margin-bottom: 10%; - } + width: 100%; + padding-left: 1.75rem /* 28px */; + padding-right: 1.75rem /* 28px */; + display: flex; + flex-direction: column; + justify-content: center; + gap: 0.75rem /* 12px */; .buttonDisabled { opacity: 0.3; } - } - .helpButton { - & > button { + li > button { width: 100%; + letter-spacing: 0.05em; + font-weight: 300; + text-align: left; } } diff --git a/frontend/src/context/ScenarioContextProvider.jsx b/frontend/src/context/ScenarioContextProvider.jsx index 35eed6a2..e05c1e2c 100644 --- a/frontend/src/context/ScenarioContextProvider.jsx +++ b/frontend/src/context/ScenarioContextProvider.jsx @@ -1,6 +1,8 @@ -import { useState } from "react"; +import { useContext, useEffect, useState } from "react"; + import { useGet } from "../hooks/crudHooks"; import useLocalStorage from "../hooks/useLocalStorage"; +import AuthenticationContext from "./AuthenticationContext"; import ScenarioContext from "./ScenarioContext"; /** @@ -8,6 +10,7 @@ import ScenarioContext from "./ScenarioContext"; * ScenarioContextProvider allows access to scenario info and the refetch function */ export default function ScenarioContextProvider({ children }) { + const { user } = useContext(AuthenticationContext); const [currentScenario, setCurrentScenario] = useLocalStorage( "currentScenario", null @@ -16,20 +19,30 @@ export default function ScenarioContextProvider({ children }) { const [assignedScenarios, setAssignedScenarios] = useState(); const [roleList, setRoleList] = useState(); - const { reFetch } = useGet(`api/scenario`, setScenarios, true); + const { reFetch } = useGet(`api/scenario`, setScenarios, true, !user); const { reFetch: reFetch2 } = useGet( `api/scenario/assigned`, setAssignedScenarios, - true + true, + !user ); - useGet( + const { reFetch: reFetch3 } = useGet( `api/group/${currentScenario?._id}/roleList`, setRoleList, true, !currentScenario // Skip request if there is no current scenario. ); + // We may load before the auth is ready, refetch if we did. + useEffect(() => { + if (user) { + reFetch(); + reFetch2(); + reFetch3(); + } + }, [user]); + return ( { + reFetch(); + }, [user]); /** * monitorChange variable is used to determine @@ -67,7 +70,7 @@ export default function SceneContextProvider({ children }) { value={{ scenes, setScenes, - reFetch: getScenes?.reFetch, + reFetch, currentScene, setCurrentScene, hasChange, diff --git a/frontend/src/features/scenarioSelection/ScenarioSelectionPage.jsx b/frontend/src/features/scenarioSelection/ScenarioSelectionPage.jsx index 2f2951b0..b6d8c956 100644 --- a/frontend/src/features/scenarioSelection/ScenarioSelectionPage.jsx +++ b/frontend/src/features/scenarioSelection/ScenarioSelectionPage.jsx @@ -2,7 +2,7 @@ import MenuItem from "@material-ui/core/MenuItem"; import { useContext, useEffect, useState } from "react"; import { useHistory } from "react-router-dom"; import ContextMenu from "../../components/ContextMenu"; -import ListContainer from "../../components/ListContainer/ListContainer"; +import ThumbnailList from "../../components/ListContainer/ThumbnailList"; import ScreenContainer from "../../components/ScreenContainer/ScreenContainer"; import SideBar from "../../components/SideBar/SideBar"; import AuthenticationContext from "../../context/AuthenticationContext"; @@ -10,14 +10,17 @@ import ScenarioContext from "../../context/ScenarioContext"; import AccessLevel from "../../enums/route.access.level"; import { useDelete, usePut } from "../../hooks/crudHooks"; +import MovieFilterRoundedIcon from "@mui/icons-material/MovieFilterRounded"; +import TheatersRoundedIcon from "@mui/icons-material/TheatersRounded"; + /** * Page that shows the user's existing scenarios. * * @container */ -export default function ScenarioSelectionPage({ data = null }) { +export default function ScenarioSelectionPage() { const { - scenarios, + scenarios: userScenarios, reFetch, assignedScenarios, reFetch2, @@ -103,7 +106,7 @@ export default function ScenarioSelectionPage({ data = null }) { return ( -
+
- + + {/* Scenario List */} +
+ {/* List of scenarios created by the logged-in user */} + {userScenarios && ( +
+

+ Your Scenarios +

+ +
+ { + scenario.components = scenario.thumbnail?.components || []; + return scenario; + })} + onItemSelected={setCurrentScenario} + onItemDoubleClick={editScenario} + onItemBlur={changeScenarioName} + invalidNameId={invalidNameId} + /> +
+
+ )} + + {/* List of scenarios assigned to the logged-in user */} + {assignedScenarios && ( +
+

+ Assigned Scenarios +

+ { + scenario.components = scenario.thumbnail?.components || []; + return scenario; + })} + onItemSelected={(scenario) => { + // For assigned scenarios, play the scenario on click. + window.open(`/play/${scenario._id}`, "_blank"); + }} + invalidNameId={invalidNameId} + highlightOnSelect={false} + /> +
+ )} +
); diff --git a/frontend/src/features/sceneSelection/SceneSelectionPage.jsx b/frontend/src/features/sceneSelection/SceneSelectionPage.jsx index e048ebee..3d1561f2 100644 --- a/frontend/src/features/sceneSelection/SceneSelectionPage.jsx +++ b/frontend/src/features/sceneSelection/SceneSelectionPage.jsx @@ -1,5 +1,5 @@ -import { Button, Divider, MenuItem } from "@material-ui/core"; -import ListContainer from "components/ListContainer/ListContainer"; +import { Divider, MenuItem } from "@material-ui/core"; +import ThumbnailList from "components/ListContainer/ThumbnailList"; import Papa from "papaparse"; import { useContext, useEffect, useRef, useState } from "react"; import { @@ -16,9 +16,16 @@ import ShareModal from "../../components/ShareModal/ShareModal"; import TopBar from "../../components/TopBar/TopBar"; import AuthenticationContext from "../../context/AuthenticationContext"; import AuthoringToolContextProvider from "../../context/AuthoringToolContextProvider"; +import ScenarioContext from "../../context/ScenarioContext"; import SceneContext from "../../context/SceneContext"; import AccessLevel from "../../enums/route.access.level"; -import { useDelete, usePatch, usePost, usePut } from "../../hooks/crudHooks"; +import { + useDelete, + useGet, + usePatch, + usePost, + usePut, +} from "../../hooks/crudHooks"; import AuthoringToolPage from "../authoring/AuthoringToolPage"; // !! this should be handled by the backend instead @@ -35,14 +42,23 @@ function generateUID() { * * @container */ -export function SceneSelectionPage({ data = null }) { +export function SceneSelectionPage() { const [isShareModalOpen, setShareModalOpen] = useState(false); const { scenarioId } = useParams(); const { url } = useRouteMatch(); const history = useHistory(); + const { currentScenario, setCurrentScenario } = useContext(ScenarioContext); const { scenes, currentScene, setCurrentScene, reFetch } = useContext(SceneContext); - const { getUserIdToken, VpsUser } = useContext(AuthenticationContext); + const { user, getUserIdToken, VpsUser } = useContext(AuthenticationContext); + + // Retrieve scenario on load + useGet( + `api/scenario/${scenarioId}`, + setCurrentScenario, + true, + !(user && (!currentScenario || currentScenario?._id != scenarioId)) + ); // File input is a hidden input element that is activated via a click handler // This allows us to have an UI button that acts like a file element. @@ -228,17 +244,17 @@ export function SceneSelectionPage({ data = null }) { {/* On top of the action button available in the top menu bar, we also override user's rightclick context menu to offer the same functionality. */} -
+
{/* Scene list */} - diff --git a/frontend/src/hooks/crudHooks.jsx b/frontend/src/hooks/crudHooks.jsx index 1e5f9114..27fa5dfe 100644 --- a/frontend/src/hooks/crudHooks.jsx +++ b/frontend/src/hooks/crudHooks.jsx @@ -233,6 +233,8 @@ export function useGet(url, setData, requireAuth = true, skipRequest = false) { } useEffect(() => { + let isMounted = true; + async function fetchData() { let hasError = false; setLoading(true); @@ -252,7 +254,7 @@ export function useGet(url, setData, requireAuth = true, skipRequest = false) { hasError = isRealError(err); }); - if (!hasError) { + if (!hasError && isMounted) { setData(response.data); } @@ -263,7 +265,11 @@ export function useGet(url, setData, requireAuth = true, skipRequest = false) { if (!skipRequest) { fetchData(); } - }, [url, version]); + + return () => { + isMounted = false; + }; + }, [url, skipRequest, version]); return { isLoading, reFetch }; } diff --git a/frontend/src/index.css b/frontend/src/index.css index 109fb1e4..47852938 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -5,7 +5,7 @@ /* MonaSans - licensed under OFL. See https://github.com/github/mona-sans/blob/main/LICENSE */ @font-face { font-family: "MonaSans"; - src: url("fonts/MonaSans.woff2") format("woff2"); + src: url("/fonts/MonaSans.woff2") format("woff2"); } /* Pass down viewport width/height to children */ diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 4271e3f5..6a344b3a 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -18,6 +18,7 @@ module.exports = { themes: [ { VPSTheme: { + ...require("daisyui/src/theming/themes")["emerald"], primary: "#fafafa", secondary: "#035084", error: "#c13216",