Coursemology/coursemology2

View on GitHub
client/app/bundles/course/assessment/question/text-responses/components/TextResponseForm.tsx

Summary

Maintainability
B
5 hrs
Test Coverage
import { useRef, useState } from 'react';
import { Alert } from '@mui/material';
import {
  AttachmentType,
  TextResponseData,
  TextResponseFormData,
} from 'types/course/assessment/question/text-responses';

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

import translations from '../../../translations';
import CommonQuestionFields from '../../components/CommonQuestionFields';
import { questionSchema, validateSolutions } from '../commons/validations';
import {
  getAttachmentTypeFromMaxAttachment,
  getMaxAttachmentFromAttachmentType,
  getMaxAttachmentSize,
} from '../utils';

import FileUploadManager from './FileUploadManager';
import SolutionsManager, { SolutionsManagerRef } from './SolutionsManager';

export interface TextResponseFormProps<T extends 'new' | 'edit'> {
  with: TextResponseFormData<T>;
  onSubmit: (data: TextResponseData) => Promise<void>;
}

const TextResponseForm = <T extends 'new' | 'edit'>(
  props: TextResponseFormProps<T>,
): JSX.Element => {
  const { with: data } = props;

  const formattedData = {
    ...data,
    question: {
      ...data.question,
      attachmentType:
        data.question?.attachmentType ??
        getAttachmentTypeFromMaxAttachment(data.question?.maxAttachments),
    },
  };

  const [submitting, setSubmitting] = useState(false);
  const [form, setForm] = useState<FormEmitter>();
  const [isSolutionsDirty, setIsSolutionsDirty] = useState(false);

  const solutionsRef = useRef<SolutionsManagerRef>(null);

  const prepareSolutions = async (
    questionType: 'file_upload' | 'text_response',
  ): Promise<TextResponseData<T>['solutions'] | undefined> => {
    solutionsRef.current?.resetErrors();
    if (questionType === 'file_upload') return [];

    const solutions = solutionsRef.current?.getSolutions() ?? [];
    const errors = await validateSolutions(solutions);

    if (errors) {
      solutionsRef.current?.setErrors(errors);
      return undefined;
    }

    return solutions;
  };

  const { t } = useTranslation();

  const handleSubmit = async (
    question: TextResponseData['question'],
  ): Promise<void> => {
    const solutions = await prepareSolutions(data.questionType);
    if (!solutions) return;

    const newData: TextResponseData = {
      questionType: data.questionType,
      isAssessmentAutograded: data.isAssessmentAutograded,
      question: {
        ...question,
        isAttachmentRequired:
          question.attachmentType === AttachmentType.NO_ATTACHMENT
            ? false
            : question.isAttachmentRequired,
        maxAttachments: getMaxAttachmentFromAttachmentType(question),
        maxAttachmentSize: getMaxAttachmentSize(question),
      },
      solutions,
    };
    setSubmitting(true);
    props.onSubmit(newData).catch((errors) => {
      setSubmitting(false);
      form?.receiveErrors?.(errors);
    });
  };

  return (
    <Form
      contextual
      dirty={isSolutionsDirty}
      disabled={submitting}
      emitsVia={setForm}
      headsUp
      initialValues={formattedData.question!}
      onSubmit={handleSubmit}
      validates={questionSchema(
        t,
        data.defaultMaxAttachmentSize!,
        data.defaultMaxAttachments!,
      )}
    >
      {(control): JSX.Element => (
        <>
          <CommonQuestionFields
            availableSkills={data.availableSkills}
            control={control}
            disabled={submitting}
            skillsUrl={data.skillsUrl}
          />
          {data.isAssessmentAutograded &&
            data.questionType === 'file_upload' && (
              <Alert severity="info">{t(translations.fileUploadNote)}</Alert>
            )}

          <Section
            sticksToNavbar
            subtitle={t(translations.fileUploadDescription)}
            title={t(translations.fileUpload)}
          >
            <Subsection
              subtitle={t(translations.attachmentSettingsDescription)}
              title={t(translations.attachmentSettings)}
            >
              <FileUploadManager
                disabled={submitting}
                isTextResponseQuestion={data.questionType === 'text_response'}
              />
            </Subsection>
          </Section>

          {data.questionType === 'text_response' && (
            <Section
              sticksToNavbar
              subtitle={t(translations.solutionsHint)}
              title={t(translations.solutions)}
            >
              <SolutionsManager
                ref={solutionsRef}
                disabled={submitting}
                for={data.solutions ?? []}
                isAssessmentAutograded={data.isAssessmentAutograded}
                onDirtyChange={setIsSolutionsDirty}
              />
            </Section>
          )}
        </>
      )}
    </Form>
  );
};

export default TextResponseForm;