use content json also for own menu (#25744)
* use content json also for own menu Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * change isHidden with isVisible Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> --------- Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
This commit is contained in:
parent
41dd1d2161
commit
2163fae7ae
7 changed files with 102 additions and 220 deletions
26
js/apps/account-ui/public/content.json
Normal file
26
js/apps/account-ui/public/content.json
Normal file
|
@ -0,0 +1,26 @@
|
|||
[
|
||||
{ "label": "personalInfo", "path": "/" },
|
||||
{
|
||||
"label": "accountSecurity",
|
||||
"children": [
|
||||
{ "label": "signingIn", "path": "account-security/signing-in" },
|
||||
{ "label": "deviceActivity", "path": "account-security/device-activity" },
|
||||
{
|
||||
"label": "linkedAccounts",
|
||||
"path": "account-security/linked-accounts",
|
||||
"isVisible": "isLinkedAccountsEnabled"
|
||||
}
|
||||
]
|
||||
},
|
||||
{ "label": "applications", "path": "applications" },
|
||||
{
|
||||
"label": "groups",
|
||||
"path": "groups",
|
||||
"isVisible": "isViewGroupsEnabled"
|
||||
},
|
||||
{
|
||||
"label": "resources",
|
||||
"path": "resources",
|
||||
"isVisible": "isMyResourcesEnabled"
|
||||
}
|
||||
]
|
|
@ -2,29 +2,29 @@ import { Spinner } from "@patternfly/react-core";
|
|||
import { Suspense, lazy, useMemo, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { environment } from "../environment";
|
||||
import { ContentComponentParams } from "../routes";
|
||||
import { joinPath } from "../utils/joinPath";
|
||||
import { usePromise } from "../utils/usePromise";
|
||||
import { fetchContentJson } from "./ContentRenderer";
|
||||
import { ContentItem, ModulePageDef, isExpansion } from "./content";
|
||||
import { ContentComponentParams } from "../routes";
|
||||
import fetchContentJson from "./fetchContent";
|
||||
import { MenuItem } from "../root/PageNav";
|
||||
|
||||
function findComponent(
|
||||
content: ContentItem[],
|
||||
content: MenuItem[],
|
||||
componentId: string,
|
||||
): string | undefined {
|
||||
for (const item of content) {
|
||||
if ("path" in item && item.path === componentId) {
|
||||
return (item as ModulePageDef).modulePath;
|
||||
if ("path" in item && item.path === componentId && "modulePath" in item) {
|
||||
return item.modulePath;
|
||||
}
|
||||
if (isExpansion(item)) {
|
||||
return findComponent(item.content, componentId);
|
||||
if ("children" in item) {
|
||||
return findComponent(item.children, componentId);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const ContentComponent = () => {
|
||||
const [content, setContent] = useState<ContentItem[]>();
|
||||
const [content, setContent] = useState<MenuItem[]>();
|
||||
const { componentId } = useParams<ContentComponentParams>();
|
||||
|
||||
usePromise((signal) => fetchContentJson({ signal }), setContent);
|
||||
|
|
|
@ -1,101 +0,0 @@
|
|||
import { NavExpandable, Spinner } from "@patternfly/react-core";
|
||||
import { TFunction } from "i18next";
|
||||
import { Suspense, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { CallOptions } from "../api/methods";
|
||||
import { environment } from "../environment";
|
||||
import { TFuncKey } from "../i18n";
|
||||
import { NavLink } from "../root/PageNav";
|
||||
import { ContentComponentParams } from "../routes";
|
||||
import { joinPath } from "../utils/joinPath";
|
||||
import { usePromise } from "../utils/usePromise";
|
||||
import {
|
||||
ContentItem,
|
||||
Expansion,
|
||||
PageDef,
|
||||
isChildOf,
|
||||
isExpansion,
|
||||
} from "./content";
|
||||
|
||||
export async function fetchContentJson(
|
||||
opts: CallOptions = {},
|
||||
): Promise<ContentItem[]> {
|
||||
const response = await fetch(
|
||||
joinPath(environment.resourceUrl, "/content.json"),
|
||||
opts,
|
||||
);
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
function createNavItem(page: PageDef, activePage: string, t: TFunction) {
|
||||
return (
|
||||
<NavLink to={"content/" + page.path} isActive={activePage === page.path}>
|
||||
{t(page.label as TFuncKey)}
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
function createExpandableNav(
|
||||
item: Expansion,
|
||||
t: TFunction,
|
||||
activePage: string,
|
||||
groupNum: number,
|
||||
) {
|
||||
return (
|
||||
<NavExpandable
|
||||
key={item.id}
|
||||
title={t(item.label as TFuncKey)}
|
||||
isExpanded={isChildOf(item, activePage)}
|
||||
>
|
||||
{createNavItems(t, activePage, item.content, groupNum + 1)}
|
||||
</NavExpandable>
|
||||
);
|
||||
}
|
||||
|
||||
function createNavItems(
|
||||
t: TFunction,
|
||||
activePage: string,
|
||||
contentParam: ContentItem[],
|
||||
groupNum: number,
|
||||
) {
|
||||
return contentParam.map((item: ContentItem) => {
|
||||
if (isExpansion(item)) {
|
||||
return createExpandableNav(item, t, activePage!, groupNum);
|
||||
} else {
|
||||
const page: PageDef = item as PageDef;
|
||||
return createNavItem(page, activePage, t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
type MenuProps = {
|
||||
content: ContentItem[];
|
||||
};
|
||||
|
||||
const Menu = ({ content }: MenuProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { componentId: activePage } = useParams<ContentComponentParams>();
|
||||
|
||||
const groupNum = 0;
|
||||
return content.map((item) => {
|
||||
if (isExpansion(item)) {
|
||||
return createExpandableNav(item, t, activePage!, groupNum);
|
||||
} else {
|
||||
const page: PageDef = item as PageDef;
|
||||
return createNavItem(page, activePage!, t);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const ContentMenu = () => {
|
||||
const [content, setContent] = useState<ContentItem[]>();
|
||||
|
||||
usePromise((signal) => fetchContentJson({ signal }), setContent);
|
||||
|
||||
return (
|
||||
<Suspense fallback={<Spinner />}>
|
||||
{content && <Menu content={content} />}
|
||||
</Suspense>
|
||||
);
|
||||
};
|
|
@ -1,38 +0,0 @@
|
|||
export interface ContentItem {
|
||||
id?: string;
|
||||
label: string;
|
||||
labelParams?: string[];
|
||||
hidden?: string;
|
||||
}
|
||||
|
||||
export interface Expansion extends ContentItem {
|
||||
content: ContentItem[];
|
||||
}
|
||||
|
||||
export interface PageDef extends ContentItem {
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface ComponentPageDef extends PageDef {
|
||||
component: React.ComponentType;
|
||||
}
|
||||
|
||||
export interface ModulePageDef extends PageDef {
|
||||
modulePath: string;
|
||||
componentName: string;
|
||||
}
|
||||
|
||||
export function isExpansion(
|
||||
contentItem: ContentItem,
|
||||
): contentItem is Expansion {
|
||||
return "content" in contentItem;
|
||||
}
|
||||
|
||||
export function isChildOf(parent: Expansion, child: string): boolean {
|
||||
for (const item of parent.content) {
|
||||
if (isExpansion(item) && isChildOf(item, child)) return true;
|
||||
if ("path" in item && item.path === child) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
14
js/apps/account-ui/src/content/fetchContent.ts
Normal file
14
js/apps/account-ui/src/content/fetchContent.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { CallOptions } from "../api/methods";
|
||||
import { environment } from "../environment";
|
||||
import { MenuItem } from "../root/PageNav";
|
||||
import { joinPath } from "../utils/joinPath";
|
||||
|
||||
export default async function fetchContentJson(
|
||||
opts: CallOptions = {},
|
||||
): Promise<MenuItem[]> {
|
||||
const response = await fetch(
|
||||
joinPath(environment.resourceUrl, "/content.json"),
|
||||
opts,
|
||||
);
|
||||
return await response.json();
|
||||
}
|
|
@ -1,3 +1,17 @@
|
|||
export type Feature = {
|
||||
isRegistrationEmailAsUsername: boolean;
|
||||
isEditUserNameAllowed: boolean;
|
||||
isInternationalizationEnabled: boolean;
|
||||
isLinkedAccountsEnabled: boolean;
|
||||
isEventsEnabled: boolean;
|
||||
isMyResourcesEnabled: boolean;
|
||||
isTotpConfigured: boolean;
|
||||
deleteAccountAllowed: boolean;
|
||||
updateEmailFeatureEnabled: boolean;
|
||||
updateEmailActionEnabled: boolean;
|
||||
isViewGroupsEnabled: boolean;
|
||||
};
|
||||
|
||||
export type Environment = {
|
||||
/** The URL to the root of the auth server. */
|
||||
authUrl: string;
|
||||
|
@ -14,19 +28,7 @@ export type Environment = {
|
|||
/** The locale of the user */
|
||||
locale: string;
|
||||
/** Feature flags */
|
||||
features: {
|
||||
isRegistrationEmailAsUsername: boolean;
|
||||
isEditUserNameAllowed: boolean;
|
||||
isInternationalizationEnabled: boolean;
|
||||
isLinkedAccountsEnabled: boolean;
|
||||
isEventsEnabled: boolean;
|
||||
isMyResourcesEnabled: boolean;
|
||||
isTotpConfigured: boolean;
|
||||
deleteAccountAllowed: boolean;
|
||||
updateEmailFeatureEnabled: boolean;
|
||||
updateEmailActionEnabled: boolean;
|
||||
isViewGroupsEnabled: boolean;
|
||||
};
|
||||
features: Feature;
|
||||
};
|
||||
|
||||
// The default environment, used during development.
|
||||
|
|
|
@ -4,11 +4,14 @@ import {
|
|||
NavItem,
|
||||
NavList,
|
||||
PageSidebar,
|
||||
Spinner,
|
||||
} from "@patternfly/react-core";
|
||||
import {
|
||||
PropsWithChildren,
|
||||
MouseEvent as ReactMouseEvent,
|
||||
Suspense,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
|
@ -18,79 +21,55 @@ import {
|
|||
useLinkClickHandler,
|
||||
useLocation,
|
||||
} from "react-router-dom";
|
||||
import { ContentMenu } from "../content/ContentRenderer";
|
||||
import { environment } from "../environment";
|
||||
import fetchContentJson from "../content/fetchContent";
|
||||
import { TFuncKey } from "../i18n";
|
||||
import { usePromise } from "../utils/usePromise";
|
||||
import { Feature, environment } from "../environment";
|
||||
|
||||
type RootMenuItem = {
|
||||
label: TFuncKey;
|
||||
path: string;
|
||||
isHidden?: boolean;
|
||||
isHidden?: keyof Feature;
|
||||
modulePath?: string;
|
||||
};
|
||||
|
||||
type MenuItemWithChildren = {
|
||||
label: TFuncKey;
|
||||
children: MenuItem[];
|
||||
isHidden?: boolean;
|
||||
isHidden?: keyof Feature;
|
||||
};
|
||||
|
||||
type MenuItem = RootMenuItem | MenuItemWithChildren;
|
||||
export type MenuItem = RootMenuItem | MenuItemWithChildren;
|
||||
|
||||
const menuItems: MenuItem[] = [
|
||||
{
|
||||
label: "personalInfo",
|
||||
path: "/",
|
||||
},
|
||||
{
|
||||
label: "accountSecurity",
|
||||
children: [
|
||||
{
|
||||
label: "signingIn",
|
||||
path: "account-security/signing-in",
|
||||
},
|
||||
{
|
||||
label: "deviceActivity",
|
||||
path: "account-security/device-activity",
|
||||
},
|
||||
{
|
||||
label: "linkedAccounts",
|
||||
path: "account-security/linked-accounts",
|
||||
isHidden: !environment.features.isLinkedAccountsEnabled,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "applications",
|
||||
path: "applications",
|
||||
},
|
||||
{
|
||||
label: "groups",
|
||||
path: "groups",
|
||||
isHidden: !environment.features.isViewGroupsEnabled,
|
||||
},
|
||||
{
|
||||
label: "resources",
|
||||
path: "resources",
|
||||
isHidden: !environment.features.isMyResourcesEnabled,
|
||||
},
|
||||
];
|
||||
export const PageNav = () => {
|
||||
const [menuItems, setMenuItems] = useState<MenuItem[]>();
|
||||
|
||||
export const PageNav = () => (
|
||||
usePromise((signal) => fetchContentJson({ signal }), setMenuItems);
|
||||
return (
|
||||
<PageSidebar
|
||||
nav={
|
||||
<Nav>
|
||||
<NavList>
|
||||
<ContentMenu />
|
||||
<Suspense fallback={<Spinner />}>
|
||||
{menuItems
|
||||
.filter((menuItem) => !menuItem.isHidden)
|
||||
?.filter((menuItem) =>
|
||||
menuItem.isHidden
|
||||
? environment.features[menuItem.isHidden]
|
||||
: true,
|
||||
)
|
||||
.map((menuItem) => (
|
||||
<NavMenuItem key={menuItem.label as string} menuItem={menuItem} />
|
||||
<NavMenuItem
|
||||
key={menuItem.label as string}
|
||||
menuItem={menuItem}
|
||||
/>
|
||||
))}
|
||||
</Suspense>
|
||||
</NavList>
|
||||
</Nav>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type NavMenuItemProps = {
|
||||
menuItem: MenuItem;
|
||||
|
|
Loading…
Reference in a new issue