From 66726876c38f100efafa9e43d897097a61c3926c Mon Sep 17 00:00:00 2001 From: Abigail Alexander Date: Mon, 27 Jul 2026 15:24:22 +0100 Subject: [PATCH 1/4] Add ability to save Quick Screen to local storage --- src/components/QuickScreenSettings.tsx | 144 ------------ src/components/QuickScreens.tsx | 53 ----- src/components/QuickScreens/Display.tsx | 66 ++++++ src/components/QuickScreens/Settings.tsx | 209 ++++++++++++++++++ .../QuickScreens/StorageBrowser.tsx | 153 +++++++++++++ src/routes/QuickScreensPage.tsx | 4 +- .../components/QuickScreenSettings.test.tsx | 2 +- src/tests/components/QuickScreens.test.tsx | 2 +- 8 files changed, 432 insertions(+), 201 deletions(-) delete mode 100644 src/components/QuickScreenSettings.tsx delete mode 100644 src/components/QuickScreens.tsx create mode 100644 src/components/QuickScreens/Display.tsx create mode 100644 src/components/QuickScreens/Settings.tsx create mode 100644 src/components/QuickScreens/StorageBrowser.tsx diff --git a/src/components/QuickScreenSettings.tsx b/src/components/QuickScreenSettings.tsx deleted file mode 100644 index 45e44df..0000000 --- a/src/components/QuickScreenSettings.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { styled, useTheme } from "@mui/material/styles"; -import MuiDrawer from "@mui/material/Drawer"; -import List from "@mui/material/List"; -import ListItem from "@mui/material/ListItem"; -import ListItemButton from "@mui/material/ListItemButton"; -import ListItemIcon from "@mui/material/ListItemIcon"; -import AddIcon from "@mui/icons-material/Add"; -import LibraryAddIcon from "@mui/icons-material/LibraryAdd"; -import SaveIcon from "@mui/icons-material/Save"; -import UploadFileIcon from "@mui/icons-material/UploadFile"; -import DeleteIcon from "@mui/icons-material/Delete"; -import { ListItemText, Tooltip } from "@mui/material"; -import { useNavigate } from "react-router"; - -const NEW_QUICK_SCREEN = { - path: "/new.bob", - macros: {}, - defaultProtocol: "ca" -}; - -const Drawer = styled(MuiDrawer)(() => ({ - overflowX: "hidden", - width: "3%", - minWidth: "50px", - "& .MuiPaper-root": { - position: "relative", - overflowX: "hidden" - } -})); - -export default function QuickScreenSettings() { - const theme = useTheme(); - const navigate = useNavigate(); - - /** - * Loads a new blank quick screen - */ - const onClickNew = () => { - //Change to a blank Quick Screen - navigate("/quick-screens/", { - state: { pageState: { quickScreen: NEW_QUICK_SCREEN } }, - replace: true - }); - }; - - /** - * Adds a new screen to the current view to draw components from - */ - const onClickAdd = () => { - console.log("clicked add"); - }; - - /** - * Saves the current quick screen to local storage - */ - const onClickSave = () => { - console.log("clicked save"); - }; - - /** - * Loads a quick screen from local storage - */ - const onClickLoad = () => { - console.log("clicked load"); - }; - - /** - * Loads a quick screen from local storage - */ - const onClickDelete = () => { - console.log("clicked delete"); - }; - - const SETTINGS_LIST = [ - { - name: "New", - text: "Create new blank Quick Screen", - icon: , - onClick: onClickNew - }, - { - name: "Add", - text: "Add a .bob file to the view", - icon: , - onClick: onClickAdd - }, - { - name: "Save", - text: "Save the current Quick Screen", - icon: , - onClick: onClickSave - }, - { - name: "Load", - text: "Load a Quick Screen", - icon: , - onClick: onClickLoad - }, - { - name: "Delete", - text: "Discard the current Quick Screen", - icon: , - onClick: onClickDelete - } - ]; - - return ( - - - {SETTINGS_LIST.map(item => ( - - - - - {item.icon} - - - - - - ))} - - - ); -} diff --git a/src/components/QuickScreens.tsx b/src/components/QuickScreens.tsx deleted file mode 100644 index f120802..0000000 --- a/src/components/QuickScreens.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { Box, Paper as MuiPaper, styled, Typography } from "@mui/material"; -import { - DynamicPageWidget, - newRelativePosition -} from "@diamondlightsource/cs-web-lib"; -import { - useWindowWidth, - APP_BAR_HEIGHT, - useWindowHeight -} from "../utils/helper"; -import { useRef } from "react"; -import QuickScreenSettings from "./QuickScreenSettings"; -import { useLocation } from "react-router"; - -const Paper = styled(MuiPaper)(({ theme }) => ({ - height: `calc(${useWindowHeight()}px - ${APP_BAR_HEIGHT}px - 50px)`, - margin: `calc(${APP_BAR_HEIGHT}px + 15px) 5px 5px 5px`, - width: `calc(${useWindowWidth()}px - 10px - ${theme.spacing(7)} - 8px)` -})); - -export default function QuickScreens() { - const displayUuidRef = useRef(); - const location = useLocation(); - const quickScreen = location.state?.pageState?.quickScreen; - - return ( - - - - {quickScreen ? ( - { - // The uuid allows the json representation of the display instance to be selected from the redux store - displayUuidRef.current = uuid; - }} - targetDisplayType="displayGridLayout" - /> - ) : ( - - No Quick Screen Loaded - - )} - - - ); -} diff --git a/src/components/QuickScreens/Display.tsx b/src/components/QuickScreens/Display.tsx new file mode 100644 index 0000000..ca4cf65 --- /dev/null +++ b/src/components/QuickScreens/Display.tsx @@ -0,0 +1,66 @@ +import { Box, Paper as MuiPaper, styled, Typography } from "@mui/material"; +import { + DynamicPageWidget, + newRelativePosition +} from "@diamondlightsource/cs-web-lib"; +import { + useWindowWidth, + APP_BAR_HEIGHT, + useWindowHeight +} from "../../utils/helper"; +import { createContext, useState } from "react"; +import QuickScreenSettings from "./Settings"; +import { useLocation } from "react-router"; + +// Local quick screen storage handler +export const StorageContext = createContext<{ + bobDisplayUuid?: string; + setBobDisplayUuid: any; +}>({ bobDisplayUuid: "", setBobDisplayUuid: () => null }); + +const Paper = styled(MuiPaper)(({ theme }) => ({ + height: `calc(${useWindowHeight()}px - ${APP_BAR_HEIGHT}px - 50px)`, + margin: `calc(${APP_BAR_HEIGHT}px + 15px) 5px 5px 5px`, + width: `calc(${useWindowWidth()}px - 10px - ${theme.spacing(7)} - 8px)` +})); + +export default function QuickScreenDisplay() { + const [bobDisplayUuid, setBobDisplayUuid] = useState(); + const location = useLocation(); + const quickScreen = location.state?.pageState?.quickScreen; + + return ( + + + + + {quickScreen ? ( + { + // The uuid allows the json representation of the display instance to be selected from the redux store + setBobDisplayUuid(uuid); + }} + targetDisplayType="displayGridLayout" + /> + ) : ( + + No Quick Screen Loaded + + )} + + + + ); +} diff --git a/src/components/QuickScreens/Settings.tsx b/src/components/QuickScreens/Settings.tsx new file mode 100644 index 0000000..741dce9 --- /dev/null +++ b/src/components/QuickScreens/Settings.tsx @@ -0,0 +1,209 @@ +import { styled, useTheme } from "@mui/material/styles"; +import MuiDrawer from "@mui/material/Drawer"; +import List from "@mui/material/List"; +import ListItem from "@mui/material/ListItem"; +import ListItemButton from "@mui/material/ListItemButton"; +import ListItemIcon from "@mui/material/ListItemIcon"; +import AddIcon from "@mui/icons-material/Add"; +import LibraryAddIcon from "@mui/icons-material/LibraryAdd"; +import SaveIcon from "@mui/icons-material/Save"; +import UploadFileIcon from "@mui/icons-material/UploadFile"; +import DeleteIcon from "@mui/icons-material/Delete"; +import CloseIcon from "@mui/icons-material/Close"; +import { + Dialog as MuiDialog, + DialogContent, + DialogTitle, + Grid, + IconButton, + ListItemText, + Stack, + Tooltip +} from "@mui/material"; +import { useNavigate } from "react-router"; +import { useState } from "react"; +import LocalStorageBrowser from "./StorageBrowser"; + +const NEW_QUICK_SCREEN = { + path: "/new.bob", + macros: {}, + defaultProtocol: "ca" +}; + +const Drawer = styled(MuiDrawer)(() => ({ + overflowX: "hidden", + width: "3%", + minWidth: "50px", + "& .MuiPaper-root": { + position: "relative", + overflowX: "hidden" + } +})); + +const Dialog = styled(MuiDialog)(({ theme }) => ({ + "& .MuiDialogContent-root": { + padding: theme.spacing(2) + }, + "& .MuiDialogActions-root": { + padding: theme.spacing(1) + } +})); + +export default function QuickScreenSettings() { + const theme = useTheme(); + const navigate = useNavigate(); + const [storageModalOpen, setStorageModalOpen] = useState(false); + + const handleCloseModal = (_event: any) => { + setStorageModalOpen(false); + }; + + /** + * Loads a new blank quick screen + */ + const onClickNew = () => { + //Change to a blank Quick Screen + navigate("/quick-screens/", { + state: { pageState: { quickScreen: NEW_QUICK_SCREEN } }, + replace: true + }); + }; + + /** + * Adds a new screen to the current view to draw components from + */ + const onClickAdd = () => { + console.log("clicked add"); + }; + + /** + * Saves the current quick screen to local storage + */ + const onClickSave = () => { + setStorageModalOpen(true); + }; + + /** + * Loads a quick screen from local storage + */ + const onClickLoad = () => { + console.log("clicked load"); + }; + + /** + * Loads a quick screen from local storage + */ + const onClickDelete = () => { + console.log("clicked delete"); + }; + + const SETTINGS_LIST = [ + { + name: "New", + text: "Create new blank Quick Screen", + icon: , + onClick: onClickNew + }, + { + name: "Add", + text: "Add a .bob file to the view", + icon: , + onClick: onClickAdd + }, + { + name: "Save", + text: "Save the current Quick Screen", + icon: , + onClick: onClickSave + }, + { + name: "Load", + text: "Load a Quick Screen", + icon: , + onClick: onClickLoad + }, + { + name: "Delete", + text: "Discard the current Quick Screen", + icon: , + onClick: onClickDelete + } + ]; + + return ( + <> + + + {SETTINGS_LIST.map(item => ( + + + + + {item.icon} + + + + + + ))} + + + + + Quick Screen Browser + + + + + + + + + ({ + position: "absolute", + right: 8, + top: 8, + color: theme.palette.primary.main + })} + > + + + + + ); +} diff --git a/src/components/QuickScreens/StorageBrowser.tsx b/src/components/QuickScreens/StorageBrowser.tsx new file mode 100644 index 0000000..c4f24d1 --- /dev/null +++ b/src/components/QuickScreens/StorageBrowser.tsx @@ -0,0 +1,153 @@ +import { RichTreeView } from "@mui/x-tree-view/RichTreeView"; +import { useContext, useEffect, useState } from "react"; +import { TreeViewBaseItem } from "@mui/x-tree-view"; +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Stack, + TextField +} from "@mui/material"; +import { + useDisplayInstance, + useNotification +} from "@diamondlightsource/cs-web-lib"; +import { StorageContext } from "./Display"; + +/** + * Gets all Quick Screens currently in local storage and + * converts them to Tree View Items + * @returns + */ +function getQuickScreens(): TreeViewBaseItem[] { + const children: TreeViewBaseItem[] = []; + + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i)!; + + children.push({ + id: key, + label: key + }); + } + + children.sort((a, b) => a.label.localeCompare(b.label)); + return children; +} + +export default function LocalStorageBrowser(props: { setModalOpen: any }) { + const [quickScreens, setQuickScreens] = useState([]); + const quickScreenStorage = useContext(StorageContext); + const [selectedKey, setSelectedKey] = useState(""); + const [quickScreenName, setQuickScreenName] = useState(""); + const fileContent = useDisplayInstance(quickScreenStorage.bobDisplayUuid!); + const { showWarning, showError } = useNotification(); + const [confirmOverwrite, setConfirmOverwrite] = useState(false); + + useEffect(() => { + setQuickScreens(getQuickScreens()); + }, []); + + const handleSelection = (_: any, itemId: any) => { + const storedValue = localStorage.getItem(itemId) ?? ""; + console.log(storedValue); + setSelectedKey(itemId); + setQuickScreenName(itemId); + }; + + const handleSave = () => { + // If no file name or file content, show notification + if (!quickScreenName.trim() || !fileContent) { + if (!quickScreenName.trim()) + showWarning("Unable to save: no Quick Screen name given."); + if (!fileContent) + showError("Unable to save: no Quick Screen content found."); + return; + } + + const oldScreen = localStorage.getItem(quickScreenName); + const newScreen = createNewScreen(); + // Check if content already exists for this name + if (oldScreen && oldScreen !== newScreen) { + // User confirmation to overwrite + setConfirmOverwrite(true); + return; + } + saveQuickScreen(); + }; + + /** + * Creates the new Quick Screen instance + */ + const createNewScreen = () => + JSON.stringify({ + macros: fileContent?.macros ?? {}, + description: fileContent?.description + }); + + const saveQuickScreen = () => { + const newScreen = createNewScreen(); + localStorage.setItem(quickScreenName, newScreen); + props.setModalOpen(false); + }; + + return ( + + + + + + setQuickScreenName(e.target.value)} + fullWidth + /> + + + setConfirmOverwrite(false)} + > + Overwrite Quick Screen? + + + A Quick Screen named {quickScreenName} already + exists. Saving will replace the existing content. Are you sure you + want to continue? + + + + + + + + + ); +} diff --git a/src/routes/QuickScreensPage.tsx b/src/routes/QuickScreensPage.tsx index ea6994a..41c445c 100644 --- a/src/routes/QuickScreensPage.tsx +++ b/src/routes/QuickScreensPage.tsx @@ -1,6 +1,6 @@ import { Box, CssBaseline } from "@mui/material"; import { useEffect } from "react"; -import QuickScreens from "../components/QuickScreens"; +import QuickScreenDisplay from "../components/QuickScreens/Display"; import DLSAppBar from "../components/AppBar"; /** @@ -17,7 +17,7 @@ export function QuickScreensPage() { - + ); diff --git a/src/tests/components/QuickScreenSettings.test.tsx b/src/tests/components/QuickScreenSettings.test.tsx index f595189..0f3f93a 100644 --- a/src/tests/components/QuickScreenSettings.test.tsx +++ b/src/tests/components/QuickScreenSettings.test.tsx @@ -1,6 +1,6 @@ import { fireEvent, render } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; -import QuickScreenSettings from "../../components/QuickScreenSettings"; +import QuickScreenSettings from "../../components/QuickScreens/Settings"; const renderComponent = () => { return render(); diff --git a/src/tests/components/QuickScreens.test.tsx b/src/tests/components/QuickScreens.test.tsx index b7cd130..7e73c85 100644 --- a/src/tests/components/QuickScreens.test.tsx +++ b/src/tests/components/QuickScreens.test.tsx @@ -1,6 +1,6 @@ import { render } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; -import QuickScreens from "../../components/QuickScreens"; +import QuickScreens from "../../components/QuickScreens/Display"; const renderComponent = () => { return render(); From 8befc62c7bc33e052325ae2461042afada1cd908 Mon Sep 17 00:00:00 2001 From: Abigail Alexander Date: Tue, 28 Jul 2026 10:23:24 +0100 Subject: [PATCH 2/4] Add folder structure --- .../QuickScreens/StorageBrowser.tsx | 158 ++++++++++++++++-- src/components/ScreenTreeView.tsx | 28 +--- src/components/utils.ts | 21 +++ 3 files changed, 165 insertions(+), 42 deletions(-) create mode 100644 src/components/utils.ts diff --git a/src/components/QuickScreens/StorageBrowser.tsx b/src/components/QuickScreens/StorageBrowser.tsx index c4f24d1..9a7e4a0 100644 --- a/src/components/QuickScreens/StorageBrowser.tsx +++ b/src/components/QuickScreens/StorageBrowser.tsx @@ -1,6 +1,12 @@ import { RichTreeView } from "@mui/x-tree-view/RichTreeView"; import { useContext, useEffect, useState } from "react"; -import { TreeViewBaseItem } from "@mui/x-tree-view"; +import { + TreeItem2, + TreeItem2Props, + TreeViewBaseItem, + TreeViewItemId, + useTreeItem2 +} from "@mui/x-tree-view"; import { Box, Button, @@ -17,6 +23,40 @@ import { useNotification } from "@diamondlightsource/cs-web-lib"; import { StorageContext } from "./Display"; +import { getAllScreensWithChildrenItemIds } from "../utils"; +import FolderIcon from "@mui/icons-material/Folder"; +import FolderOpenIcon from "@mui/icons-material/FolderOpen"; +import SubdirectoryArrowRightIcon from "@mui/icons-material/SubdirectoryArrowRight"; + +/** + * Custom Tree Item that lets us change icon + * @param props + * @returns + */ +function QuickScreenTreeItem(props: TreeItem2Props) { + const { status } = useTreeItem2({ + itemId: props.itemId, + children: props.children + }); + + return ( + + status.expandable ? ( + status.expanded ? ( + + ) : ( + + ) + ) : ( + + ) + }} + /> + ); +} /** * Gets all Quick Screens currently in local storage and @@ -24,39 +64,105 @@ import { StorageContext } from "./Display"; * @returns */ function getQuickScreens(): TreeViewBaseItem[] { - const children: TreeViewBaseItem[] = []; + const tree: TreeViewBaseItem[] = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i)!; - children.push({ - id: key, - label: key - }); + // Check the item is part of Quick Screens + if (!key || !key.startsWith("quickScreens/")) continue; + // Fetch content + const screenName = key.substring("quickScreens/".length); + const parts = screenName.split("/"); + // Check if screen is in folder + let currentLevel = tree; + // For each folder, create child file labels/folders + for (let j = 0; j < parts.length; j++) { + const part = parts[j]; + const id = parts.slice(0, j + 1).join("/"); + + let branch = currentLevel.find(item => item.id === id); + const isFolder = j < parts.length - 1; + + // Create new branch + if (!branch) { + branch = { + id, + label: part, + ...(isFolder ? { children: [] } : {}) + }; + + currentLevel.push(branch); + } + + if (branch.children) { + currentLevel = branch.children; + } + } } - children.sort((a, b) => a.label.localeCompare(b.label)); - return children; + // Recursively sort tree alphabetically + const sortTree = (items: TreeViewBaseItem[]) => { + items.sort((a, b) => a.label.localeCompare(b.label)); + + for (const item of items) { + if (item.children) { + sortTree(item.children); + } + } + }; + sortTree(tree); + return tree; } +/** + * Find a node in the treeview when given its id + * @param items + * @param id + * @returns + */ +const findNodeById = ( + items: TreeViewBaseItem[], + id: string +): TreeViewBaseItem | undefined => { + for (const item of items) { + if (item.id === id) { + return item; + } + if (item.children) { + const found = findNodeById(item.children, id); + if (found) { + return found; + } + } + } + return undefined; +}; + export default function LocalStorageBrowser(props: { setModalOpen: any }) { const [quickScreens, setQuickScreens] = useState([]); - const quickScreenStorage = useContext(StorageContext); const [selectedKey, setSelectedKey] = useState(""); const [quickScreenName, setQuickScreenName] = useState(""); + const [selectedIsFolder, setSelectedIsFolder] = useState(false); + const [expandedScreens, setExpandedScreens] = useState([]); + const [confirmOverwrite, setConfirmOverwrite] = useState(false); + const quickScreenStorage = useContext(StorageContext); const fileContent = useDisplayInstance(quickScreenStorage.bobDisplayUuid!); const { showWarning, showError } = useNotification(); - const [confirmOverwrite, setConfirmOverwrite] = useState(false); useEffect(() => { - setQuickScreens(getQuickScreens()); + const screens = getQuickScreens(); + setQuickScreens(screens); + // Set list of expanded screens + getAllScreensWithChildrenItemIds(screens, setExpandedScreens); }, []); const handleSelection = (_: any, itemId: any) => { - const storedValue = localStorage.getItem(itemId) ?? ""; - console.log(storedValue); setSelectedKey(itemId); setQuickScreenName(itemId); + // Check if the item selected is a folder + const selectedNode = findNodeById(quickScreens, itemId); + setSelectedIsFolder(!!selectedNode?.children?.length); }; const handleSave = () => { @@ -68,8 +174,8 @@ export default function LocalStorageBrowser(props: { setModalOpen: any }) { showError("Unable to save: no Quick Screen content found."); return; } - - const oldScreen = localStorage.getItem(quickScreenName); + // Check if user is trying to save as folder and prevent + const oldScreen = localStorage.getItem(`quickScreens/${quickScreenName}`); const newScreen = createNewScreen(); // Check if content already exists for this name if (oldScreen && oldScreen !== newScreen) { @@ -89,9 +195,12 @@ export default function LocalStorageBrowser(props: { setModalOpen: any }) { description: fileContent?.description }); + /** + * Save the Quick Screen to local storage + */ const saveQuickScreen = () => { const newScreen = createNewScreen(); - localStorage.setItem(quickScreenName, newScreen); + localStorage.setItem(`quickScreens/${quickScreenName}`, newScreen); props.setModalOpen(false); }; @@ -109,16 +218,29 @@ export default function LocalStorageBrowser(props: { setModalOpen: any }) { items={quickScreens} onSelectedItemsChange={handleSelection} selectedItems={selectedKey} + expandedItems={expandedScreens} + onExpandedItemsChange={(_event, _itemIds) => + getAllScreensWithChildrenItemIds(quickScreens, setExpandedScreens) + } + slots={{ item: QuickScreenTreeItem }} /> setQuickScreenName(e.target.value)} + onChange={e => { + const selectedNode = findNodeById(quickScreens, e.target.value); + setSelectedIsFolder(!!selectedNode?.children?.length); + setQuickScreenName(e.target.value) + }} fullWidth /> - diff --git a/src/components/ScreenTreeView.tsx b/src/components/ScreenTreeView.tsx index fda2b12..e552538 100644 --- a/src/components/ScreenTreeView.tsx +++ b/src/components/ScreenTreeView.tsx @@ -6,16 +6,13 @@ import { BeamlineTreeStateContext } from "../App"; import { MenuContext } from "../routes/SynopticPage"; import { executeOpenPageActionWithFileGuid } from "../utils/csWebLibActions"; import { FileIDs } from "../store"; +import { getAllScreensWithChildrenItemIds } from "./utils"; export default function ScreenTreeView() { const { state } = useContext(BeamlineTreeStateContext); const { menuOpen } = useContext(MenuContext); const fileContext = useContext(FileContext); - const [expandedScreens, setExpandedScreens] = useState([]); - - const handleExpandedScreensChange = (screenIds: string[]) => { - setExpandedScreens(screenIds); - }; + const [expandedScreens, setExpandedScreens] = useState([]); const handleClick = (itemId: string) => { const selectedBeamlineId = state.currentBeamline; @@ -41,24 +38,7 @@ export default function ScreenTreeView() { // When beamline is updated, trigger refresh of expanded screens to fully expand all useEffect(() => { - const getAllScreensWithChildrenItemIds = async ( - screenTree: TreeViewBaseItem[] - ) => { - const screenIds: TreeViewItemId[] = []; - - const registerScreenId = (item: TreeViewBaseItem): void => { - if (item.children?.length) { - screenIds.push(item.id); - (item.children as TreeViewBaseItem[]).forEach(registerScreenId); - } - }; - - for (const screen of screenTree) { - await registerScreenId(screen); - } - setExpandedScreens(screenIds); - }; - getAllScreensWithChildrenItemIds(currentScreenTree); + getAllScreensWithChildrenItemIds(currentScreenTree, setExpandedScreens); }, [state.currentBeamline, state.beamlines, currentScreenTree]); return ( @@ -69,7 +49,7 @@ export default function ScreenTreeView() { selectedItems={state.currentScreenId} expandedItems={expandedScreens} onExpandedItemsChange={(_event, itemIds) => - handleExpandedScreensChange(itemIds) + setExpandedScreens(itemIds) } onItemClick={(_event, itemId) => { if (!itemId || currentFileMetadata[itemId]?.exists === false) { diff --git a/src/components/utils.ts b/src/components/utils.ts new file mode 100644 index 0000000..b701377 --- /dev/null +++ b/src/components/utils.ts @@ -0,0 +1,21 @@ +import { TreeViewBaseItem, TreeViewItemId } from "@mui/x-tree-view"; +import { Dispatch, SetStateAction } from "react"; + +export const getAllScreensWithChildrenItemIds = ( + screenTree: TreeViewBaseItem[], + setExpandedScreens: Dispatch> +) => { + const screenIds: TreeViewItemId[] = []; + + const registerScreenId = (item: TreeViewBaseItem): void => { + if (item.children?.length) { + screenIds.push(item.id); + (item.children as TreeViewBaseItem[]).forEach(registerScreenId); + } + }; + + for (const screen of screenTree) { + registerScreenId(screen); + } + setExpandedScreens(screenIds); +}; From 1cd53b0667c3fc916ab9f154f0462db72b6077ea Mon Sep 17 00:00:00 2001 From: Abigail Alexander Date: Tue, 28 Jul 2026 13:34:47 +0100 Subject: [PATCH 3/4] Add tests --- .../QuickScreens/StorageBrowser.tsx | 32 +- src/components/utils.ts | 29 ++ ...s.test.tsx => QuickScreenDisplay.test.tsx} | 0 .../QuickScreenStorageBrowser.test.tsx | 387 ++++++++++++++++++ src/tests/components/utils.test.ts | 173 ++++++++ 5 files changed, 593 insertions(+), 28 deletions(-) rename src/tests/components/{QuickScreens.test.tsx => QuickScreenDisplay.test.tsx} (100%) create mode 100644 src/tests/components/QuickScreenStorageBrowser.test.tsx create mode 100644 src/tests/components/utils.test.ts diff --git a/src/components/QuickScreens/StorageBrowser.tsx b/src/components/QuickScreens/StorageBrowser.tsx index 9a7e4a0..91d67e5 100644 --- a/src/components/QuickScreens/StorageBrowser.tsx +++ b/src/components/QuickScreens/StorageBrowser.tsx @@ -23,7 +23,7 @@ import { useNotification } from "@diamondlightsource/cs-web-lib"; import { StorageContext } from "./Display"; -import { getAllScreensWithChildrenItemIds } from "../utils"; +import { findNodeById, getAllScreensWithChildrenItemIds } from "../utils"; import FolderIcon from "@mui/icons-material/Folder"; import FolderOpenIcon from "@mui/icons-material/FolderOpen"; import SubdirectoryArrowRightIcon from "@mui/icons-material/SubdirectoryArrowRight"; @@ -63,7 +63,7 @@ function QuickScreenTreeItem(props: TreeItem2Props) { * converts them to Tree View Items * @returns */ -function getQuickScreens(): TreeViewBaseItem[] { +export function getQuickScreens(): TreeViewBaseItem[] { const tree: TreeViewBaseItem[] = []; for (let i = 0; i < localStorage.length; i++) { @@ -115,30 +115,6 @@ function getQuickScreens(): TreeViewBaseItem[] { return tree; } -/** - * Find a node in the treeview when given its id - * @param items - * @param id - * @returns - */ -const findNodeById = ( - items: TreeViewBaseItem[], - id: string -): TreeViewBaseItem | undefined => { - for (const item of items) { - if (item.id === id) { - return item; - } - if (item.children) { - const found = findNodeById(item.children, id); - if (found) { - return found; - } - } - } - return undefined; -}; - export default function LocalStorageBrowser(props: { setModalOpen: any }) { const [quickScreens, setQuickScreens] = useState([]); const [selectedKey, setSelectedKey] = useState(""); @@ -232,8 +208,8 @@ export default function LocalStorageBrowser(props: { setModalOpen: any }) { onChange={e => { const selectedNode = findNodeById(quickScreens, e.target.value); setSelectedIsFolder(!!selectedNode?.children?.length); - setQuickScreenName(e.target.value) - }} + setQuickScreenName(e.target.value); + }} fullWidth />