blob: 583c01987af7ab4e1da90780f41453a17917b840 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { FC, useEffect, useState } from 'react';
import { Accordion, Nav } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { useNavigate, useMatch, NavLink } from 'react-router-dom';
import classNames from 'classnames';
import { floppyNavigation } from '@/utils';
import { Icon } from '@/components';
import './index.css';
export interface MenuItem {
name: string;
path?: string;
pathPrefix?: string;
icon?: string;
displayName?: string;
badgeContent?: string | number;
children?: MenuItem[];
}
function MenuNode({
menu,
callback,
activeKey,
expanding = false,
path = '/',
}: {
menu: MenuItem;
callback: (evt: any, menu: MenuItem, href: string, isLeaf: boolean) => void;
activeKey: string;
expanding?: boolean;
path?: string;
}) {
const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' });
const isLeaf = !menu.children || menu.children.length === 0;
const href = isLeaf ? `${path}${menu.path || ''}` : '#';
return (
<Nav.Item key={menu.path} className="w-100">
{isLeaf ? (
<Nav.Link
eventKey={menu.path}
as={NavLink}
to={href}
onClick={(evt) => {
callback(evt, menu, href, isLeaf);
}}
className={classNames(
'text-nowrap d-flex flex-nowrap align-items-center w-100',
{
expanding,
active:
activeKey === menu.path ||
(menu.path && activeKey.startsWith(`${menu.path}/`)) ||
// if pathPrefix is set, activate when activeKey starts with the pathPrefix
(menu.pathPrefix && activeKey.startsWith(menu.pathPrefix)),
},
)}>
{menu?.icon && <Icon name={menu.icon} className="me-2" />}
<span className="me-auto text-truncate">
{menu.displayName ? menu.displayName : t(menu.name)}
</span>
{menu.badgeContent ? (
<span className="badge text-bg-dark">{menu.badgeContent}</span>
) : null}
{!isLeaf && (
<Icon className="collapse-indicator" name="chevron-right" />
)}
</Nav.Link>
) : (
<Nav.Link
eventKey={menu.path}
as="button"
href={href}
onClick={(evt) => {
callback(evt, menu, href, isLeaf);
}}
className={classNames(
'text-nowrap d-flex flex-nowrap align-items-center w-100',
{
expanding,
active:
activeKey === menu.path ||
(menu.path && activeKey.startsWith(`${menu.path}/`)) ||
(menu.pathPrefix && activeKey.startsWith(menu.pathPrefix)),
},
)}>
{menu?.icon && <Icon name={menu.icon} className="me-2" />}
<span className="me-auto text-truncate">
{menu.displayName ? menu.displayName : t(menu.name)}
</span>
{menu.badgeContent ? (
<span className="badge text-bg-dark">{menu.badgeContent}</span>
) : null}
{!isLeaf && (
<Icon className="collapse-indicator" name="chevron-right" />
)}
</Nav.Link>
)}
{menu.children && menu.children.length > 0 ? (
<Accordion.Collapse eventKey={menu.path || menu.name} className="ms-4">
<>
{menu.children.map((leaf) => {
return (
<MenuNode
menu={leaf}
callback={callback}
activeKey={activeKey}
path={path}
key={leaf.path || leaf.name}
/>
);
})}
</>
</Accordion.Collapse>
) : null}
</Nav.Item>
);
}
interface AccordionProps {
menus: MenuItem[];
path?: string;
}
const AccordionNav: FC<AccordionProps> = ({ menus = [], path = '/' }) => {
const navigate = useNavigate();
const pathMatch = useMatch(`${path}*`);
// auto set menu fields
menus.forEach((m) => {
if (!m.path) {
m.path = m.name;
}
if (!Array.isArray(m.children)) {
m.children = [];
}
m.children.forEach((sm) => {
if (!sm.path) {
sm.path = sm.name;
}
if (!Array.isArray(sm.children)) {
sm.children = [];
}
});
});
const splat = pathMatch && pathMatch.params['*'];
let activeKey: string = menus[0]?.path || menus[0]?.name || '';
if (splat) {
activeKey = splat;
}
const getOpenKey = () => {
let openKey = '';
menus.forEach((li) => {
if (li.children && li.children.length > 0) {
const matchedChild = li.children.find((el) => {
// exact match or path prefix match
return (
el.path === activeKey ||
(el.path && activeKey.startsWith(`${el.path}/`)) ||
// if pathPrefix is set, activate when activeKey starts with the pathPrefix
(el.pathPrefix && activeKey.startsWith(el.pathPrefix))
);
});
if (matchedChild) {
openKey = li.path || li.name || '';
}
}
});
return openKey;
};
const [openKey, setOpenKey] = useState(getOpenKey());
const menuClick = (evt, menu, href, isLeaf) => {
evt.stopPropagation();
if (isLeaf) {
if (floppyNavigation.shouldProcessLinkClick(evt)) {
evt.preventDefault();
navigate(href);
}
} else {
setOpenKey(openKey === menu.path ? '' : menu.path);
}
};
useEffect(() => {
setOpenKey(getOpenKey());
}, [activeKey, menus]);
return (
<Accordion activeKey={openKey} flush id="answerAccordion">
<Nav variant="pills" className="flex-column" activeKey={activeKey}>
{menus.map((li) => {
return (
<MenuNode
menu={li}
path={path}
callback={menuClick}
activeKey={activeKey}
expanding={openKey === (li.path || li.name)}
key={li.path || li.name}
/>
);
})}
</Nav>
</Accordion>
);
};
export default AccordionNav;