RocketChat/Rocket.Chat

View on GitHub
apps/meteor/client/uikit/hooks/useUiKitView.ts

Summary

Maintainability
A
1 hr
Test Coverage
import { useSafely } from '@rocket.chat/fuselage-hooks';
import { extractInitialStateFromLayout } from '@rocket.chat/fuselage-ui-kit';
import type * as UiKit from '@rocket.chat/ui-kit';
import type { Dispatch } from 'react';
import { useEffect, useMemo, useReducer, useState } from 'react';

import { useUiKitActionManager } from './useUiKitActionManager';

const reduceValues = (
    values: { [actionId: string]: { value: unknown; blockId?: string } },
    { actionId, payload }: { actionId: string; payload: { value: unknown; blockId?: string } },
): { [actionId: string]: { value: unknown; blockId?: string } } => ({
    ...values,
    [actionId]: payload,
});

const getViewId = (view: UiKit.View): string => {
    if ('id' in view && typeof view.id === 'string') {
        return view.id;
    }

    if ('viewId' in view && typeof view.viewId === 'string') {
        return view.viewId;
    }

    throw new Error('Invalid view');
};

const getViewFromInteraction = (interaction: UiKit.ServerInteraction): UiKit.View | undefined => {
    if ('view' in interaction && typeof interaction.view === 'object') {
        return interaction.view;
    }

    if (interaction.type === 'banner.open') {
        return interaction;
    }

    return undefined;
};

type UseUiKitViewReturnType<TView extends UiKit.View> = {
    view: TView;
    errors?: { [field: string]: string }[];
    values: { [actionId: string]: { value: unknown; blockId?: string } };
    updateValues: Dispatch<{ actionId: string; payload: { value: unknown; blockId?: string } }>;
    state: {
        [blockId: string]: {
            [key: string]: unknown;
        };
    };
};

export function useUiKitView<S extends UiKit.View>(initialView: S): UseUiKitViewReturnType<S> {
    const [errors, setErrors] = useSafely(useState<{ [field: string]: string }[] | undefined>());
    const [values, updateValues] = useSafely(useReducer(reduceValues, initialView.blocks, extractInitialStateFromLayout));
    const [view, updateView] = useSafely(useState(initialView));
    const actionManager = useUiKitActionManager();

    const state = useMemo(() => {
        return Object.entries(values).reduce<{ [blockId: string]: { [actionId: string]: unknown } }>((obj, [key, payload]) => {
            if (!payload?.blockId) {
                return obj;
            }

            const { blockId, value } = payload;
            obj[blockId] = obj[blockId] || {};
            obj[blockId][key] = value;

            return obj;
        }, {});
    }, [values]);

    const viewId = getViewId(view);

    useEffect(() => {
        const handleUpdate = (interaction: UiKit.ServerInteraction): void => {
            if (interaction.type === 'errors') {
                setErrors(interaction.errors);
                return;
            }

            updateView((view) => ({ ...view, ...getViewFromInteraction(interaction) }));
        };

        actionManager.on(viewId, handleUpdate);

        return (): void => {
            actionManager.off(viewId, handleUpdate);
        };
    }, [actionManager, setErrors, updateView, viewId]);

    return { view, errors, values, updateValues, state };
}