Coursemology/coursemology2

View on GitHub
client/app/bundles/course/assessment/question/programming/ProgrammingForm.tsx

Summary

Maintainability
B
6 hrs
Test Coverage
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
  ProgrammingFormData,
  ProgrammingPostStatusData,
} from 'types/course/assessment/question/programming';

import Section from 'lib/components/core/layouts/Section';
import Form, { FormEmitter } from 'lib/components/form/Form';
import { loadingToast } from 'lib/hooks/toast';
import useTranslation from 'lib/hooks/useTranslation';

import translations from '../../translations';

import schema, { isPackageFieldsDirty } from './commons/validation';
import BuildLog from './components/sections/BuildLog';
import EvaluatorFields from './components/sections/EvaluatorFields';
import FeedbackFields from './components/sections/FeedbackFields';
import LanguageFields from './components/sections/LanguageFields';
import PackageFields, {
  PACKAGE_SECTION_ID,
} from './components/sections/PackageFields';
import QuestionFields from './components/sections/QuestionFields';
import SubmitWarningDialog from './components/sections/SubmitWarningDialog';
import { ProgrammingFormDataProvider } from './hooks/ProgrammingFormDataContext';
import useLanguageMode from './hooks/useLanguageMode';
import { watchEvaluation } from './operations';

interface ProgrammingFormProps {
  with: ProgrammingFormData;
  dirty?: boolean;
  onSubmit?: (data: ProgrammingFormData) => Promise<ProgrammingPostStatusData>;
  revalidate?: (
    response: ProgrammingPostStatusData,
    data: ProgrammingFormData,
  ) => Promise<ProgrammingFormData>;
}

const ProgrammingForm = (props: ProgrammingFormProps): JSX.Element => {
  const [data, setData] = useState(props.with);

  const { t } = useTranslation();

  const [submitting, setSubmitting] = useState(false);
  const [form, setForm] = useState<FormEmitter<ProgrammingFormData>>();
  const [pending, setPending] = useState<() => void>();

  const { languageOptions, getModeFromId } = useLanguageMode(data.languages);

  const navigate = useNavigate();

  const submitForm = async (rawData: ProgrammingFormData): Promise<void> => {
    if (!props.onSubmit) return undefined;

    setSubmitting(true);

    const toast = loadingToast(t(translations.savingChanges));

    try {
      const response = await props.onSubmit(rawData);

      const toastSuccessAndRedirect = (): void => {
        toast.success(t(translations.questionSavedRedirecting));
        navigate(response.redirectAssessmentUrl);
      };

      if (!response.importJobUrl) return toastSuccessAndRedirect();

      toast.update(t(translations.evaluatingSubmissions));

      let debounced = false;

      return watchEvaluation(
        response.importJobUrl,
        () => {
          if (debounced) return;
          debounced = true;

          toastSuccessAndRedirect();
        },
        async () => {
          if (debounced) return;
          debounced = true;

          const newData = await props.revalidate?.(response, rawData);
          if (newData) {
            setData(newData);
            form?.resetTo?.(newData, true);
          }

          toast.error(t(translations.questionSavedButPackageError));

          setSubmitting(false);
          window.location.href = `#${PACKAGE_SECTION_ID}`;
        },
      );
    } catch (error) {
      if (!(error instanceof Error)) throw error;

      toast.error(error.message || t(translations.errorWhenSavingQuestion));
      return setSubmitting(false);
    }
  };

  const preProcessForm = (draft: ProgrammingFormData): ProgrammingFormData => {
    if (draft.testUi?.mode) {
      draft.testUi.mode = getModeFromId(draft.question.languageId);
    }

    return draft;
  };

  return (
    <ProgrammingFormDataProvider from={data}>
      <Form
        contextual
        dirty={props.dirty}
        disabled={submitting}
        emitsVia={setForm}
        headsUp
        initialValues={data}
        onSubmit={(rawData): void => {
          if (
            data.question.hasSubmissions &&
            isPackageFieldsDirty(data, rawData)
          ) {
            setPending(() => () => submitForm(rawData));
          } else {
            submitForm(rawData);
          }
        }}
        transformsBy={preProcessForm}
        validates={schema(t)}
      >
        <QuestionFields disabled={submitting} />

        <Section sticksToNavbar title={t(translations.languageAndEvaluation)}>
          <LanguageFields
            disabled={submitting}
            getModeFromId={getModeFromId}
            languageOptions={languageOptions}
          />

          <EvaluatorFields disabled={submitting} />
        </Section>

        <PackageFields disabled={submitting} getModeFromId={getModeFromId} />

        <FeedbackFields disabled={submitting} />

        <BuildLog />
      </Form>

      <SubmitWarningDialog
        onClose={(): void => setPending(undefined)}
        onConfirm={(): void => pending?.()}
        open={Boolean(pending)}
      />
    </ProgrammingFormDataProvider>
  );
};

export default ProgrammingForm;