mirror of
https://github.com/Mintplex-Labs/anything-llm
synced 2026-04-25 17:15:37 +02:00
185 lines
5.3 KiB
JavaScript
185 lines
5.3 KiB
JavaScript
import React, { useEffect, useState } from "react";
|
|
import { CaretRight } from "@phosphor-icons/react";
|
|
import { Link, useLocation } 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 = useLocation();
|
|
const hasChildren = childOptions.length > 0;
|
|
const hasVisibleChildren = hasVisibleOptions(user, childOptions);
|
|
const { isExpanded, setIsExpanded } = useIsExpanded({
|
|
storageKey,
|
|
hasVisibleChildren,
|
|
childOptions,
|
|
location: location.pathname,
|
|
});
|
|
|
|
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.pathname)) ||
|
|
location.pathname === href
|
|
: location.pathname === href;
|
|
|
|
const handleClick = (e) => {
|
|
if (hasChildren) {
|
|
e.preventDefault();
|
|
const newExpandedState = !isExpanded;
|
|
setIsExpanded(newExpandedState);
|
|
localStorage.setItem(storageKey, JSON.stringify(newExpandedState));
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<div
|
|
className={`
|
|
flex items-center justify-between w-full
|
|
transition-all duration-300
|
|
rounded-[6px]
|
|
${
|
|
isActive
|
|
? "bg-white/5 font-medium border-outline"
|
|
: "hover:bg-white/5"
|
|
}
|
|
`}
|
|
>
|
|
<Link
|
|
to={href}
|
|
className={`flex flex-grow items-center px-[12px] h-[32px] font-medium ${
|
|
isChild ? "text-white/70 hover:text-white" : "text-white"
|
|
}`}
|
|
onClick={hasChildren ? handleClick : undefined}
|
|
>
|
|
{icon}
|
|
<p
|
|
className={`${
|
|
isChild ? "text-xs" : "text-sm"
|
|
} leading-loose whitespace-nowrap overflow-hidden ml-2 ${
|
|
isActive ? "text-white" : ""
|
|
} ${!icon && "pl-5"}`}
|
|
>
|
|
{btnText}
|
|
</p>
|
|
</Link>
|
|
{hasChildren && (
|
|
<button onClick={handleClick} className="p-2 text-white">
|
|
<CaretRight
|
|
size={16}
|
|
weight="bold"
|
|
className={`transition-transform ${
|
|
isExpanded ? "rotate-90" : ""
|
|
}`}
|
|
/>
|
|
</button>
|
|
)}
|
|
</div>
|
|
{isExpanded && hasChildren && (
|
|
<div className="mt-1 rounded-r-lg w-full">
|
|
{childOptions.map((childOption, index) => (
|
|
<MenuOption
|
|
key={index}
|
|
{...childOption} // flex and roles go here.
|
|
user={user}
|
|
isChild={true}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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 };
|
|
}
|
|
|
|
/**
|
|
* Checks if the child options are visible to the user.
|
|
* This hides the top level options if the child options are not visible
|
|
* for either the users permissions or the child options hidden prop is set to true by other means.
|
|
* If all child options return false for `isVisible` then the parent option will not be visible as well.
|
|
* @param {object} user - The user object.
|
|
* @param {array} childOptions - The child options.
|
|
* @returns {boolean} - True if the child options are visible, false otherwise.
|
|
*/
|
|
function hasVisibleOptions(user = null, childOptions = []) {
|
|
if (!Array.isArray(childOptions) || childOptions?.length === 0) return false;
|
|
|
|
function isVisible({
|
|
roles = [],
|
|
user = null,
|
|
flex = false,
|
|
hidden = false,
|
|
}) {
|
|
if (hidden) return 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, hidden: opt.hidden })
|
|
);
|
|
}
|
|
|
|
function generateStorageKey({ key = "" }) {
|
|
const _key = key.replace(/\s+/g, "_").toLowerCase();
|
|
return `anything_llm_menu_${_key}_expanded`;
|
|
}
|