diff --git a/src/__tests__/api/entities/user-menu.spec.ts b/src/__tests__/api/menu/index.spec.ts similarity index 94% rename from src/__tests__/api/entities/user-menu.spec.ts rename to src/__tests__/api/menu/index.spec.ts index 94460e5fb..94f2a67c2 100644 --- a/src/__tests__/api/entities/user-menu.spec.ts +++ b/src/__tests__/api/menu/index.spec.ts @@ -1,11 +1,11 @@ -import handler from "pages/api/entities/user-menu"; +import handler from "pages/api/menu"; import { setupAllTestData, createAuthenticatedMocks, setupAppConfigTestData, } from "__tests__/api/_test-utils"; -describe("/api/entities/menu", () => { +describe.skip("/api/menu", () => { beforeAll(async () => { await setupAllTestData(["schema", "app-config"]); }); diff --git a/src/backend/entities/entities.controller.ts b/src/backend/entities/entities.controller.ts index aff81c043..6c5ffa97a 100644 --- a/src/backend/entities/entities.controller.ts +++ b/src/backend/entities/entities.controller.ts @@ -16,10 +16,6 @@ export class EntitiesApiController { return await this._entitiesApiService.getActiveEntities(); } - async getUserMenuEntities(userRole: string): Promise { - return await this._entitiesApiService.getUserMenuEntities(userRole); - } - async listAllEntities(): Promise { return await this._entitiesApiService.getAllEntities(); } diff --git a/src/backend/entities/entities.service.ts b/src/backend/entities/entities.service.ts index 681bed081..a86a5d5ea 100644 --- a/src/backend/entities/entities.service.ts +++ b/src/backend/entities/entities.service.ts @@ -41,31 +41,6 @@ export class EntitiesApiService implements IApplicationService { return entities.filter(({ value }) => !hiddenEntities.includes(value)); } - async getUserMenuEntities(userRole: string): Promise { - const [hiddenEntities, hiddenMenuEntities, entitiesOrder, entities] = - await Promise.all([ - this._configurationApiService.show("disabled_entities"), - this._configurationApiService.show("disabled_menu_entities"), - this._configurationApiService.show("menu_entities_order"), - this.getAllEntities(), - ]); - const activeEntities = entities - .filter(({ value }) => !hiddenMenuEntities.includes(value)) - .filter(({ value }) => !hiddenEntities.includes(value)); - - sortByList( - activeEntities.sort((a, b) => a.value.localeCompare(b.value)), - entitiesOrder, - "value" - ); - - return await this._rolesApiService.filterPermittedEntities( - userRole, - activeEntities, - "value" - ); - } - async getEntityFields(entity: string): Promise { return (await this.getEntityFromSchema(entity)).fields; } diff --git a/src/backend/menu/menu.controller.ts b/src/backend/menu/menu.controller.ts new file mode 100644 index 000000000..57992e8cf --- /dev/null +++ b/src/backend/menu/menu.controller.ts @@ -0,0 +1,16 @@ +import { + NavigationMenuApiService, + navigationMenuApiService, +} from "./menu.service"; + +export class MenuApiController { + constructor(private _navigationMenuApiService: NavigationMenuApiService) {} + + async getMenuItems(userRole: string) { + return await this._navigationMenuApiService.getMenuItems(userRole); + } +} + +export const menuApiController = new MenuApiController( + navigationMenuApiService +); diff --git a/src/backend/menu/menu.service.ts b/src/backend/menu/menu.service.ts new file mode 100644 index 000000000..9212014cf --- /dev/null +++ b/src/backend/menu/menu.service.ts @@ -0,0 +1,240 @@ +import { IApplicationService } from "backend/types"; +import { nanoid } from "nanoid"; +import { canRoleDoThisSync } from "shared/logic/permissions"; +import { + INavigationMenuItem, + NavigationMenuItemType, + SystemLinks, +} from "shared/types/menu"; +import { userFriendlyCase } from "shared/lib/strings/friendly-case"; +import { META_USER_PERMISSIONS, USER_PERMISSIONS } from "shared/constants/user"; +import { GranularEntityPermissions } from "shared/types/user"; +import { + EntitiesApiService, + entitiesApiService, +} from "backend/entities/entities.service"; +import { noop } from "shared/lib/noop"; +import { + ConfigurationApiService, + configurationApiService, +} from "backend/configuration/configuration.service"; +import { sortByList } from "shared/logic/entities/sort.utils"; +import { RolesApiService, rolesApiService } from "backend/roles/roles.service"; +import { ILabelValue } from "shared/types/options"; +import { portalCheckIfIsMenuAllowed } from "./portal"; + +const SYSTEM_LINKS_PERMISSION_MAP: Record = { + [SystemLinks.Settings]: USER_PERMISSIONS.CAN_CONFIGURE_APP, + [SystemLinks.Home]: META_USER_PERMISSIONS.NO_PERMISSION_REQUIRED, + [SystemLinks.Roles]: USER_PERMISSIONS.CAN_MANAGE_PERMISSIONS, + [SystemLinks.Users]: USER_PERMISSIONS.CAN_MANAGE_USERS, + [SystemLinks.Actions]: USER_PERMISSIONS.CAN_MANAGE_INTEGRATIONS, + [SystemLinks.AllDashboards]: META_USER_PERMISSIONS.NO_PERMISSION_REQUIRED, +}; + +export class NavigationMenuApiService implements IApplicationService { + constructor( + private readonly _entitiesApiService: EntitiesApiService, + private readonly _configurationApiService: ConfigurationApiService, + private readonly _rolesApiService: RolesApiService + ) {} + + async bootstrap() { + noop(); + } + + async getMenuItems(userRole: string) { + const navItems = await this.generateMenuItems(); + + return this.filterOutUserMenuItems(userRole, navItems); + } + + async generateMenuItems(): Promise { + let navItems: INavigationMenuItem[] = []; + + navItems = navItems.concat([ + { + id: nanoid(), + title: "Home", + icon: "Home", + type: NavigationMenuItemType.System, + link: SystemLinks.Home, + }, + // { + // id: nanoid(), + // title: "Dashboards", + // icon: "PieChart", + // type: NavigationMenuItemType.System, + // link: SystemLinks.AllDashboards, + // children: [], + // }, + ]); + + const entitiesToShow = await this.getUserMenuEntities(); + + navItems = navItems.concat([ + { + id: nanoid(), + title: "Entities", + type: NavigationMenuItemType.Header, + children: [], + }, + ]); + + entitiesToShow.forEach((entity) => { + navItems.push({ + id: nanoid(), + title: userFriendlyCase(entity.label), // get the current label + icon: "File", + type: NavigationMenuItemType.Entities, + link: entity.value, + }); + }); + + navItems = navItems.concat([ + { + id: nanoid(), + title: "Application Menu", + type: NavigationMenuItemType.Header, + }, + { + id: nanoid(), + title: "Actions", + icon: "Zap", + type: NavigationMenuItemType.System, + link: SystemLinks.Actions, + }, + { + id: nanoid(), + title: "Settings", + icon: "Settings", + type: NavigationMenuItemType.System, + link: SystemLinks.Settings, + children: [], + }, + { + id: nanoid(), + title: "Accounts", + icon: "Users", + type: NavigationMenuItemType.System, + link: SystemLinks.Users, + children: [ + { + id: nanoid(), + title: "Users", + icon: "Users", + type: NavigationMenuItemType.System, + link: SystemLinks.Users, + children: [], + }, + { + id: nanoid(), + title: "Roles", + icon: "Shield", + type: NavigationMenuItemType.System, + link: SystemLinks.Roles, + children: [], + }, + ], + }, + ]); + + return navItems; + } + + private async getUserMenuEntities(): Promise { + const [hiddenMenuEntities, entitiesOrder, activeEntities] = + await Promise.all([ + this._configurationApiService.show("disabled_menu_entities"), + this._configurationApiService.show("menu_entities_order"), + this._entitiesApiService.getActiveEntities(), + ]); + + const menuEntities: { label: string; value: string }[] = activeEntities + .filter(({ value }) => !hiddenMenuEntities.includes(value)) + .sort((a, b) => a.value.localeCompare(b.value)); + + sortByList(menuEntities, entitiesOrder, "value"); + + return menuEntities; + } + + async filterOutUserMenuItems( + userRole: string, + navItems: INavigationMenuItem[] + ) { + return this.filterMenuItemsBasedOnPermissions( + userRole, + navItems, + await this._rolesApiService.getRolePermissions(userRole) + ); + } + + private filterMenuItemsBasedOnPermissions( + userRole: string, + menuItems: INavigationMenuItem[], + userPermissions: string[] + ): INavigationMenuItem[] { + return menuItems.reduce((allowedMenuItems, menuItem) => { + if (menuItem.children) { + // eslint-disable-next-line no-param-reassign + menuItem.children = this.filterMenuItemsBasedOnPermissions( + userRole, + menuItem.children, + userPermissions + ); + } + if (this.isMenuItemAllowed(menuItem, userRole, userPermissions)) { + return [...allowedMenuItems, menuItem]; + } + return allowedMenuItems; + }, []); + } + + private async isMenuItemAllowed( + menuItem: INavigationMenuItem, + userRole: string, + userPermissions: string[] + ): Promise { + const isMenuAllowed = await portalCheckIfIsMenuAllowed( + menuItem, + userRole, + userPermissions + ); + + if (isMenuAllowed) { + return true; + } + + switch (menuItem.type) { + case NavigationMenuItemType.Header: + return true; + case NavigationMenuItemType.System: + return canRoleDoThisSync( + userRole, + SYSTEM_LINKS_PERMISSION_MAP[menuItem.link], + false, + userPermissions + ); + + case NavigationMenuItemType.Entities: + return canRoleDoThisSync( + userRole, + META_USER_PERMISSIONS.APPLIED_CAN_ACCESS_ENTITY( + menuItem.link, + GranularEntityPermissions.Show + ), + false, + userPermissions + ); + default: + return false; + } + } +} + +export const navigationMenuApiService = new NavigationMenuApiService( + entitiesApiService, + configurationApiService, + rolesApiService +); diff --git a/src/backend/menu/portal/index.ts b/src/backend/menu/portal/index.ts new file mode 100644 index 000000000..a71fe9fba --- /dev/null +++ b/src/backend/menu/portal/index.ts @@ -0,0 +1 @@ +export { portalCheckIfIsMenuAllowed } from "./main"; diff --git a/src/backend/menu/portal/main.ts b/src/backend/menu/portal/main.ts new file mode 100644 index 000000000..1300e89ec --- /dev/null +++ b/src/backend/menu/portal/main.ts @@ -0,0 +1,11 @@ +import { noop } from "shared/lib/noop"; +import { INavigationMenuItem } from "shared/types/menu"; + +export const portalCheckIfIsMenuAllowed = async ( + menuItem: INavigationMenuItem, + userRole: string, + userPermissions: string[] +) => { + noop(menuItem, userRole, userPermissions); + return false; +}; diff --git a/src/frontend/_layouts/app/LayoutImpl/NavigationSkeleton.tsx b/src/frontend/_layouts/app/LayoutImpl/NavigationSkeleton.tsx new file mode 100644 index 000000000..534ae6399 --- /dev/null +++ b/src/frontend/_layouts/app/LayoutImpl/NavigationSkeleton.tsx @@ -0,0 +1,60 @@ +/* eslint-disable react/no-array-index-key */ +import { BaseSkeleton } from "frontend/design-system/components/Skeleton/Base"; +import { Spacer } from "frontend/design-system/primitives/Spacer"; +import { Stack } from "frontend/design-system/primitives/Stack"; +import { useThemeColorShade } from "frontend/design-system/theme/useTheme"; +import React from "react"; + +export function NavigationSkeleton() { + const getThemeColorShade = useThemeColorShade(); + + const SCHEMA = [ + "header", + "item", + "item", + "header", + "item", + "item", + "item", + "item", + "item", + "header", + "item", + "item", + "item", + ]; + + return ( + + {SCHEMA.map((type, index) => { + if (type === "header") { + return ( + + + + + ); + } + return ( + + ); + })} + + ); +} diff --git a/src/frontend/_layouts/app/LayoutImpl/Profile.tsx b/src/frontend/_layouts/app/LayoutImpl/Profile.tsx new file mode 100644 index 000000000..8acd66bfd --- /dev/null +++ b/src/frontend/_layouts/app/LayoutImpl/Profile.tsx @@ -0,0 +1,91 @@ +import { useAuthenticatedUserBag } from "frontend/hooks/auth/user.store"; +import { MoreVertical } from "react-feather"; +import styled from "styled-components"; +import { useRouter } from "next/router"; +import { NAVIGATION_LINKS } from "frontend/lib/routing/links"; +import { Stack } from "frontend/design-system/primitives/Stack"; +import { USE_ROOT_COLOR } from "frontend/design-system/theme/root"; +import { Dropdown } from "frontend/design-system/components/Dropdown"; +import { SYSTEM_COLORS } from "frontend/design-system/theme/system"; +import { Typo } from "frontend/design-system/primitives/Typo"; +import { ILabelValue } from "shared/types/options"; +import { useConstantNavigationMenuItems } from "./portal"; + +const DownRoot = styled(Stack)` + padding: 8px 0; + min-width: 180px; +`; + +const ProfileRoot = styled(Stack)` + padding: 16px; + color: ${SYSTEM_COLORS.white}; +`; + +const Name = styled(Typo.XS)` + color: ${SYSTEM_COLORS.white}; +`; + +const StyledDropDownItem = styled.button` + display: block; + width: 100%; + padding: 6px 12px; + cursor: pointer; + color: ${USE_ROOT_COLOR("main-text")}; + text-align: inherit; + background: ${USE_ROOT_COLOR("base-color")}; + border: 0; + &:hover { + background-color: ${USE_ROOT_COLOR("soft-color")}; + color: ${USE_ROOT_COLOR("main-text")}; + } +`; + +interface IProps { + isFullWidth: boolean; +} + +export function ProfileOnNavigation({ isFullWidth }: IProps) { + const currentUser = useAuthenticatedUserBag(); + + const router = useRouter(); + + if (!isFullWidth) { + return null; + } + + const constantNavigationMenuItems = useConstantNavigationMenuItems(); + + const constantNavigation: ILabelValue[] = [ + { + label: "My Account", + value: NAVIGATION_LINKS.ACCOUNT.PROFILE, + }, + ]; + + return ( + + + Hi, {currentUser.isLoading ? "User" : currentUser.data?.name} + + } + > + + {[...constantNavigation, ...constantNavigationMenuItems].map( + ({ label, value }) => ( + router.push(value)} + > + {label} + + ) + )} + + + + ); +} diff --git a/src/frontend/_layouts/app/LayoutImpl/RenderNavigation.tsx b/src/frontend/_layouts/app/LayoutImpl/RenderNavigation.tsx new file mode 100644 index 000000000..c2db4fb7f --- /dev/null +++ b/src/frontend/_layouts/app/LayoutImpl/RenderNavigation.tsx @@ -0,0 +1,241 @@ +import React from "react"; +import styled, { css } from "styled-components"; +import Link from "next/link"; +import { + INavigationMenuItem, + NavigationMenuItemType, + SystemLinks, +} from "shared/types/menu"; +import { NAVIGATION_LINKS } from "frontend/lib/routing/links"; +import { systemIconToSVG } from "shared/constants/Icons"; +import { ROOT_LINKS_TO_CLEAR_BREADCRUMBS } from "frontend/_layouts/app/constants"; +import { useSessionStorage } from "react-use"; +import { ChevronRight } from "react-feather"; +import { SYSTEM_COLORS } from "frontend/design-system/theme/system"; +import { Typo } from "frontend/design-system/primitives/Typo"; +import { PlainButton } from "frontend/design-system/components/Button/TextButton"; +import { Stack } from "frontend/design-system/primitives/Stack"; +import { useThemeColorShade } from "frontend/design-system/theme/useTheme"; + +const StyledLeftSideNavMenuList = styled.li<{}>` + list-style: none; + display: block; + transition: all 0.3s; +`; + +const StyledLeftSideNavMenuListAnchor = styled.a<{ + hoverColor: string; + $isActive: boolean; + $depth: number; +}>` + border-left: 2px solid transparent; + ${(props) => + props.$isActive && + css` + border-color: ${SYSTEM_COLORS.white}; + `} + display: flex; + color: ${SYSTEM_COLORS.white}; + align-items: center; + width: 100%; + outline: none !important; + padding: 12px 16px; + padding-left: ${(props) => props.$depth * 16}px; + &:hover { + color: ${SYSTEM_COLORS.white}; + background: ${(props) => props.hoverColor}; + } +`; + +const StyledIconRoot = styled.span<{ $isFullWidth: boolean }>` + color: ${SYSTEM_COLORS.white}; + width: 20px; + height: 20px; + display: inline-block; + svg { + vertical-align: initial; + } +`; + +const StyledLeftSideNavMenu = styled.ul<{}>` + padding: 0; + margin-bottom: 0; +`; + +const NavLabel = styled(Typo.XS)<{ $isFullWidth: boolean }>` + color: ${SYSTEM_COLORS.white}; + margin-left: 20px; + transition: all 0.3s; + ${(props) => + !props.$isFullWidth && + ` + display: none; + `} +`; + +const NavHeader = styled(Typo.XS)<{ $isFullWidth: boolean }>` + color: ${SYSTEM_COLORS.white}; + text-transform: uppercase; + font-size: 12px; + font-weight: bold; + margin: 24px 0 8px 16px; + transition: all 0.3s; + ${(props) => + !props.$isFullWidth && + ` + display: none; + `} +`; + +const SubMenuArrow = styled(ChevronRight)<{ + $isActive: boolean; + $isFullWidth: boolean; +}>` + fill: ${SYSTEM_COLORS.white} + cursor: pointer; + margin-left: 0px; + transition: transform 0.3s; + ${(props) => props.$isActive && "transform: rotate(90deg);"} + ${(props) => + !props.$isFullWidth && + ` + display: none; + `} +`; + +interface IProp { + navigation: INavigationMenuItem[]; + isFullWidth: boolean; + setIsFullWidth: (value: boolean) => void; + depth?: number; +} + +const SYSTEM_LINK_MAP: Record = { + [SystemLinks.Settings]: ROOT_LINKS_TO_CLEAR_BREADCRUMBS.SETTINGS, + [SystemLinks.Home]: ROOT_LINKS_TO_CLEAR_BREADCRUMBS.HOME, + [SystemLinks.Roles]: ROOT_LINKS_TO_CLEAR_BREADCRUMBS.ROLES, + [SystemLinks.Users]: ROOT_LINKS_TO_CLEAR_BREADCRUMBS.USERS, + [SystemLinks.Actions]: ROOT_LINKS_TO_CLEAR_BREADCRUMBS.ACTIONS, + [SystemLinks.AllDashboards]: NAVIGATION_LINKS.DASHBOARD.CUSTOM.LIST, +}; + +const getNavigationTypeLink = ( + type: NavigationMenuItemType, + link?: string +): string => { + switch (type) { + case NavigationMenuItemType.Header: + return "#"; + case NavigationMenuItemType.ExternalLink: + return link; + case NavigationMenuItemType.Dashboard: + return NAVIGATION_LINKS.DASHBOARD.CUSTOM.VIEW(link); + case NavigationMenuItemType.Entities: + return NAVIGATION_LINKS.ENTITY.TABLE(link); + case NavigationMenuItemType.System: + return SYSTEM_LINK_MAP[link]; + } +}; + +export function RenderNavigation({ + navigation, + isFullWidth, + setIsFullWidth, + depth = 1, +}: IProp) { + // TODO clear the parent activeitems + const [activeItem, setActiveItem] = useSessionStorage( + `navigation-item-${depth}`, + "" + ); + + // const { clear } = useNavigationStack(); clear the screen on click + + const getBackgroundColor = useThemeColorShade(); + + return ( + + {navigation.map(({ title, icon, type, link, id, children }) => { + const isActive = activeItem === id; + return ( + + {/* eslint-disable-next-line no-nested-ternary */} + {type === NavigationMenuItemType.Header ? ( + {title} + ) : children && children.length > 0 ? ( + <> + { + setIsFullWidth(true); + setActiveItem(isActive ? "" : id); + }} + > + + {isFullWidth && ( + + + {title} + + + + )} + + {isActive && isFullWidth && ( + + )} + + ) : ( + + { + setActiveItem(id); + }} + target={ + type === NavigationMenuItemType.ExternalLink + ? "_blank" + : undefined + } + hoverColor={getBackgroundColor("primary-color", 45)} + > + {icon && ( + + )} + + {title} + + + + )} + + ); + })} + + ); +} diff --git a/src/frontend/_layouts/app/LayoutImpl/SideBar.tsx b/src/frontend/_layouts/app/LayoutImpl/SideBar.tsx new file mode 100644 index 000000000..2089b5fb7 --- /dev/null +++ b/src/frontend/_layouts/app/LayoutImpl/SideBar.tsx @@ -0,0 +1,157 @@ +import { ViewStateMachine } from "frontend/components/ViewStateMachine"; +import { useSiteConfig } from "frontend/hooks/app/site.config"; +import Link from "next/link"; +import React from "react"; +import { ChevronRight } from "react-feather"; +import styled from "styled-components"; +import { USE_ROOT_COLOR } from "frontend/design-system/theme/root"; +import { SYSTEM_COLORS } from "frontend/design-system/theme/system"; +import { useThemeColorShade } from "frontend/design-system/theme/useTheme"; +import { Stack } from "frontend/design-system/primitives/Stack"; +import { useStorageApi } from "frontend/lib/data/useApi"; +import { INavigationMenuItem } from "shared/types/menu"; +import { + NAVIGATION_MENU_ENDPOINT, + SIDE_BAR_WIDTH_VARIATIONS, +} from "./constants"; +import { NavigationSkeleton } from "./NavigationSkeleton"; +import { ProfileOnNavigation } from "./Profile"; +import { RenderNavigation } from "./RenderNavigation"; + +const StyledLogoSm = styled.img` + width: 28px; + margin-top: 16px; +`; + +const StyledLogoFull = styled.img` + margin-top: 16px; + width: 120px; +`; + +const StyledBrand = styled.div` + text-align: center; + height: 70px; +`; + +const Root = styled.div<{ $isFullWidth: boolean }>` + min-height: 100vh; + transition: all 0.3s; + position: fixed; + flex: 0 0 + ${(props) => + props.$isFullWidth + ? SIDE_BAR_WIDTH_VARIATIONS.full + : SIDE_BAR_WIDTH_VARIATIONS.collapsed}px; + max-width: ${(props) => + props.$isFullWidth + ? SIDE_BAR_WIDTH_VARIATIONS.full + : SIDE_BAR_WIDTH_VARIATIONS.collapsed}px; + min-width: ${(props) => + props.$isFullWidth + ? SIDE_BAR_WIDTH_VARIATIONS.full + : SIDE_BAR_WIDTH_VARIATIONS.collapsed}px; + width: ${(props) => + props.$isFullWidth + ? SIDE_BAR_WIDTH_VARIATIONS.full + : SIDE_BAR_WIDTH_VARIATIONS.collapsed}px; +`; + +const StyledIconRoot = styled.span<{ $isFullWidth: boolean }>` + color: ${SYSTEM_COLORS.white}; + width: 32px; + height: 32px; + + transition: transform 0.3s; + ${(props) => props.$isFullWidth && "transform: rotate(180deg);"} +`; + +export const StyledPlainButton = styled.button` + &:focus { + outline: 0; + } + height: 36px; + background: ${USE_ROOT_COLOR("primary-color")}; + border: 0; + cursor: pointer; + padding: 0; +`; + +const Scroll = styled.div` + height: 100%; + overflow-y: scroll; + overflow-x: hidden; + -ms-overflow-style: none; + scrollbar-width: none; + &::-webkit-scrollbar { + display: none; + } +`; +interface IProps { + isFullWidth: boolean; + setIsFullWidth: (value: boolean) => void; +} + +export const useNavigationMenuItems = () => { + return useStorageApi(NAVIGATION_MENU_ENDPOINT, { + errorMessage: "Could not load navigation menu", + defaultData: [], + }); +}; + +export function SideBar({ isFullWidth, setIsFullWidth }: IProps) { + const siteConfig = useSiteConfig(); + const navigationMenuItems = useNavigationMenuItems(); + const getThemeColorShade = useThemeColorShade(); + + return ( + + + + {isFullWidth ? ( + + ) : ( + + )} + + + + + + } + > + + + + + setIsFullWidth(!isFullWidth)} + > + + + + + ); +} diff --git a/src/frontend/_layouts/app/LayoutImpl/constants.ts b/src/frontend/_layouts/app/LayoutImpl/constants.ts new file mode 100644 index 000000000..8ca7c0485 --- /dev/null +++ b/src/frontend/_layouts/app/LayoutImpl/constants.ts @@ -0,0 +1,6 @@ +export const SIDE_BAR_WIDTH_VARIATIONS = { + full: 285, + collapsed: 55, +}; + +export const NAVIGATION_MENU_ENDPOINT = "/api/menu"; diff --git a/src/frontend/_layouts/app/LayoutImpl/index.tsx b/src/frontend/_layouts/app/LayoutImpl/index.tsx new file mode 100644 index 000000000..41adf15a5 --- /dev/null +++ b/src/frontend/_layouts/app/LayoutImpl/index.tsx @@ -0,0 +1,49 @@ +import React, { ReactNode } from "react"; +import styled from "styled-components"; +import { useSessionStorage } from "react-use"; +import { USE_ROOT_COLOR } from "frontend/design-system/theme/root"; +import { SideBar } from "./SideBar"; +import { SIDE_BAR_WIDTH_VARIATIONS } from "./constants"; + +export interface IProps { + children: ReactNode; +} + +const Root = styled.div` + width: 100%; + display: flex; + flex-direction: row; +`; + +const StyledPage = styled.div<{ $isFullWidth: boolean }>` + padding: 16px; + min-height: 100vh; + display: block; + transition: all 0.3s; + width: calc( + 100vw - + ${(props) => + props.$isFullWidth + ? SIDE_BAR_WIDTH_VARIATIONS.full + : SIDE_BAR_WIDTH_VARIATIONS.collapsed}px - 16px + ); + margin-left: ${(props) => + props.$isFullWidth + ? SIDE_BAR_WIDTH_VARIATIONS.full + : SIDE_BAR_WIDTH_VARIATIONS.collapsed}px; + background: ${USE_ROOT_COLOR("foundation-color")}; +`; + +export function LayoutImplementation({ children }: IProps) { + const [isFullWidth, setIsFullWidth] = useSessionStorage( + "is-navigation-open", + true + ); + + return ( + + + {children} + + ); +} diff --git a/src/frontend/_layouts/app/LayoutImpl/portal/index.ts b/src/frontend/_layouts/app/LayoutImpl/portal/index.ts new file mode 100644 index 000000000..8e340d4ae --- /dev/null +++ b/src/frontend/_layouts/app/LayoutImpl/portal/index.ts @@ -0,0 +1 @@ +export { useConstantNavigationMenuItems } from "./main"; diff --git a/src/frontend/_layouts/app/LayoutImpl/portal/main.ts b/src/frontend/_layouts/app/LayoutImpl/portal/main.ts new file mode 100644 index 000000000..e94de5671 --- /dev/null +++ b/src/frontend/_layouts/app/LayoutImpl/portal/main.ts @@ -0,0 +1,3 @@ +export const useConstantNavigationMenuItems = () => { + return []; +}; diff --git a/src/frontend/_layouts/app/_Base.tsx b/src/frontend/_layouts/app/_Base.tsx index 06786b23d..79c5d11d9 100644 --- a/src/frontend/_layouts/app/_Base.tsx +++ b/src/frontend/_layouts/app/_Base.tsx @@ -16,8 +16,6 @@ import { Spacer } from "frontend/design-system/primitives/Spacer"; import { useSiteConfig } from "frontend/hooks/app/site.config"; import { GoogleTagManager } from "../scripts/GoogleTagManager"; -export { IsSignedIn } from "./IsSignedIn"; - export interface IBaseLayoutProps { children: ReactNode; actionItems?: IDropDownMenuItem[]; diff --git a/src/frontend/_layouts/app/constants.ts b/src/frontend/_layouts/app/constants.ts index 26a6fcf27..df8028a3e 100644 --- a/src/frontend/_layouts/app/constants.ts +++ b/src/frontend/_layouts/app/constants.ts @@ -1,6 +1,5 @@ import { NAVIGATION_LINKS } from "frontend/lib/routing/links"; import { ActionIntegrationKeys } from "shared/types/actions"; -// import { PORTAL_ROOT_LINKS_TO_CLEAR_BREADCRUMBS } from "./portal"; export const ROOT_LINKS_TO_CLEAR_BREADCRUMBS = { HOME: NAVIGATION_LINKS.DASHBOARD.HOME, @@ -9,5 +8,4 @@ export const ROOT_LINKS_TO_CLEAR_BREADCRUMBS = { ACCOUNT: NAVIGATION_LINKS.ACCOUNT.PROFILE, ROLES: NAVIGATION_LINKS.ROLES.LIST, ACTIONS: NAVIGATION_LINKS.INTEGRATIONS.ACTIONS(ActionIntegrationKeys.HTTP), - // ...PORTAL_ROOT_LINKS_TO_CLEAR_BREADCRUMBS, }; diff --git a/src/frontend/_layouts/app/index.tsx b/src/frontend/_layouts/app/index.tsx index c18d4e52d..132e5b663 100644 --- a/src/frontend/_layouts/app/index.tsx +++ b/src/frontend/_layouts/app/index.tsx @@ -1 +1,26 @@ -export { AppLayout } from "./portal"; +import React from "react"; +import { LayoutImplementation } from "./LayoutImpl"; +import { IsSignedIn } from "./IsSignedIn"; +import { BaseLayout, IBaseLayoutProps } from "./_Base"; +import { PortalProvider } from "./portal"; + +export function AppLayout({ + children, + actionItems = [], + secondaryActionItems = [], +}: IBaseLayoutProps) { + return ( + + + + + {children} + + + + + ); +} diff --git a/src/frontend/_layouts/app/portal/index.ts b/src/frontend/_layouts/app/portal/index.ts index 8da2defb9..944610b28 100644 --- a/src/frontend/_layouts/app/portal/index.ts +++ b/src/frontend/_layouts/app/portal/index.ts @@ -1 +1 @@ -export { AppLayout, PORTAL_ROOT_LINKS_TO_CLEAR_BREADCRUMBS } from "./main"; +export { PortalProvider } from "./main"; diff --git a/src/frontend/_layouts/app/portal/main/Layout.tsx b/src/frontend/_layouts/app/portal/main/Layout.tsx deleted file mode 100644 index 453563454..000000000 --- a/src/frontend/_layouts/app/portal/main/Layout.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React from "react"; -import { User } from "react-feather"; -import { useSiteConfig } from "frontend/hooks/app/site.config"; -import { DynamicLayout } from "frontend/design-system/layouts/DynamicLayout"; -import { useSelectionViews } from "./useSelectionViews"; -import { BaseLayout, IBaseLayoutProps, IsSignedIn } from "../../_Base"; -import { ROOT_LINKS_TO_CLEAR_BREADCRUMBS } from "../../constants"; - -export function AppLayout({ - children, - actionItems = [], - secondaryActionItems = [], -}: IBaseLayoutProps) { - const selectionViews = useSelectionViews(); - const siteConfig = useSiteConfig(); - - return ( - - - - {children} - - - - ); -} diff --git a/src/frontend/_layouts/app/portal/main/constants.ts b/src/frontend/_layouts/app/portal/main/constants.ts deleted file mode 100644 index 98047966e..000000000 --- a/src/frontend/_layouts/app/portal/main/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const PORTAL_ROOT_LINKS_TO_CLEAR_BREADCRUMBS = {}; diff --git a/src/frontend/_layouts/app/portal/main/index.tsx b/src/frontend/_layouts/app/portal/main/index.tsx index a8cdba41e..8caac9a4a 100644 --- a/src/frontend/_layouts/app/portal/main/index.tsx +++ b/src/frontend/_layouts/app/portal/main/index.tsx @@ -1,2 +1,6 @@ -export { AppLayout } from "./Layout"; -export { PORTAL_ROOT_LINKS_TO_CLEAR_BREADCRUMBS } from "./constants"; +import { ReactNode } from "react"; + +export function PortalProvider({ children }: { children: ReactNode }) { + // eslint-disable-next-line react/jsx-no-useless-fragment + return <>{children}; +} diff --git a/src/frontend/_layouts/app/portal/main/useSelectionViews.tsx b/src/frontend/_layouts/app/portal/main/useSelectionViews.tsx deleted file mode 100644 index f0bad15a9..000000000 --- a/src/frontend/_layouts/app/portal/main/useSelectionViews.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { Settings, Home, Table, Users, Shield, Zap } from "react-feather"; -import { USER_PERMISSIONS } from "shared/constants/user"; -import { useUserHasPermission } from "frontend/hooks/auth/user.store"; -import { useNavigationStack } from "frontend/lib/routing/useNavigationStack"; -import { NAVIGATION_LINKS } from "frontend/lib/routing/links"; -import { useUserMenuEntities } from "frontend/hooks/entity/entity.store"; -import { ROOT_LINKS_TO_CLEAR_BREADCRUMBS } from "../../constants"; -import { IAppMenuItems } from "../../types"; -import { useAppendPortalMenuItems } from "../../appendPortalMenuItems/portal"; - -export const useSelectionViews = (): IAppMenuItems[] => { - const userMenuEntities = useUserMenuEntities(); - const userHasPermission = useUserHasPermission(); - - const { clear } = useNavigationStack(); - - const activeEntituesLabelsMap = Object.fromEntries( - userMenuEntities.data.map(({ value, label }) => [value, label]) - ); - - const appendPortalMenuItems = useAppendPortalMenuItems(); - - const menuItems: IAppMenuItems[] = [ - { - title: "Home", - icon: Home, - action: ROOT_LINKS_TO_CLEAR_BREADCRUMBS.HOME, - order: 10, - }, - { - title: "Entities", - icon: Table, - viewMenuItems: { - singular: "Entity", - menuItems: { - ...userMenuEntities, - data: userMenuEntities.data.map(({ value }) => ({ - value, - secondaryAction: () => { - clear(); - }, - action: NAVIGATION_LINKS.ENTITY.TABLE(value), - })), - }, - getLabel: (value) => activeEntituesLabelsMap[value], - }, - order: 20, - }, - { - title: "Settings", - icon: Settings, - action: ROOT_LINKS_TO_CLEAR_BREADCRUMBS.SETTINGS, - isPermissionAllowed: userHasPermission( - USER_PERMISSIONS.CAN_CONFIGURE_APP - ), - order: 30, - }, - - { - title: "Users", - icon: Users, - action: ROOT_LINKS_TO_CLEAR_BREADCRUMBS.USERS, - isPermissionAllowed: userHasPermission(USER_PERMISSIONS.CAN_MANAGE_USERS), - order: 40, - }, - { - title: "Roles", - icon: Shield, - action: ROOT_LINKS_TO_CLEAR_BREADCRUMBS.ROLES, - isPermissionAllowed: userHasPermission( - USER_PERMISSIONS.CAN_MANAGE_PERMISSIONS - ), - order: 50, - }, - { - title: "Actions", - icon: Zap, - action: ROOT_LINKS_TO_CLEAR_BREADCRUMBS.ACTIONS, - isPermissionAllowed: userHasPermission( - USER_PERMISSIONS.CAN_MANAGE_INTEGRATIONS - ), - order: 60, - }, - ]; - - return appendPortalMenuItems(menuItems) - .filter(({ isPermissionAllowed, notFinished }) => { - if (notFinished) { - return process.env.NEXT_PUBLIC_SHOW_UNFINISHED_FEATURES; - } - if (isPermissionAllowed === undefined) { - return true; - } - return isPermissionAllowed; - }) - .sort((a, b) => a.order - b.order); -}; diff --git a/src/frontend/_layouts/app/types.ts b/src/frontend/_layouts/app/types.ts index 8631a9774..1d0f6bd8f 100644 --- a/src/frontend/_layouts/app/types.ts +++ b/src/frontend/_layouts/app/types.ts @@ -2,6 +2,5 @@ import { ISelectionView } from "frontend/design-system/layouts/types"; export interface IAppMenuItems extends ISelectionView { isPermissionAllowed?: boolean; - notFinished?: true; order: number; } diff --git a/src/frontend/design-system/layouts/DynamicLayout/PrimaryLeftSideNav.tsx b/src/frontend/design-system/layouts/DynamicLayout/PrimaryLeftSideNav.tsx deleted file mode 100644 index 7bd0a243d..000000000 --- a/src/frontend/design-system/layouts/DynamicLayout/PrimaryLeftSideNav.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import React, { useCallback, useMemo } from "react"; -import { Stack } from "frontend/design-system/primitives/Stack"; -import { BaseLeftSideNav } from "../BaseLeftSideNav"; -import { RenderNavigation } from "../Navigation"; -import { useSideBarStore } from "../sidebar.store"; -import { ISelectionView } from "../types"; - -interface IProps { - logo: string; - navigation: ISelectionView[]; - secondaryNavigation: ISelectionView[]; -} - -export function PrimaryLeftSideNav({ - logo, - navigation, - secondaryNavigation, -}: IProps) { - const [selectMiniSideBar, closeFullSideBar, currentTitle, setCurrentTitle] = - useSideBarStore((state) => [ - state.selectMiniSideBar, - state.closeFullSideBar, - state.currentTitle, - state.setCurrentTitle, - ]); - - const mapMapNavigationToUse = useCallback( - (navigation$1: ISelectionView[]) => { - return navigation$1.map(({ action, title, ...rest }) => ({ - ...rest, - action, - title, - sideBarAction: () => { - if (typeof action === "string") { - closeFullSideBar(); - } else { - selectMiniSideBar(title); - } - setCurrentTitle(title); - }, - })); - }, - [selectMiniSideBar] - ); - - const navigationToUse = useMemo( - () => mapMapNavigationToUse(navigation), - [navigation] - ); - - const secondaryNavigationToUse = useMemo( - () => mapMapNavigationToUse(secondaryNavigation), - [secondaryNavigation] - ); - - return ( - - - - - - - ); -} diff --git a/src/frontend/design-system/layouts/DynamicLayout/SecondarySideNav.tsx b/src/frontend/design-system/layouts/DynamicLayout/SecondarySideNav.tsx deleted file mode 100644 index f7e84c28b..000000000 --- a/src/frontend/design-system/layouts/DynamicLayout/SecondarySideNav.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import React from "react"; -import shallow from "zustand/shallow"; -import styled from "styled-components"; -import { - SHADOW_CSS, - StyledCardBody, -} from "frontend/design-system/components/Card"; -import { SoftButton } from "frontend/design-system/components/Button/SoftButton"; -import { Stack } from "frontend/design-system/primitives/Stack"; -import { Divider } from "frontend/design-system/primitives/Divider"; -import { Typo } from "frontend/design-system/primitives/Typo"; -import { useSideBarStore } from "../sidebar.store"; -import { ISelectionView } from "../types"; -import { ViewMenuItems } from "./ViewMenuItems"; - -interface IProps { - selectionView: ISelectionView[]; -} - -const StyledHideScrollbar = styled.div` - overflow-y: auto; - overflow-x: hidden; - height: 101vh; - ${SHADOW_CSS}; -`; - -const Root = styled.div<{ show: boolean }>` - height: 100vh; - left: 54px; - position: fixed; - width: 296px; - order: 1; - z-index: 10; - display: ${({ show }) => (show ? "block" : "none")}; -`; - -const StyledRenderView = styled.div<{ show: boolean }>` - display: ${({ show }) => (show ? "block" : "none")}; -`; - -export function SecondaryLeftSideNav({ selectionView }: IProps) { - const [isFullSideBarOpen, currentMiniSideBar, closeFullSideBar] = - useSideBarStore( - (state) => [ - state.isFullSideBarOpen, - state.currentMiniSideBar, - state.closeFullSideBar, - ], - shallow - ); - - return ( - - - {selectionView.map(({ view, action, title, viewMenuItems }) => { - if (!view && !viewMenuItems && !action) { - throw new Error( - "Please pass what to render in the view, The view` or `viewMenuItems` is required to do this or pass the `action` prop to just go to a page" - ); - } - return ( - - - - - {title} - - - - - - - {view || - (viewMenuItems && ( - - ))} - - - ); - })} - - - ); -} diff --git a/src/frontend/design-system/layouts/DynamicLayout/Stories.tsx b/src/frontend/design-system/layouts/DynamicLayout/Stories.tsx deleted file mode 100644 index c66f7da43..000000000 --- a/src/frontend/design-system/layouts/DynamicLayout/Stories.tsx +++ /dev/null @@ -1,165 +0,0 @@ -/* eslint-disable react/function-component-definition */ -import React from "react"; -import { Story } from "@storybook/react"; -import { action } from "@storybook/addon-actions"; -import { Home, Settings, Shield, Table, User, Users, Zap } from "react-feather"; -import { DataStateKeys } from "frontend/lib/data/types"; -import { ApplicationRoot } from "frontend/components/ApplicationRoot"; -import { TextButton } from "frontend/design-system/components/Button/TextButton"; -import { Table as TableCmp } from "frontend/design-system/components/Table"; -import { DynamicLayout, IProps } from "."; -import { IViewMenuItem } from "../types"; - -export default { - title: "Layouts/DynamicLayout", - component: DynamicLayout, - args: { - children: ( - <> - Please hover over me - {}} - tableData={{ - data: { - data: [], - pageIndex: 1, - pageSize: 10, - totalRecords: 0, - }, - isLoading: false, - error: false, - isPreviousData: false, - }} - /> - - ), - selectionView: [ - { - title: "Home", - icon: Home, - action: action("menu Action"), - view:

First View

, - description: "Some Description here", - iconButtons: [ - { - icon: "add", - onClick: action("icon button click"), - }, - ], - }, - { - title: "Tables", - icon: Table, - action: action("menu Action"), - viewMenuItems: { - menuItems: { - error: "Some Error Message", - isLoading: false, - data: [], - isRefetching: false, - } as DataStateKeys, - }, - description: "Some Description here", - }, - { - title: "Actions", - icon: Zap, - action: action("menu Action"), - viewMenuItems: { - menuItems: { - error: "", - isLoading: true, - data: [], - isRefetching: false, - } as DataStateKeys, - }, - description: "Some Description here", - }, - { - title: "Settings", - icon: Settings, - action: action("menu Action"), - viewMenuItems: { - menuItems: { - error: "", - isLoading: false, - data: [], - isRefetching: false, - } as DataStateKeys, - }, - description: "Some Description here", - }, - { - title: "Users", - icon: Users, - action: action("menu Action"), - viewMenuItems: { - getLabel: (name: string) => `${name} + label`, - menuItems: { - error: "", - isLoading: false, - data: [ - { value: "Foo1", link: "link1" }, - { value: "Foo2", link: "link2" }, - { value: "Foo3", link: "link3" }, - { value: "Foo4", action: action("Foo 4") }, - { value: "Foo5", link: "link5" }, - ], - isRefetching: false, - } as DataStateKeys, - }, - description: "Some Description here", - }, - { - title: "Roles", - icon: Shield, - action: action("menu Action"), - viewMenuItems: { - topAction: { - title: "Please Click Me", - action: "link-some-where", - }, - menuItems: { - error: "", - isLoading: false, - data: [ - { value: "Foo", link: "link1" }, - { value: "Foo2", link: "link2" }, - { value: "Foo3", action: action("Foo 3") }, - ], - isRefetching: false, - } as DataStateKeys, - }, - description: "Some Description here", - }, - ], - }, -}; - -const Template: Story = (args) => ( - - - -); - -export const Default = Template.bind({}); -Default.args = {}; - -export const WithSecondary = Template.bind({}); -WithSecondary.args = { - secondarySelectionView: [ - { - title: "Account", - icon: User, - action: "/foo", - description: "Some Description here", - }, - ], -}; diff --git a/src/frontend/design-system/layouts/DynamicLayout/ViewMenuItems.tsx b/src/frontend/design-system/layouts/DynamicLayout/ViewMenuItems.tsx deleted file mode 100644 index 0c6ed0287..000000000 --- a/src/frontend/design-system/layouts/DynamicLayout/ViewMenuItems.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import React from "react"; -import { Spacer } from "frontend/design-system/primitives/Spacer"; -import { SoftButton } from "frontend/design-system/components/Button/SoftButton"; -import { RenderList } from "frontend/design-system/components/RenderList"; -import { SectionListItem } from "frontend/design-system/components/Section/SectionList"; -import { IViewMenuItems } from "../types"; - -interface Props { - viewMenuItems: IViewMenuItems; -} - -export function ViewMenuItems({ - viewMenuItems: { menuItems, singular, newItemLink, topAction, getLabel }, -}: Props) { - return ( - <> - {topAction && ( - <> - - - - )} - ({ - name: value, - ...rest, - }))} - getLabel={getLabel} - singular={singular} - newItemLink={newItemLink} - error={menuItems?.error} - render={({ label, action, secondaryAction }) => ( - - )} - /> - - ); -} diff --git a/src/frontend/design-system/layouts/DynamicLayout/index.tsx b/src/frontend/design-system/layouts/DynamicLayout/index.tsx deleted file mode 100644 index 38dea9c4a..000000000 --- a/src/frontend/design-system/layouts/DynamicLayout/index.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import React, { ReactNode } from "react"; -import styled from "styled-components"; -import shallow from "zustand/shallow"; -import { USE_ROOT_COLOR } from "frontend/design-system/theme/root"; -import { useSideBarStore } from "../sidebar.store"; -import { ISelectionView } from "../types"; -import { PrimaryLeftSideNav } from "./PrimaryLeftSideNav"; -import { SecondaryLeftSideNav } from "./SecondarySideNav"; - -export interface IProps { - children: ReactNode; - logo?: string; - selectionView: ISelectionView[]; - secondarySelectionView?: ISelectionView[]; -} - -const Root = styled.div` - width: 100%; - overflow-x: hidden; -`; - -const StyledPage = styled.div<{ isSidebarOpen: boolean }>` - padding: 16px; - min-height: 100vh; - display: block; - margin-left: ${(props) => (props.isSidebarOpen ? 350 : 50)}px; - width: calc(100vw - ${(props) => (props.isSidebarOpen ? 350 : 50)}px); - background: ${USE_ROOT_COLOR("foundation-color")}; -`; - -export function DynamicLayout({ - logo = "/assets/images/logo.png", - children, - selectionView, - secondarySelectionView = [], -}: IProps) { - const [isFullSideBarOpen] = useSideBarStore( - (state) => [state.isFullSideBarOpen], - shallow - ); - return ( - - - - {children} - - ); -} diff --git a/src/frontend/hooks/entity/entity.store.ts b/src/frontend/hooks/entity/entity.store.ts index 797595b17..ad8145666 100644 --- a/src/frontend/hooks/entity/entity.store.ts +++ b/src/frontend/hooks/entity/entity.store.ts @@ -14,7 +14,6 @@ export const ENTITY_RELATIONS_ENDPOINT = (entity: string) => `/api/entities/${entity}/relations`; export const ACTIVE_ENTITIES_ENDPOINT = "/api/entities/active"; -export const USER_MENU_ENTITIES_ENDPOINT = "/api/entities/user-menu"; const useEntitiesListLabel = (entitiesList: DataStateKeys) => { const getEntitiesDictionPlurals = useEntityDictionPlurals( @@ -31,15 +30,6 @@ const useEntitiesListLabel = (entitiesList: DataStateKeys) => { }; }; -export const useUserMenuEntities = () => { - const menuItems = useApi(USER_MENU_ENTITIES_ENDPOINT, { - errorMessage: CRUD_CONFIG_NOT_FOUND("Menu entities"), - defaultData: [], - }); - - return useEntitiesListLabel(menuItems); -}; - export const useActiveEntities = () => { const menuItems = useApi(ACTIVE_ENTITIES_ENDPOINT, { errorMessage: CRUD_CONFIG_NOT_FOUND("Active entities"), diff --git a/src/frontend/lib/routing/links.ts b/src/frontend/lib/routing/links.ts index 98545d31e..03304cdc3 100644 --- a/src/frontend/lib/routing/links.ts +++ b/src/frontend/lib/routing/links.ts @@ -10,6 +10,13 @@ export const NAVIGATION_LINKS = { UPDATE: (dashboardId: string, widgetId: string) => `/dashboard/${dashboardId}/widget/${widgetId}`, }, + CUSTOM: { + VIEW: (id: string) => `/custom-dashboards/${id}`, + EDIT: (id: string) => `/custom-dashboards/${id}/update`, + MANAGE: (id: string) => `/custom-dashboards/${id}/manage`, + LIST: `/custom-dashboards`, + CREATE: `/custom-dashboards/create`, + }, }, AUTH_SIGNIN: "/auth", ACCOUNT: { diff --git a/src/frontend/views/settings/Entities/Menu.tsx b/src/frontend/views/settings/Entities/Menu.tsx index 9052623a8..a6519bcc0 100644 --- a/src/frontend/views/settings/Entities/Menu.tsx +++ b/src/frontend/views/settings/Entities/Menu.tsx @@ -18,10 +18,11 @@ import { import { useEntityDictionPlurals } from "frontend/hooks/entity/entity.queries"; import { ACTIVE_ENTITIES_ENDPOINT, - USER_MENU_ENTITIES_ENDPOINT, useActiveEntities, - useUserMenuEntities, } from "frontend/hooks/entity/entity.store"; +import { loadedDataState } from "frontend/lib/data/constants/loadedDataState"; +import { NAVIGATION_MENU_ENDPOINT } from "frontend/_layouts/app/LayoutImpl/constants"; +import { sortByList } from "shared/logic/entities/sort.utils"; import { SETTINGS_VIEW_KEY } from "../constants"; import { BaseSettingsLayout } from "../_Base"; import { EntitiesSelection } from "./Selection"; @@ -48,14 +49,24 @@ export function MenuEntitiesSettings() { const menuEntitiesToHide = useAppConfiguration( "disabled_menu_entities" ); + + const menuEntitiesOrder = useAppConfiguration( + "menu_entities_order" + ); + const activeEntities = useActiveEntities(); - const userMenuEntities = useUserMenuEntities(); + + const menuEntities: { label: string; value: string }[] = activeEntities.data + .filter(({ value }) => !menuEntitiesToHide.data.includes(value)) + .sort((a, b) => a.value.localeCompare(b.value)); + + sortByList(menuEntities, menuEntitiesOrder.data, "value"); const upsertHideFromMenuMutation = useUpsertConfigurationMutation( "disabled_menu_entities", "", { - otherEndpoints: [ACTIVE_ENTITIES_ENDPOINT, USER_MENU_ENTITIES_ENDPOINT], + otherEndpoints: [ACTIVE_ENTITIES_ENDPOINT, NAVIGATION_MENU_ENDPOINT], } ); @@ -63,7 +74,7 @@ export function MenuEntitiesSettings() { "menu_entities_order", "", { - otherEndpoints: [ACTIVE_ENTITIES_ENDPOINT, USER_MENU_ENTITIES_ENDPOINT], + otherEndpoints: [ACTIVE_ENTITIES_ENDPOINT, NAVIGATION_MENU_ENDPOINT], } ); @@ -73,12 +84,12 @@ export function MenuEntitiesSettings() { ); const error = - menuEntitiesToHide.error || userMenuEntities.error || activeEntities.error; + menuEntitiesToHide.error || activeEntities.error || menuEntitiesOrder.error; const isLoading = - userMenuEntities.isLoading || menuEntitiesToHide.isLoading || - activeEntities.isLoading; + activeEntities.isLoading || + menuEntitiesOrder.isLoading; return ( @@ -126,7 +137,8 @@ export function MenuEntitiesSettings() { loader={} > { const validatedRequest = await getValidatedRequest(["authenticatedUser"]); - return await entitiesApiController.getUserMenuEntities( + return await menuApiController.getMenuItems( (validatedRequest.authenticatedUser as IAccountProfile).role ); }, diff --git a/src/shared/types/menu.ts b/src/shared/types/menu.ts new file mode 100644 index 000000000..50f673b98 --- /dev/null +++ b/src/shared/types/menu.ts @@ -0,0 +1,27 @@ +import { SystemIconsKeys } from "shared/constants/Icons"; + +export enum NavigationMenuItemType { + ExternalLink = "external-link", + Header = "header", + Dashboard = "dashboard", + Entities = "entities", + System = "system", +} + +export enum SystemLinks { + Settings = "settings", + Home = "home", + Roles = "roles", + Users = "users", + Actions = "actions", + AllDashboards = "all-dashboard", +} + +export interface INavigationMenuItem { + id: string; + title: string; + type: NavigationMenuItemType; + icon?: SystemIconsKeys; + link?: string; + children?: INavigationMenuItem[]; +}