2024-09-10 09:50:11 +00:00
|
|
|
/*
|
|
|
|
* Copyright © 2024 Hexastack. All rights reserved.
|
|
|
|
*
|
|
|
|
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
|
|
|
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
|
|
|
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
|
|
|
*/
|
|
|
|
|
|
|
|
import {
|
|
|
|
faAlignLeft,
|
|
|
|
faAsterisk,
|
|
|
|
faBars,
|
|
|
|
faCogs,
|
|
|
|
faComments,
|
|
|
|
faDatabase,
|
|
|
|
faGraduationCap,
|
|
|
|
faLanguage,
|
|
|
|
faTags,
|
|
|
|
faUserCircle,
|
|
|
|
faUsers,
|
|
|
|
IconDefinition,
|
|
|
|
} from "@fortawesome/free-solid-svg-icons";
|
2024-09-22 11:51:56 +00:00
|
|
|
import { Flag, Language } from "@mui/icons-material";
|
2024-09-10 09:50:11 +00:00
|
|
|
import AppsIcon from "@mui/icons-material/Apps";
|
|
|
|
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
|
|
|
|
import DriveFolderUploadIcon from "@mui/icons-material/DriveFolderUpload";
|
|
|
|
import FolderIcon from "@mui/icons-material/Folder";
|
|
|
|
import HomeIcon from "@mui/icons-material/Home";
|
|
|
|
import PeopleAltRoundedIcon from "@mui/icons-material/PeopleAltRounded";
|
|
|
|
import SettingsAccessibilityRoundedIcon from "@mui/icons-material/SettingsAccessibilityRounded";
|
|
|
|
import { CSSObject, Grid, IconButton, styled, Theme } from "@mui/material";
|
|
|
|
import MuiDrawer from "@mui/material/Drawer";
|
|
|
|
import { OverridableComponent } from "@mui/material/OverridableComponent";
|
|
|
|
import { useRouter } from "next/router";
|
|
|
|
import { FC, useMemo } from "react";
|
|
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
|
|
|
|
import { HexabotLogo } from "@/app-components/logos/HexabotLogo";
|
|
|
|
import { Sidebar } from "@/app-components/menus/Sidebar";
|
|
|
|
import { useAuth } from "@/hooks/useAuth";
|
2024-09-20 15:21:30 +00:00
|
|
|
import { useConfig } from "@/hooks/useConfig";
|
2024-09-10 09:50:11 +00:00
|
|
|
import { useHasPermission } from "@/hooks/useHasPermission";
|
|
|
|
import { EntityType } from "@/services/types";
|
|
|
|
import { PermissionAction } from "@/types/permission.types";
|
|
|
|
import { getLayout } from "@/utils/laylout";
|
|
|
|
|
|
|
|
const drawerWidth = 280;
|
|
|
|
const openedMixin = (theme: Theme): CSSObject => ({
|
|
|
|
width: drawerWidth,
|
|
|
|
transition: theme.transitions.create("width", {
|
|
|
|
easing: theme.transitions.easing.sharp,
|
|
|
|
duration: theme.transitions.duration.enteringScreen,
|
|
|
|
}),
|
|
|
|
overflowX: "hidden",
|
|
|
|
});
|
|
|
|
const closedMixin = (theme: Theme, isFloated: boolean): CSSObject => ({
|
|
|
|
...(isFloated && { position: "absolute" }),
|
|
|
|
transition: theme.transitions.create("width", {
|
|
|
|
easing: theme.transitions.easing.sharp,
|
|
|
|
duration: theme.transitions.duration.leavingScreen,
|
|
|
|
}),
|
|
|
|
overflowX: "hidden",
|
|
|
|
width: `calc(${theme.spacing(7)} + 1px)`,
|
|
|
|
[theme.breakpoints.up("sm")]: {
|
|
|
|
width: `calc(${theme.spacing(8)} + 1px)`,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
const DrawerHeader = styled("div")(({ theme }) => ({
|
|
|
|
display: "flex",
|
|
|
|
alignItems: "center",
|
|
|
|
justifyContent: "flex-end",
|
|
|
|
padding: theme.spacing(0, 1),
|
|
|
|
// necessary for content to be below app bar
|
|
|
|
...theme.mixins.toolbar,
|
|
|
|
}));
|
|
|
|
const Drawer = styled(MuiDrawer, {
|
|
|
|
shouldForwardProp: (prop) => prop !== "isToggled",
|
|
|
|
})(({ theme, open, ModalProps }) => ({
|
|
|
|
width: drawerWidth,
|
|
|
|
flexShrink: 0,
|
|
|
|
whiteSpace: "nowrap",
|
|
|
|
boxSizing: "border-box",
|
|
|
|
borderRadius: "0px",
|
|
|
|
...(open && {
|
|
|
|
...openedMixin(theme),
|
|
|
|
"& .MuiDrawer-paper": openedMixin(theme),
|
|
|
|
}),
|
|
|
|
...(!open && {
|
|
|
|
...closedMixin(theme, !!ModalProps?.open),
|
|
|
|
"& .MuiDrawer-paper": closedMixin(theme, !!ModalProps?.open),
|
|
|
|
}),
|
|
|
|
}));
|
|
|
|
const StyledDrawerHeader = styled(DrawerHeader)(() => ({
|
|
|
|
top: 0,
|
|
|
|
zIndex: 1,
|
|
|
|
position: "sticky",
|
|
|
|
maxHeight: "60px",
|
|
|
|
background: "#fffe",
|
|
|
|
}));
|
|
|
|
|
|
|
|
export type MenuItem = {
|
|
|
|
text: string;
|
|
|
|
href?: string;
|
|
|
|
Icon?: OverridableComponent<any> | IconDefinition;
|
|
|
|
requires?: { [key in EntityType]?: PermissionAction[] };
|
|
|
|
submenuItems?: MenuItem[];
|
|
|
|
};
|
|
|
|
|
2024-09-20 15:21:30 +00:00
|
|
|
const getMenuItems = (ssoEnabled: boolean): MenuItem[] => [
|
2024-09-10 09:50:11 +00:00
|
|
|
{
|
|
|
|
text: "menu.dashboard",
|
|
|
|
href: "/",
|
|
|
|
Icon: HomeIcon,
|
|
|
|
requires: {
|
|
|
|
[EntityType.BOTSTATS]: [PermissionAction.READ],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: "menu.visual_editor",
|
|
|
|
href: "/visual-editor",
|
|
|
|
Icon: AppsIcon,
|
|
|
|
requires: {
|
|
|
|
[EntityType.BLOCK]: [PermissionAction.READ],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: "menu.nlp",
|
|
|
|
href: "/nlp",
|
|
|
|
Icon: faGraduationCap,
|
|
|
|
requires: {
|
|
|
|
[EntityType.NLP_SAMPLE]: [PermissionAction.READ],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: "menu.inbox",
|
|
|
|
href: "/inbox",
|
|
|
|
Icon: faComments,
|
|
|
|
requires: {
|
|
|
|
[EntityType.MESSAGE]: [PermissionAction.READ],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: "menu.categories",
|
|
|
|
href: "/categories",
|
|
|
|
Icon: FolderIcon,
|
|
|
|
requires: {
|
|
|
|
[EntityType.CATEGORY]: [PermissionAction.READ],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: "menu.context_vars",
|
|
|
|
href: "/context-vars",
|
|
|
|
Icon: faAsterisk,
|
|
|
|
requires: {
|
|
|
|
[EntityType.CONTEXT_VAR]: [PermissionAction.READ],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: "menu.manage_content",
|
|
|
|
Icon: faDatabase,
|
|
|
|
submenuItems: [
|
|
|
|
{
|
|
|
|
text: "menu.persistent_menu",
|
|
|
|
href: "/content/persistent-menu",
|
|
|
|
Icon: faBars,
|
|
|
|
requires: {
|
|
|
|
[EntityType.MENU]: [PermissionAction.READ],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: "menu.cms",
|
|
|
|
href: "/content/types",
|
|
|
|
Icon: faAlignLeft,
|
|
|
|
requires: {
|
|
|
|
[EntityType.CONTENT_TYPE]: [PermissionAction.READ],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: "menu.media_library",
|
|
|
|
href: "/content/media-library",
|
|
|
|
Icon: DriveFolderUploadIcon,
|
|
|
|
requires: {
|
|
|
|
[EntityType.ATTACHMENT]: [PermissionAction.READ],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: "menu.manage_subscribers",
|
|
|
|
Icon: faUserCircle,
|
|
|
|
submenuItems: [
|
|
|
|
{
|
|
|
|
text: "menu.subscribers",
|
|
|
|
href: "/subscribers",
|
|
|
|
Icon: faUserCircle,
|
|
|
|
requires: {
|
|
|
|
[EntityType.SUBSCRIBER]: [PermissionAction.READ],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: "menu.labels",
|
|
|
|
href: "/subscribers/labels",
|
|
|
|
Icon: faTags,
|
|
|
|
requires: {
|
|
|
|
[EntityType.LABEL]: [PermissionAction.READ],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
// {
|
|
|
|
// text: 'menu.broadcast',
|
|
|
|
// href: "/subscribers/broadcast",
|
|
|
|
// Icon: faBullhorn,
|
|
|
|
// },
|
|
|
|
],
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: "menu.admin",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: "menu.manage_users",
|
|
|
|
Icon: faUsers,
|
|
|
|
submenuItems: [
|
|
|
|
{
|
|
|
|
text: "menu.users",
|
|
|
|
href: "/users",
|
|
|
|
Icon: PeopleAltRoundedIcon,
|
|
|
|
requires: {
|
|
|
|
[EntityType.USER]: [PermissionAction.READ],
|
|
|
|
},
|
|
|
|
},
|
2024-09-20 15:21:30 +00:00
|
|
|
...(!ssoEnabled
|
2024-09-10 09:50:11 +00:00
|
|
|
? [
|
|
|
|
{
|
|
|
|
text: "menu.roles",
|
|
|
|
href: "/roles",
|
|
|
|
Icon: SettingsAccessibilityRoundedIcon,
|
|
|
|
requires: {
|
|
|
|
[EntityType.ROLE]: [PermissionAction.READ],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
]
|
|
|
|
: []),
|
|
|
|
],
|
|
|
|
},
|
2024-09-22 11:51:56 +00:00
|
|
|
{
|
|
|
|
text: "menu.manage_localization",
|
|
|
|
Icon: Language,
|
|
|
|
submenuItems: [
|
|
|
|
{
|
|
|
|
text: "menu.languages",
|
|
|
|
href: "/localization/languages",
|
|
|
|
Icon: Flag,
|
|
|
|
requires: {
|
|
|
|
[EntityType.LANGUAGE]: [PermissionAction.READ],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: "menu.translations",
|
|
|
|
href: "/localization/translations",
|
|
|
|
Icon: faLanguage,
|
|
|
|
requires: {
|
|
|
|
[EntityType.TRANSLATION]: [PermissionAction.READ],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
2024-09-10 09:50:11 +00:00
|
|
|
{
|
|
|
|
text: "menu.settings",
|
|
|
|
href: "/settings",
|
|
|
|
Icon: faCogs,
|
|
|
|
requires: {
|
|
|
|
[EntityType.SETTING]: [PermissionAction.READ, PermissionAction.UPDATE],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
];
|
|
|
|
|
|
|
|
export type VerticalMenuProps = {
|
|
|
|
isSideBarOpen: boolean;
|
|
|
|
onToggleIn: () => void;
|
|
|
|
onToggleOut: () => void;
|
|
|
|
};
|
|
|
|
|
|
|
|
export const VerticalMenu: FC<VerticalMenuProps> = ({
|
|
|
|
isSideBarOpen,
|
|
|
|
onToggleIn,
|
|
|
|
onToggleOut,
|
|
|
|
}) => {
|
2024-09-20 15:21:30 +00:00
|
|
|
const { ssoEnabled } = useConfig();
|
2024-09-10 09:50:11 +00:00
|
|
|
const { t } = useTranslation();
|
|
|
|
const { isAuthenticated } = useAuth();
|
|
|
|
const router = useRouter();
|
|
|
|
const hasPermission = useHasPermission();
|
2024-09-20 15:21:30 +00:00
|
|
|
const menuItems = getMenuItems(ssoEnabled);
|
2024-09-10 09:50:11 +00:00
|
|
|
// Filter menu item to which user is allowed access
|
|
|
|
const availableMenuItems = useMemo(() => {
|
2024-09-20 15:21:30 +00:00
|
|
|
return menuItems.filter(({ requires: requiredPermissions }) => {
|
2024-09-10 09:50:11 +00:00
|
|
|
return (
|
|
|
|
!requiredPermissions ||
|
|
|
|
Object.entries(requiredPermissions).every((permission) => {
|
|
|
|
const entityType = permission[0] as EntityType;
|
|
|
|
const actions = permission[1];
|
|
|
|
|
|
|
|
return actions.every((action) => hasPermission(entityType, action));
|
|
|
|
})
|
|
|
|
);
|
|
|
|
});
|
|
|
|
}, [t, hasPermission]);
|
|
|
|
const hasTemporaryDrawer =
|
|
|
|
getLayout(router.pathname.slice(1)) === "full_width";
|
|
|
|
|
|
|
|
return isAuthenticated ? (
|
|
|
|
<Drawer
|
|
|
|
open={isSideBarOpen}
|
|
|
|
ModalProps={{
|
|
|
|
open: isSideBarOpen && hasTemporaryDrawer,
|
|
|
|
keepMounted: true,
|
|
|
|
}}
|
|
|
|
variant={hasTemporaryDrawer ? "temporary" : "permanent"}
|
|
|
|
onClose={(_, reason) => {
|
|
|
|
reason === "backdropClick" && onToggleOut();
|
|
|
|
}}
|
|
|
|
PaperProps={{
|
|
|
|
sx: { borderRadius: "0px" },
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<StyledDrawerHeader>
|
|
|
|
<Grid item xs ml="10px">
|
|
|
|
<HexabotLogo />
|
|
|
|
</Grid>
|
|
|
|
<Grid>
|
|
|
|
<IconButton onClick={onToggleOut}>
|
|
|
|
<ChevronLeftIcon />
|
|
|
|
</IconButton>
|
|
|
|
</Grid>
|
|
|
|
</StyledDrawerHeader>
|
|
|
|
<Sidebar
|
|
|
|
menu={availableMenuItems}
|
|
|
|
pathname={router.pathname}
|
|
|
|
isToggled={isSideBarOpen}
|
|
|
|
toggleFunction={() => (isSideBarOpen ? onToggleOut() : onToggleIn())}
|
|
|
|
/>
|
|
|
|
</Drawer>
|
|
|
|
) : null;
|
|
|
|
};
|