Source: data-engine-taglib/src/main/resources/META-INF/resources/data_layout_builder/js/components/sidebar/MultiPanelSidebar.es.js

/**
 * Copyright (c) 2000-present Liferay, Inc. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2.1 of the License, or (at your option)
 * any later version.
 *
 * This library is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 */

import ClayButton, {ClayButtonWithIcon} from '@clayui/button';
import ClayIcon from '@clayui/icon';
import ClayLoadingIndicator from '@clayui/loading-indicator';
import {ClayTooltipProvider} from '@clayui/tooltip';
import classNames from 'classnames';
import {useIsMounted} from 'frontend-js-react-web';
import React from 'react';

import AppContext from '../../AppContext.es';
import useLazy from '../../hooks/useLazy.es';
import useLoad from '../../hooks/useLoad.es';
import usePlugins from '../../hooks/usePlugins.es';
import useStateSafe from '../../hooks/useStateSafe.es';

const {Suspense, useCallback, useContext, useEffect} = React;

/**
 * Failure to preload is a non-critical failure, so we'll use this to swallow
 * rejected promises silently.
 */
const swallow = [(value) => value, (_error) => undefined];

export default function MultiPanelSidebar({
	panels,
	sidebarPanels,
	variant = 'dark',
}) {
	const [{sidebarOpen, sidebarPanelId}, dispatch] = useContext(AppContext);
	const [hasError, setHasError] = useStateSafe(false);
	const isMounted = useIsMounted();
	const load = useLoad();
	const {getInstance, register} = usePlugins();

	const panel = sidebarPanels[sidebarPanelId];
	const promise = panel
		? load(sidebarPanelId, panel.pluginEntryPoint)
		: Promise.resolve();

	const app = {
		dispatch,
		panel,
		sidebarOpen,
		sidebarPanelId,
	};

	let registerPanel;

	if (sidebarPanelId) {
		registerPanel = register(sidebarPanelId, promise, {app, panel});
	}

	useEffect(
		() => {
			if (panel) {
				togglePlugin(panel);
			}
			else if (sidebarPanelId) {
				dispatch({
					payload: {
						sidebarOpen: false,
						sidebarPanelId: null,
					},
					type: 'SWITCH_SIDEBAR_PANEL',
				});
			}
		},
		/* eslint-disable react-hooks/exhaustive-deps */
		[panel, sidebarOpen, sidebarPanelId]
	);

	useEffect(() => {
		const sideNavigation = Liferay.SideNavigation.instance(
			document.querySelector('.product-menu-toggle')
		);

		if (sideNavigation) {
			const onCloseSidebar = () => {
				dispatch({
					payload: {
						sidebarOpen: false,
						sidebarPanelId: null,
					},
					type: 'SWITCH_SIDEBAR_PANEL',
				});
			};

			const sideNavigationListener = sideNavigation.on(
				'openStart.lexicon.sidenav',
				onCloseSidebar
			);

			return () => {
				sideNavigationListener.removeListener();
			};
		}
	}, []);

	const SidebarPanel = useLazy(
		useCallback(({instance}) => {
			if (typeof instance.renderSidebar === 'function') {
				return instance.renderSidebar();
			}
			else if (typeof instance === 'function') {
				return instance;
			}
			else {
				return null;
			}
		}, [])
	);

	const handleClick = (panel) => {
		const open =
			panel.sidebarPanelId === sidebarPanelId ? !sidebarOpen : true;
		const productMenuToggle = document.querySelector(
			'.product-menu-toggle'
		);

		if (productMenuToggle && !sidebarOpen) {
			Liferay.SideNavigation.hide(productMenuToggle);
		}

		dispatch({
			payload: {
				sidebarOpen: open,
				sidebarPanelId: panel.sidebarPanelId,
			},
			type: 'SWITCH_SIDEBAR_PANEL',
		});
	};

	const togglePlugin = () => {
		if (hasError) {
			setHasError(false);
		}

		if (registerPanel) {
			registerPanel.then((plugin) => {
				if (
					plugin &&
					typeof plugin.activate === 'function' &&
					isMounted()
				) {
					plugin.activate();
				}
				else if (!plugin) {
					setHasError(true);
				}
			});
		}
	};

	return (
		<ClayTooltipProvider>
			<div
				className={classNames(
					'multi-panel-sidebar',
					`multi-panel-sidebar-${variant}`
				)}
			>
				<nav
					className={classNames(
						'multi-panel-sidebar-buttons',
						'tbar',
						'tbar-stacked',
						variant === 'dark'
							? `tbar-${variant}-d1`
							: `tbar-${variant}`
					)}
				>
					<ul className="tbar-nav">
						{panels.reduce((elements, group, groupIndex) => {
							const buttons = group.map((panelId) => {
								const panel = sidebarPanels[panelId];

								const active =
									sidebarOpen && sidebarPanelId === panelId;
								const {
									icon,
									isLink,
									label,
									pluginEntryPoint,
									url,
								} = panel;

								const prefetch = () =>
									load(
										panel.sidebarPanelId,
										pluginEntryPoint
									).then(...swallow);

								const btnClasses = classNames(
									'tbar-btn tbar-btn-monospaced',
									{active}
								);

								return (
									<li
										className={classNames(
											'tbar-item',
											`tbar-item--${panel.sidebarPanelId}`
										)}
										key={panel.sidebarPanelId}
									>
										{isLink ? (
											<a
												className={btnClasses}
												href={url}
											>
												<ClayIcon symbol={icon} />
											</a>
										) : (
											<ClayButtonWithIcon
												aria-pressed={active}
												className={btnClasses}
												data-tooltip-align="left"
												displayType="unstyled"
												id={panel.sidebarPanelId}
												onClick={() =>
													handleClick(panel)
												}
												onFocus={prefetch}
												onMouseEnter={prefetch}
												symbol={icon}
												title={label}
											/>
										)}
									</li>
								);
							});

							if (groupIndex === panels.length - 1) {
								return elements.concat(buttons);
							}
							else {
								return elements.concat([
									...buttons,
									<hr key={`separator-${groupIndex}`} />,
								]);
							}
						}, [])}
					</ul>
				</nav>
				<div
					className={classNames('multi-panel-sidebar-content', {
						'multi-panel-sidebar-content-open': sidebarOpen,
					})}
				>
					{hasError ? (
						<div>
							<ClayButton
								block
								displayType="secondary"
								onClick={() => {
									dispatch({
										payload: {
											sidebarOpen: false,
											sidebarPanelId:
												panels[0] && panels[0][0],
										},
										type: 'SWITCH_SIDEBAR_PANEL',
									});
									setHasError(false);
								}}
								small
							>
								{Liferay.Language.get('refresh')}
							</ClayButton>
						</div>
					) : (
						<ErrorBoundary
							handleError={() => {
								setHasError(true);
							}}
						>
							<Suspense fallback={<ClayLoadingIndicator />}>
								<SidebarPanel
									getInstance={getInstance}
									pluginId={sidebarPanelId}
								/>
							</Suspense>
						</ErrorBoundary>
					)}
				</div>
			</div>
		</ClayTooltipProvider>
	);
}

class ErrorBoundary extends React.Component {
	static getDerivedStateFromError(_error) {
		return {hasError: true};
	}

	constructor(props) {
		super(props);

		this.state = {hasError: false};
	}

	componentDidCatch(error) {
		if (this.props.handleError) {
			this.props.handleError(error);
		}
	}

	render() {
		if (this.state.hasError) {
			return null;
		}
		else {
			return this.props.children;
		}
	}
}