diff --git a/frontend/src/components/SettingsSidebar/MenuOption/index.jsx b/frontend/src/components/SettingsSidebar/MenuOption/index.jsx new file mode 100644 index 00000000000..20924d53dc6 --- /dev/null +++ b/frontend/src/components/SettingsSidebar/MenuOption/index.jsx @@ -0,0 +1,168 @@ +import React, { useEffect, useState } from "react"; +import { CaretRight } from "@phosphor-icons/react"; +import { Link } from "react-router-dom"; + +export default function MenuOption({ + btnText, + icon, + href, + childOptions = [], + flex = false, + user = null, + roles = [], + hidden = false, + isChild = false, +}) { + const storageKey = generateStorageKey({ key: btnText }); + const location = window.location.pathname; + const hasChildren = childOptions.length > 0; + const hasVisibleChildren = hasVisibleOptions(user, childOptions); + const { isExpanded, setIsExpanded } = useIsExpanded({ + storageKey, + hasVisibleChildren, + childOptions, + location, + }); + + if (hidden) return null; + + // If this option is a parent level option + if (!isChild) { + // and has no children then use its flex props and roles prop directly + if (!hasChildren) { + if (!flex && !roles.includes(user?.role)) return null; + if (flex && !!user && !roles.includes(user?.role)) return null; + } + + // if has children and no visible children - remove it. + if (hasChildren && !hasVisibleChildren) return null; + } else { + // is a child so we use it's permissions + if (!flex && !roles.includes(user?.role)) return null; + if (flex && !!user && !roles.includes(user?.role)) return null; + } + + const isActive = hasChildren + ? (!isExpanded && childOptions.some((child) => child.href === location)) || + location === href + : location === href; + + const handleClick = (e) => { + if (hasChildren) { + e.preventDefault(); + const newExpandedState = !isExpanded; + setIsExpanded(newExpandedState); + localStorage.setItem(storageKey, JSON.stringify(newExpandedState)); + } + }; + + return ( +
+
+ + {icon} +

+ {btnText} +

+ + {hasChildren && ( + + )} +
+ {isExpanded && hasChildren && ( +
+ {childOptions.map((childOption, index) => ( + + ))} +
+ )} +
+ ); +} + +function useIsExpanded({ + storageKey = "", + hasVisibleChildren = false, + childOptions = [], + location = null, +}) { + const [isExpanded, setIsExpanded] = useState(() => { + if (hasVisibleChildren) { + const storedValue = localStorage.getItem(storageKey); + if (storedValue !== null) { + return JSON.parse(storedValue); + } + return childOptions.some((child) => child.href === location); + } + return false; + }); + + useEffect(() => { + if (hasVisibleChildren) { + const shouldExpand = childOptions.some( + (child) => child.href === location + ); + if (shouldExpand && !isExpanded) { + setIsExpanded(true); + localStorage.setItem(storageKey, JSON.stringify(true)); + } + } + }, [location]); + + return { isExpanded, setIsExpanded }; +} + +function hasVisibleOptions(user = null, childOptions = []) { + if (!Array.isArray(childOptions) || childOptions?.length === 0) return false; + + function isVisible({ roles = [], user = null, flex = false }) { + if (!flex && !roles.includes(user?.role)) return false; + if (flex && !!user && !roles.includes(user?.role)) return false; + return true; + } + + return childOptions.some((opt) => + isVisible({ roles: opt.roles, user, flex: opt.flex }) + ); +} + +function generateStorageKey({ key = "" }) { + const _key = key.replace(/\s+/g, "_").toLowerCase(); + return `anything_llm_menu_${_key}_expanded`; +} diff --git a/frontend/src/components/SettingsSidebar/index.jsx b/frontend/src/components/SettingsSidebar/index.jsx index 4f0ea1b9662..723867e2e30 100644 --- a/frontend/src/components/SettingsSidebar/index.jsx +++ b/frontend/src/components/SettingsSidebar/index.jsx @@ -2,28 +2,15 @@ import React, { useEffect, useRef, useState } from "react"; import paths from "@/utils/paths"; import useLogo from "@/hooks/useLogo"; import { - EnvelopeSimple, - SquaresFour, - Users, - BookOpen, - ChatCenteredText, - Eye, - Key, - ChatText, - Database, - Lock, House, List, - FileCode, - Notepad, - CodeBlock, - Barcode, - ClosedCaptioning, - EyeSlash, - SplitVertical, - Microphone, Robot, Flask, + Gear, + UserCircleGear, + PencilSimpleLine, + Nut, + Toolbox, } from "@phosphor-icons/react"; import useUser from "@/hooks/useUser"; import { USER_BACKGROUND_COLOR } from "@/utils/constants"; @@ -32,6 +19,8 @@ import Footer from "../Footer"; import { Link } from "react-router-dom"; import { useTranslation } from "react-i18next"; import showToast from "@/utils/toast"; +import System from "@/models/system"; +import Option from "./MenuOption"; export default function SettingsSidebar() { const { t } = useTranslation(); @@ -118,6 +107,17 @@ export default function SettingsSidebar() {
+
+ + + Privacy & Data +
@@ -156,6 +156,15 @@ export default function SettingsSidebar() {
+
+ + + Privacy & Data +
@@ -168,233 +177,173 @@ export default function SettingsSidebar() { ); } -const Option = ({ - btnText, - icon, - href, - childLinks = [], - flex = false, - user = null, - allowedRole = [], - subOptions = null, - hidden = false, -}) => { - if (hidden) return null; +function SupportEmail() { + const [supportEmail, setSupportEmail] = useState(paths.mailToMintplex()); - const hasActiveChild = childLinks.includes(window.location.pathname); - const isActive = window.location.pathname === href; - - // Option only for multi-user - if (!flex && !allowedRole.includes(user?.role)) return null; - - // Option is dual-mode, but user exists, we need to check permissions - if (flex && !!user && !allowedRole.includes(user?.role)) return null; + useEffect(() => { + const fetchSupportEmail = async () => { + const supportEmail = await System.fetchSupportEmail(); + setSupportEmail( + supportEmail?.email + ? `mailto:${supportEmail.email}` + : paths.mailToMintplex() + ); + }; + fetchSupportEmail(); + }, []); return ( - <> -
- - {React.cloneElement(icon, { weight: isActive ? "fill" : "regular" })} -

- {btnText} -

- -
- {!!subOptions && (isActive || hasActiveChild) && ( -
- {subOptions} -
- )} - + + Contact Support + ); -}; +} const SidebarOptions = ({ user = null, t }) => ( <>