All files / blong-browser/src/widgets ComponentWidget.tsx

0% Statements 0/77
0% Branches 0/1
0% Functions 0/1
0% Lines 0/77

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78                                                                                                                                                           
/**
 * ComponentWidget — dynamically loads and renders a portal component.
 *
 * Used as a form field widget with:
 *   `widget: {type: 'component', component: 'portal.page.key', params: {...}}`
 *
 * Dispatches `component/${page}` with the configured page key to obtain a
 * React component, then renders it. Shows a centred spinner while loading.
 *
 * Both `component` and `params` values support `${field}` template expressions
 * resolved against the current form values at mount time.  Uses the
 * blong-template browser engine (Function-constructor based, same syntax as the
 * server-side trusted engine).
 */
import type {IWidgetProps} from '@feasibleone/blong';
import {renderAll} from '@feasibleone/blong-template';
import React from 'react';
import {useBlongForm} from '../components/Form/FormContext.js';
import {useBlong} from '../context/BlongContext.js';
import {ProgressSpinner} from '../primereact/index.js';

export function ComponentWidget(widgetProps: IWidgetProps) {
    const {schema, loading} = widgetProps;
    const {handler} = useBlong();
    const formCtx = useBlongForm();
    const page = schema.widget?.component ?? '';
    const params = schema.widget?.params;
    const [errorRender, setErrorRender] = React.useState<Error | null>(null);

    const [LoadedComponent, setLoadedComponent] = React.useState<React.ComponentType<
        IWidgetProps
    > | null>(null);

    const vars = formCtx?.getValues();
    React.useEffect(() => {
        if (!page || loading) return;

        let resolvedPage: string;
        let resolvedParams: Record<string, unknown> = {};
        try {
            resolvedPage = renderAll(page, vars ?? {});
            if (!resolvedPage) return;
            resolvedParams = params
                ? (renderAll(params, vars ?? {}) as Record<string, unknown>)
                : {};
        } catch (error) {
            // eslint-disable-next-line @eslint-react/set-state-in-effect
            setErrorRender(error as Error);
            return;
        }
        void (handler[`component/${resolvedPage}`]({
            ...resolvedParams,
        }, {}) as Promise<React.ComponentType | null>)?.then(component => {
            if (component) {
                setErrorRender(null);
                setLoadedComponent(() => component as React.ComponentType<IWidgetProps>);
            }
        });
    }, [page, params, formCtx, handler, loading, vars]);

    if (!LoadedComponent) {
        return (
            <div
                style={{
                    display: 'flex',
                    alignItems: 'center',
                    justifyContent: 'center',
                    height: '100%',
                }}
            >
                <ProgressSpinner title={errorRender ? errorRender.message : 'Loading...'} />
            </div>
        );
    }

    return <LoadedComponent {...widgetProps} />;
}