theforeman/foreman

View on GitHub
webpack/assets/javascripts/react_app/components/Layout/Navigation.js

Summary

Maintainability
D
2 days
Test Coverage
import React, { useState, useEffect, useMemo } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import {
  Nav,
  NavList,
  NavItem,
  NavExpandable,
  NavItemSeparator,
} from '@patternfly/react-core';
import { getCurrentPath } from './LayoutHelper';
import { NavigationSearch } from './NavigationSearch';
import {
  useForemanOrganization,
  useForemanLocation,
} from '../../Root/Context/ForemanContext';

const titleWithIcon = (title, iconClass) => (
  <div className="nav-title-icon">
    <span className={classNames(iconClass, 'nav-icon')} />
    <span className="nav-title">{title}</span>
  </div>
);

const Navigation = ({
  navigate,
  items,
  navigationActiveItem,
  setNavigationActiveItem,
}) => {
  const [currentPath, setCurrentPath] = useState(window.location.pathname);
  useEffect(() => {
    const handleLocationChange = () => {
      setCurrentPath(window.location.pathname);
    };
    window.addEventListener('popstate', handleLocationChange);
    return () => {
      window.removeEventListener('popstate', handleLocationChange);
    };
  }, []);

  const onMouseOver = index => {
    if (navigationActiveItem !== index) {
      setNavigationActiveItem(index);
    }
  };

  const pathFragment = path => path.split('?')[0];
  const subItemToItemMap = {};

  items.forEach(item => {
    item.subItems.forEach(subItem => {
      if (!subItem.isDivider && subItem.href) {
        // don't keep the query parameters for the key
        subItemToItemMap[pathFragment(subItem.href)] = item.title;
      }
    });
  });

  const currentLocation = useForemanLocation()?.title;
  const currentOrganization = useForemanOrganization()?.title;

  const groupedItems = useMemo(
    () =>
      items.map(({ className, subItems, ...rest }) => {
        const groups = [];
        let currIndex = 0;
        if (subItems.length) {
          if (subItems[0].isDivider) {
            groups.push({ title: subItems[0].title, groupItems: [] });
          } else {
            groups.push({ title: '', groupItems: [] });
          }
          subItems.forEach(sub => {
            if (sub.isDivider) {
              groups.push({ title: sub.title, groupItems: [] });
              currIndex++;
            } else {
              const isCurrentLocation =
                className.includes('location-menu') &&
                (sub.title === currentLocation ||
                  (currentLocation === undefined &&
                    sub.id === 'menu_item_any_location'));
              const isCurrentOrganization =
                className.includes('organization-menu') &&
                (sub.title === currentOrganization ||
                  (currentOrganization === undefined &&
                    sub.id === 'menu_item_any_organization'));

              groups[currIndex].groupItems.push({
                ...sub,
                isActive:
                  (currentPath && currentPath === sub.href?.split('?')[0]) ||
                  isCurrentLocation ||
                  isCurrentOrganization,
              });
            }
          });
        }
        return { ...rest, groups, className };
      }),
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [items, currentPath, currentLocation, currentOrganization]
  );

  const [currentExpandedSecondary, setCurrentExpandedSecondary] = useState(
    null
  );
  const [currentExpanded, setCurrentExpanded] = useState(
    subItemToItemMap[pathFragment(getCurrentPath())]
  );
  useEffect(() => {
    setCurrentExpanded(subItemToItemMap[pathFragment(getCurrentPath())]);
    groupedItems.some(({ groups }) =>
      groups.some(({ groupItems, title }) =>
        groupItems.some(({ href }) => {
          if (href === pathFragment(getCurrentPath())) {
            setCurrentExpandedSecondary(title);
            return true;
          }
          return false;
        })
      )
    );
    // we only want to run this when we get new items from the API to set the default expanded item, which is the current location
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [items.length]);
  if (!items.length) return null;

  const clickAndNavigate = (_onClick, href, event) => {
    if (event.ctrlKey) return;
    event.preventDefault();
    if (_onClick && typeof _onClick === 'function') {
      _onClick();
    } else {
      navigate(href);
    }

    setCurrentPath(href);
  };
  return (
    <Nav id="foreman-nav" ouiaId="foreman-nav">
      <NavList>
        <NavigationSearch clickAndNavigate={clickAndNavigate} items={items} />
        {groupedItems.map(({ title, iconClass, groups, className }, index) => (
          <React.Fragment key={index}>
            <NavExpandable
              ouiaId={`nav-expandable-${index}`}
              title={titleWithIcon(title, iconClass)}
              groupId={`nav-expandable-group-${title}`}
              isActive={
                subItemToItemMap[pathFragment(getCurrentPath())] === title
              }
              isExpanded={currentExpanded === title}
              className={className}
              onClick={() => onMouseOver(index)}
              onFocus={() => {
                onMouseOver(index);
              }}
              onExpand={() => {
                // if the current expanded item is the same as the clicked item, collapse it
                const isExpanded = currentExpanded === title;
                // close the Secondary nav if it's open
                if (isExpanded) setCurrentExpandedSecondary(null);
                // only have 1 item expanded at a time
                setCurrentExpanded(isExpanded ? null : title);
                setCurrentExpandedSecondary(null);
              }}
            >
              {groups.map((group, groupIndex) =>
                groupIndex === 0 ? (
                  group.groupItems.map(
                    ({
                      id,
                      title: subItemTitle,
                      className: subItemClassName,
                      href,
                      onClick,
                      isActive,
                    }) => (
                      <React.Fragment key={id}>
                        <NavItem
                          ouiaId={`nav-item-${id}`}
                          className={subItemClassName}
                          id={id}
                          to={href}
                          onClick={event =>
                            clickAndNavigate(onClick, href, event)
                          }
                          isActive={isActive}
                        >
                          {subItemTitle}
                        </NavItem>
                      </React.Fragment>
                    )
                  )
                ) : (
                  <React.Fragment key={groupIndex}>
                    <NavItemSeparator />
                    <NavExpandable
                      ouiaId={`nav-expandable-${index}-${groupIndex}`}
                      title={group.title}
                      isExpanded={currentExpandedSecondary === group.title}
                      onExpand={() => {
                        setCurrentExpandedSecondary(
                          currentExpandedSecondary === group.title
                            ? null
                            : group.title
                        );
                      }}
                    >
                      {group.groupItems.map(
                        ({
                          id,
                          title: subItemTitle,
                          className: subItemClassName,
                          href,
                          onClick,
                          isActive,
                        }) => (
                          <React.Fragment key={id}>
                            <NavItem
                              ouiaId={`nav-item-${id}`}
                              className={subItemClassName}
                              id={id}
                              to={href}
                              onClick={event =>
                                clickAndNavigate(onClick, href, event)
                              }
                              isActive={isActive}
                            >
                              {subItemTitle}
                            </NavItem>
                          </React.Fragment>
                        )
                      )}
                    </NavExpandable>
                  </React.Fragment>
                )
              )}
            </NavExpandable>
          </React.Fragment>
        ))}
      </NavList>
    </Nav>
  );
};

Navigation.propTypes = {
  navigate: PropTypes.func.isRequired,
  items: PropTypes.array.isRequired,
  navigationActiveItem: PropTypes.number,
  setNavigationActiveItem: PropTypes.func.isRequired,
};
Navigation.defaultProps = {
  navigationActiveItem: null,
};

export default Navigation;