import i18next, { WithT } from "i18next"; import { computed, makeObservable } from "mobx"; import { ComponentType, FC, lazy, Component, Suspense, useEffect, useState, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; import TerriaError from "../../Core/TerriaError"; import { applyTranslationIfExists, TRANSLATE_KEY_PREFIX } from "../../Language/languageHelpers"; import Terria from "../../Models/Terria"; import ViewerMode from "../../Models/ViewerMode"; import ViewState from "../../ReactViewModels/ViewState"; import MapNavigationItemController from "../../ViewModels/MapNavigation/MapNavigationItemController"; import { useViewState } from "../Context"; interface ToolProps { toolName: string; getToolComponent: () => | ComponentType | Promise>; params?: any; } /** * Loads the given tool component. * * The prop toolComponent can be an immediate React Component or a promise to * module that exports a default React Component. The promise is useful for * lazy-loading the tool. */ const Tool: FC = (props) => { const { getToolComponent, params, toolName } = props; const viewState = useViewState(); const [t] = useTranslation(); // Track the tool component & props together so that we always // pass the right props to the right tool. const [toolAndProps, setToolAndProps] = useState(undefined); useEffect(() => { setToolAndProps([ lazy(() => Promise.resolve(getToolComponent()).then((c) => ({ default: c })) ), params ]); }, [getToolComponent, params]); let ToolComponent; let toolProps; if (toolAndProps !== undefined) [ToolComponent, toolProps] = toolAndProps; return ( Loading...}> {ToolComponent !== undefined ? ( ) : null} ); }; interface ToolButtonProps extends ToolProps { icon: { id: string }; viewState: ViewState; } export class ToolButtonController extends MapNavigationItemController { constructor(private props: ToolButtonProps) { super(); makeObservable(this); } get glyph() { return this.props.icon; } get viewerMode() { return ViewerMode.Cesium; } get name() { return applyTranslationIfExists(this.props.toolName, i18next); } // TODO: do not use the global i18next instead get i18n from react-i18next // @computed get title() { const buttonState = this.active ? "open" : "closed"; return applyTranslationIfExists( `${TRANSLATE_KEY_PREFIX}tool.button.${buttonState}`, i18next, { toolName: this.name, toolNameLowerCase: this.name.toLowerCase() } ); } @computed get active() { const currentTool = this.props.viewState.currentTool; return ( super.active || (currentTool && currentTool.toolName === this.props.toolName) || false ); } activate() { this.props.viewState.openTool({ toolName: this.props.toolName, getToolComponent: this.props.getToolComponent, params: this.props.params }); super.activate(); } deactivate() { this.props.viewState.closeTool(); super.deactivate(); } } interface ToolErrorBoundaryProps extends WithT { terria: Terria; toolName: string; children: ReactNode; } class ToolErrorBoundary extends Component< ToolErrorBoundaryProps, { hasError: boolean } > { state = { hasError: false }; static getDerivedStateFromError() { return { hasError: true }; } componentDidCatch() { const { terria, toolName, t } = this.props; terria.raiseErrorToUser( new TerriaError({ title: t("tool.loadingError.title", { toolName }), message: t("tool.loadingError.message") }) ); this.setState({ hasError: true }); } render() { return this.state.hasError === true ? null : this.props.children; } } export default Tool;