import type { FunctionComponent } from 'react'; import React, { useEffect, useRef, useState, useMemo } from 'react'; import type { AsyncPanelExtension } from '@atlassian/clientside-extensions'; import { onDebug } from '@atlassian/clientside-extensions-debug'; import type { Context } from '@atlassian/clientside-extensions-registry'; import type { PanelHandlerProps } from './PanelHandler'; import { PanelHandler } from './PanelHandler'; export type AsyncPanelRenderExtension = () => Promise<{ default: AsyncPanelExtension.AsyncPanelRenderExtension; __esModule: boolean }>; export interface AsyncPanelHandlerProps { fallback?: JSX.Element | string; location: string; pluginKey: string; extensionKey?: string; renderProvider: AsyncPanelRenderExtension; contextProvider?: () => Context<{}>; RootType?: PanelHandlerProps['RootType']; } const noop = () => {}; const loadModule = async ( moduleProvider: () => Promise<{ default: AsyncPanelExtension.AsyncPanelRenderExtension; __esModule: boolean }>, extensionKey: string, location: string, ): Promise => { try { const resolvedModule = await moduleProvider(); // eslint-disable-next-line no-underscore-dangle if (!resolvedModule || !resolvedModule.__esModule || typeof resolvedModule.default !== 'function') { onDebug(({ error }) => ({ level: error, message: `The provided dynamic import of extension "${extensionKey}" at location "${location}" does not look like a module that can be asynchronously loaded.`, components: ['AsyncPanelHandler'], meta: { location, extension: extensionKey, }, })); return noop; } return resolvedModule.default; } catch (e) { onDebug(({ error }) => ({ level: error, message: `Failed trying to execute the dynamic import from extension "${extensionKey}" for the async panel at location "${location}"`, components: ['AsyncPanelHandler'], meta: { location, extension: extensionKey, error: e, }, })); return noop; } }; export const AsyncPanelHandler: FunctionComponent = ({ contextProvider, fallback, location, pluginKey, extensionKey = pluginKey, renderProvider, RootType, }) => { const rendererRef = useRef(); const [isModuleLoaded, setIsModuleLoaded] = useState(false); const context = useMemo>(() => { let contextValue: Context<{}> = {}; if (typeof contextProvider === 'function') { try { contextValue = contextProvider(); } catch (e) { onDebug(({ error }) => ({ level: error, message: `Failed to invoke "contextProvider" function for extension "${extensionKey}" at location "${location}".\nError: ${e}`, components: ['AsyncPanelHandler'], meta: { location, extension: extensionKey, }, })); } } return contextValue; }, [contextProvider, location, extensionKey]); useEffect( function asyncLoadRendererModule() { let didCancel = false; async function loadRendererModule() { setIsModuleLoaded(false); rendererRef.current = undefined; const resolvedRenderer = await loadModule(renderProvider, extensionKey, location); if (didCancel) { return; } rendererRef.current = resolvedRenderer; setIsModuleLoaded(true); } loadRendererModule(); return () => { didCancel = true; }; }, [renderProvider, extensionKey, location], ); if (!isModuleLoaded || !rendererRef.current) { return fallback ? <>{fallback} : null; } return ; };