From 446b194e11c4385aef8ff3b80c4b183d4e26e4a1 Mon Sep 17 00:00:00 2001 From: "Toni M. Brotons" <10654467+toni-neurosc@users.noreply.github.com> Date: Mon, 14 Oct 2024 14:00:24 +0200 Subject: [PATCH] Refactor SettingsSection recursive rendering component --- .../src/components/StatusBar/StatusBar.jsx | 55 +----- gui_dev/src/pages/Settings/Settings.jsx | 175 ++++++++++++------ gui_dev/src/stores/uiStore.js | 21 +++ 3 files changed, 147 insertions(+), 104 deletions(-) diff --git a/gui_dev/src/components/StatusBar/StatusBar.jsx b/gui_dev/src/components/StatusBar/StatusBar.jsx index 6bffbe15..f011e2b9 100644 --- a/gui_dev/src/components/StatusBar/StatusBar.jsx +++ b/gui_dev/src/components/StatusBar/StatusBar.jsx @@ -1,28 +1,15 @@ -import { useState } from "react"; - import { ResizeHandle } from "./ResizeHandle"; import { SocketStatus } from "./SocketStatus"; import { WebviewStatus } from "./WebviewStatus"; -import { useSettingsStore } from "@/stores"; - -import { useWebviewStore } from "@/stores"; -import { Popover, Stack, Typography } from "@mui/material"; +import { useUiStore, useWebviewStore } from "@/stores"; +import { Stack } from "@mui/material"; export const StatusBar = () => { const isWebView = useWebviewStore((state) => state.isWebView); - const validationErrors = useSettingsStore((state) => state.validationErrors); - - const [anchorEl, setAnchorEl] = useState(null); - const open = Boolean(anchorEl); - - const handleOpenErrorsPopover = (event) => { - setAnchorEl(event.currentTarget); - }; + const createStatusBarContent = useUiStore((state) => state.statusBarContent); - const handleCloseErrorsPopover = () => { - setAnchorEl(null); - }; + const StatusBarContent = createStatusBarContent(); return ( { borderColor="background.level3" height="2rem" > - {validationErrors?.length > 0 && ( - <> - - {validationErrors?.length} errors found in Settings - + {StatusBarContent && } - - - {validationErrors.map((error, index) => ( - - {index} - [{error.type}] {error.msg} - - ))} - - - - )} {/* */} {/* Current experiment */} {/* Current stream */} diff --git a/gui_dev/src/pages/Settings/Settings.jsx b/gui_dev/src/pages/Settings/Settings.jsx index 839a89ad..60b238d6 100644 --- a/gui_dev/src/pages/Settings/Settings.jsx +++ b/gui_dev/src/pages/Settings/Settings.jsx @@ -4,6 +4,7 @@ import { Button, ButtonGroup, InputAdornment, + Popover, Stack, Switch, TextField, @@ -14,7 +15,7 @@ import { Link } from "react-router-dom"; import { CollapsibleBox, TitledBox } from "@/components"; import { FrequencyRangeList } from "./FrequencyRange"; import { Dropdown } from "./Dropdown"; -import { useSettingsStore } from "@/stores"; +import { useSettingsStore, useStatusBarContent } from "@/stores"; import { filterObjectByKeys } from "@/utils/functions"; const formatKey = (key) => { @@ -116,6 +117,17 @@ const SettingsField = ({ path, Component, label, value, onChange, error }) => { ); }; +// Function to get the error corresponding to this field or its children +const getFieldError = (fieldPath, errors) => { + if (!errors) return null; + + return errors.find((error) => { + const errorPath = error.loc.join("."); + const currentPath = fieldPath.join("."); + return errorPath === currentPath || errorPath.startsWith(currentPath + "."); + }); +}; + const SettingsSection = ({ settings, title = null, @@ -124,29 +136,32 @@ const SettingsSection = ({ errors, }) => { const boxTitle = title ? title : formatKey(path[path.length - 1]); + /* + 3 possible cases: + 1. Primitive type || 2. Object with component -> Don't iterate, render directly + 3. Object without component or 4. Array -> Iterate and render recursively + */ + + const type = typeof settings; + const isObject = type === "object" && !Array.isArray(settings); + const isArray = Array.isArray(settings); + + // __field_type__ should be always present + if (isObject && !settings.__field_type__) { + console.log(settings); + throw new Error("Invalid settings object"); + } + const fieldType = isObject ? settings.__field_type__ : type; + const Component = componentRegistry[fieldType]; - // Function to get the error corresponding to this field or its children - const getFieldError = (fieldPath) => { - if (!errors) return null; - - return errors.find((error) => { - const errorPath = error.loc.join("."); - const currentPath = fieldPath.join("."); - return ( - errorPath === currentPath || errorPath.startsWith(currentPath + ".") - ); - }); - }; - - // If we receive a primitive value, we need to render a component - if (typeof settings !== "object") { - const Component = componentRegistry[typeof settings]; + // Case 1: Primitive type -> Don't iterate, render directly + if (!isObject && !isArray) { if (!Component) { - console.error(`Invalid component type: ${typeof settings}`); + console.error(`Invalid component type: ${type}`); return null; } - const error = getFieldError(path); + const error = getFieldError(path, errors); return ( - {Object.entries(settings).map(([key, value]) => { - if (key === "__field_type__") return null; - if (value === null) return null; - - const newPath = [...path, key]; - const label = key; - const isPydanticModel = - typeof value === "object" && "__field_type__" in value; - - const error = getFieldError(newPath); + // Case 2: Object with component -> Don't iterate, render directly + if (isObject && Component) { + return ( + + ); + } - const fieldType = isPydanticModel ? value.__field_type__ : typeof value; + // Case 3: Object without component or 4. Array -> Iterate and render recursively + if ((isObject && !Component) || isArray) { + return ( + + {/* Handle recursing through both objects and arrays */} + {(isArray ? settings : Object.entries(settings)).map((item, index) => { + const [key, value] = isArray ? [index.toString(), item] : item; + if (key.startsWith("__")) return null; // Skip metadata fields - const Component = componentRegistry[fieldType]; + const newPath = [...path, key]; - if (Component) { return ( - ); - } else { - return ( - - - - ); - } - })} + })} + + ); + } + + // Default case: return null and log an error + console.error(`Invalid settings object, returning null`); + return null; +}; + +const StatusBarSettingsInfo = () => { + const validationErrors = useSettingsStore((state) => state.validationErrors); + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + + const handleOpenErrorsPopover = (event) => { + setAnchorEl(event.currentTarget); + }; + + const handleCloseErrorsPopover = () => { + setAnchorEl(null); + }; + + return ( + <> + {validationErrors?.length > 0 && ( + <> + + {validationErrors?.length} errors found in Settings + + + + {validationErrors.map((error, index) => ( + + {index} - [{error.type}] {error.msg} + + ))} + + + + )} ); }; @@ -213,6 +277,7 @@ export const Settings = () => { const uploadSettings = useSettingsStore((state) => state.uploadSettings); const resetSettings = useSettingsStore((state) => state.resetSettings); const validationErrors = useSettingsStore((state) => state.validationErrors); + useStatusBarContent(StatusBarSettingsInfo); // This is needed so that the frequency ranges stay in order between updates const frequencyRangeOrder = useSettingsStore( @@ -229,6 +294,8 @@ export const Settings = () => { uploadSettings(null, true); // validateOnly = true }, [settings]); + // Inject validation error info into status bar + // This has to be after all the hooks, otherwise React will complain if (!settings) { return
Loading settings...
; diff --git a/gui_dev/src/stores/uiStore.js b/gui_dev/src/stores/uiStore.js index a229ec06..25b44382 100644 --- a/gui_dev/src/stores/uiStore.js +++ b/gui_dev/src/stores/uiStore.js @@ -1,4 +1,5 @@ import { createPersistStore } from "./createStore"; +import { useEffect } from "react"; export const useUiStore = createPersistStore("ui", (set, get) => ({ activeDrawer: null, @@ -28,4 +29,24 @@ export const useUiStore = createPersistStore("ui", (set, get) => ({ state.accordionStates[id] = defaultState; } }), + + // Hook to inject UI elements into the status bar + statusBarContent: () => {}, + setStatusBarContent: (content) => set({ statusBarContent: content }), + clearStatusBarContent: () => set({ statusBarContent: null }), })); + +// Use this hook from Page components to inject page-specific UI elements into the status bar +export const useStatusBarContent = (content) => { + const createStatusBarContent = () => content; + + const setStatusBarContent = useUiStore((state) => state.setStatusBarContent); + const clearStatusBarContent = useUiStore( + (state) => state.clearStatusBarContent + ); + + useEffect(() => { + setStatusBarContent(createStatusBarContent); + return () => clearStatusBarContent(); + }, [content, setStatusBarContent, clearStatusBarContent]); +};