RocketChat/Rocket.Chat

View on GitHub
packages/fuselage-ui-kit/src/hooks/useUiKitState.ts

Summary

Maintainability
C
7 hrs
Test Coverage
import { useMutableCallback, useSafely } from '@rocket.chat/fuselage-hooks';
import * as UiKit from '@rocket.chat/ui-kit';
import { useContext, useMemo, useState } from 'react';

import { UiKitContext } from '../contexts/UiKitContext';
import { getInitialValue } from '../utils/getInitialValue';

const getElementValueFromState = (
  actionId: string,
  values: Record<
    string,
    | {
        value: unknown;
      }
    | undefined
  >,
  initialValue: string | number | string[] | undefined
) => {
  return (
    (values &&
      (values[actionId]?.value as string | number | string[] | undefined)) ??
    initialValue
  );
};

type UiKitState<
  TElement extends UiKit.ActionableElement = UiKit.ActionableElement
> = {
  loading: boolean;
  setLoading: (loading: boolean) => void;
  error?: string;
  value: UiKit.ActionOf<TElement>;
};

export const useUiKitState = <TElement extends UiKit.ActionableElement>(
  element: TElement,
  context: UiKit.BlockContext
): [
  state: UiKitState<TElement>,
  action: (
    pseudoEvent?:
      | Event
      | { target: EventTarget }
      | { target: { value: UiKit.ActionOf<TElement> } }
  ) => void
] => {
  const { blockId, actionId, appId, dispatchActionConfig } = element;
  const {
    action,
    appId: appIdFromContext = undefined,
    viewId = undefined,
    updateState,
  } = useContext(UiKitContext);

  const initialValue = getInitialValue(element);

  const { values, errors } = useContext(UiKitContext);

  const _value = getElementValueFromState(actionId, values, initialValue);
  const error = Array.isArray(errors)
    ? errors.find((error) =>
        Object.keys(error).find((key) => key === actionId)
      )?.[actionId]
    : errors?.[actionId];

  const [value, setValue] = useSafely(useState(_value));
  const [loading, setLoading] = useSafely(useState(false));

  const actionFunction = useMutableCallback(async (e) => {
    const {
      target: { value: elValue },
    } = e;
    setLoading(true);

    if (Array.isArray(value)) {
      const idx = value.findIndex((value) => value === elValue);

      if (idx > -1) {
        setValue(value.filter((_, i) => i !== idx));
      } else {
        setValue([...value, elValue]);
      }
    } else {
      setValue(elValue);
    }

    await updateState?.(
      { blockId, appId, actionId, value: elValue, viewId },
      e
    );
    await action(
      {
        blockId,
        appId: appId || appIdFromContext || 'core',
        actionId,
        value: elValue,
        viewId,
      },
      e
    );
    setLoading(false);
  });

  // Used for triggering actions on text inputs. Removing the load state
  // makes the text input field remain focused after running the action
  const noLoadStateActionFunction = useMutableCallback(async (e) => {
    const {
      target: { value },
    } = e;
    setValue(value);

    updateState &&
      (await updateState({ blockId, appId, actionId, value, viewId }, e));

    await action(
      {
        blockId,
        appId: appId || appIdFromContext || 'core',
        actionId,
        value,
        viewId,
        dispatchActionConfig,
      },
      e
    );
  });

  const stateFunction = useMutableCallback(async (e) => {
    const {
      target: { value },
    } = e;

    setValue(value);

    await updateState?.(
      {
        blockId,
        appId: appId || appIdFromContext || 'core',
        actionId,
        value,
        viewId,
      },
      e
    );
  });

  const result: UiKitState = useMemo(
    () => ({ loading, setLoading, error, value }),
    [loading, setLoading, error, value]
  );

  if (
    element.type === 'plain_text_input' &&
    Array.isArray(element?.dispatchActionConfig) &&
    element.dispatchActionConfig.includes('on_character_entered')
  ) {
    return [result, noLoadStateActionFunction];
  }

  if (
    (context &&
      [UiKit.BlockContext.SECTION, UiKit.BlockContext.ACTION].includes(
        context
      )) ||
    (Array.isArray(element?.dispatchActionConfig) &&
      element.dispatchActionConfig.includes('on_item_selected'))
  ) {
    return [result, actionFunction];
  }

  return [result, stateFunction];
};