Commit f61423f5 authored by Marek Veselý's avatar Marek Veselý
Browse files

feat: add changes to questionnaire fetching

parent 6ce2b715
Loading
Loading
Loading
Loading
Compare 85e90b22 to 853c3bef
Original line number Diff line number Diff line
Subproject commit 85e90b22ee898d1af75775bc22ab4399c979b808
Subproject commit 853c3bef385f62f219ff43dc397a710785def111
+1 −1
Original line number Diff line number Diff line
{
  "name": "@inject/codegen",
  "version": "2.24.0",
  "version": "2.25.0",
  "description": "GraphQL API Codegen Setup for the Inject Backend",
  "main": "index.js",
  "license": "MIT",
+1 −1
Original line number Diff line number Diff line
{
  "name": "@inject/frontend",
  "version": "2.24.0",
  "version": "2.25.0",
  "description": "Main wrapper for rendering INJECT Frontend",
  "main": "index.js",
  "license": "MIT",
+42 −68
Original line number Diff line number Diff line
@@ -4,13 +4,10 @@ import type { QuestionAndAnswer } from '@/components/Questionnaire/types'
import { INVALID_VALUE } from '@/components/Questionnaire/utilities'
import type { ButtonProps } from '@blueprintjs/core'
import { Button, Callout, NonIdealState } from '@blueprintjs/core'
import type {
  Question,
  QuestionnaireDetails,
} from '@inject/graphql/fragment-types'
import type { TeamQuestionnaireStateWithQuestionnaire } from '@inject/graphql/fragment-types'
import { useTypedMutation, useTypedQuery } from '@inject/graphql/graphql'
import { AnswerQuestionnaire } from '@inject/graphql/mutations'
import { GetTeam, GetTeamQuestionnaireState } from '@inject/graphql/queries'
import { GetTeam } from '@inject/graphql/queries'
import { useLoopStatus } from '@inject/graphql/utils/useExerciseLoopStatusSubscription'
import Keys from '@inject/shared/localstorage/keys'
import { useLocalStorageState } from 'ahooks'
@@ -18,18 +15,12 @@ import type { FC } from 'react'
import { useCallback, useEffect, useMemo } from 'react'

interface TraineeQuestionnaireContentProps {
  details: QuestionnaireDetails
  teamState: TeamQuestionnaireStateWithQuestionnaire
  teamId: string
  exerciseId: string
  onClose?: () => void
}

const getDefaultAnswers = (questions: Question[]): QuestionAndAnswer[] =>
  questions.map(question => ({
    question,
    answer: INVALID_VALUE,
  }))

const MultipleUsersWarning = () => (
  <Callout intent='warning'>
    <b>Answers can be submitted only once per team!</b>
@@ -40,28 +31,46 @@ const MultipleUsersWarning = () => (

// TODO: when questionnaire is answered, remove data from local storage and use questionnaireState instead
const TraineeQuestionnaireContent: FC<TraineeQuestionnaireContentProps> = ({
  details,
  teamState,
  teamId,
  exerciseId,
  onClose,
}) => {
  const { running } = useLoopStatus()
  const defaultAnswers: QuestionAndAnswer[] = useMemo(
    () => getDefaultAnswers(details.questions),
    [details.questions]
  )
  const { answers, status, questionnaire } = teamState
  const { id, questions, content } = questionnaire

  const defaultValue: QuestionAndAnswer[] = useMemo(
    () =>
      questions.map(question => ({
        question,
        answer:
          answers.find(answer => answer.question.id === question.id)?.answer ||
          INVALID_VALUE,
      })),
    [answers, questions]
  )
  const [questionsAndAnswers, setQuestionsAndAnswers] = useLocalStorageState<
    QuestionAndAnswer[]
  >(Keys.getQuestionnaireAnswersKey(details.id, teamId), {
    defaultValue: defaultAnswers,
  >(Keys.getQuestionnaireAnswersKey(id, teamId), {
    defaultValue,
    listenStorageChange: true,
  })

  // reset state when questions change
  useEffect(() => {
    setQuestionsAndAnswers(getDefaultAnswers(details.questions))
  }, [details.questions, setQuestionsAndAnswers, teamId, exerciseId])
    setQuestionsAndAnswers(prev => {
      if (!prev) {
        return defaultValue
      }

      return prev.map(questionAndAnswer => ({
        question: questionAndAnswer.question,
        answer:
          answers.find(
            answer => answer.question.id === questionAndAnswer.question.id
          )?.answer || questionAndAnswer.answer,
      }))
    })
  }, [answers, defaultValue, questions, setQuestionsAndAnswers])

  const [{ fetching: loading }, mutate] = useTypedMutation(AnswerQuestionnaire)

@@ -83,41 +92,6 @@ const TraineeQuestionnaireContent: FC<TraineeQuestionnaireContentProps> = ({
      []
    ),
  })
  const [{ data }] = useTypedQuery({
    query: GetTeamQuestionnaireState,
    variables: {
      teamId,
      questionnaireId: details.id,
    },
    context: useMemo(
      () => ({
        suspense: true,
      }),
      []
    ),
  })
  // reset state when answers change
  useEffect(() => {
    if (!data || !data.questionnaireState) {
      return
    }
    const teamAnswers = data.questionnaireState.answers
    setQuestionsAndAnswers(prev =>
      (prev || defaultAnswers).map(questionAndAnswer => ({
        ...questionAndAnswer,
        answer:
          teamAnswers.find(
            teamAnswer =>
              teamAnswer.question.id === questionAndAnswer.question.id
          )?.answer || questionAndAnswer.answer,
      }))
    )
  }, [data, defaultAnswers, details.questions, setQuestionsAndAnswers])

  const status = useMemo(
    () => data?.questionnaireState.status,
    [data?.questionnaireState.status]
  )

  const { title, disabled } = useMemo<ButtonProps>(() => {
    switch (status) {
@@ -139,7 +113,7 @@ const TraineeQuestionnaireContent: FC<TraineeQuestionnaireContentProps> = ({
          }
        }
        if (
          (questionsAndAnswers || defaultAnswers).some(
          (questionsAndAnswers || defaultValue).some(
            questionAndAnswer => questionAndAnswer.answer === INVALID_VALUE
          )
        ) {
@@ -155,29 +129,29 @@ const TraineeQuestionnaireContent: FC<TraineeQuestionnaireContentProps> = ({
      default:
        throw new Error(`Unknown status: ${status}`)
    }
  }, [defaultAnswers, questionsAndAnswers, status])
  }, [defaultValue, questionsAndAnswers, status])

  const handleClick = useCallback(() => {
    mutate({
      questInput: {
        answers: (questionsAndAnswers || defaultAnswers).map(
        answers: (questionsAndAnswers || defaultValue).map(
          questionAndAnswer => ({
            questionId: questionAndAnswer.question.id,
            value: questionAndAnswer.answer,
          })
        ),
        questionnaireId: details.id,
        questionnaireId: id,
        teamId,
      },
    }).then(() => {
      onClose?.()
    })
  }, [defaultAnswers, details.id, mutate, onClose, questionsAndAnswers, teamId])
  }, [defaultValue, id, mutate, onClose, questionsAndAnswers, teamId])

  const handleChange = useCallback(
    (value: string, questionId: string) => {
      setQuestionsAndAnswers(prev => {
        const newValue = prev || defaultAnswers
        const newValue = prev || defaultValue
        const index = newValue.findIndex(
          questionAndAnswer => questionAndAnswer.question.id === questionId
        )
@@ -191,7 +165,7 @@ const TraineeQuestionnaireContent: FC<TraineeQuestionnaireContentProps> = ({
        ]
      })
    },
    [defaultAnswers, setQuestionsAndAnswers]
    [defaultValue, setQuestionsAndAnswers]
  )

  const done = useMemo<boolean>(() => {
@@ -207,7 +181,7 @@ const TraineeQuestionnaireContent: FC<TraineeQuestionnaireContentProps> = ({
    }
  }, [status])

  if (!data?.questionnaireState || !teamData?.team) {
  if (!teamData?.team) {
    return (
      <NonIdealState
        icon='low-voltage-pole'
@@ -225,8 +199,8 @@ const TraineeQuestionnaireContent: FC<TraineeQuestionnaireContentProps> = ({
      exerciseId={exerciseId}
      type='answering'
      callout={status === 'SENT' && numUsers > 1 && <MultipleUsersWarning />}
      questionsAndAnswers={questionsAndAnswers || defaultAnswers}
      content={details.content}
      questionsAndAnswers={questionsAndAnswers || defaultValue}
      content={content}
      disabled={() => status !== 'SENT' || !running}
      onChange={questionId => value => handleChange(value, questionId)}
      actions={
+13 −6
Original line number Diff line number Diff line
import InstructorQuestionnaireContent from '@/instructor/InstructorQuestionnaire/InstructorQuestionnaireContent'
import InstructorQuestionnaire from '@/instructor/InstructorQuestionnaire'
import type { ActionLog } from '@inject/graphql/fragment-types'
import { type FC } from 'react'
import EmailContent from './EmailContent'
@@ -52,21 +52,28 @@ const Content: FC<ContentProps> = ({
          inInstructor={inInstructor}
        />
      )
    case 'QuestionnaireType':
    case 'TeamQuestionnaireStateType': {
      const teamState = actionLog.details
      return inInstructor ? (
        <InstructorQuestionnaireContent
          details={actionLog.details}
          teamId={teamId}
        <InstructorQuestionnaire
          exerciseId={exerciseId}
          questionnaireId={teamState.questionnaire.id}
          teamId={teamId}
          status={teamState.status}
          content={teamState.questionnaire.content}
          questions={teamState.questionnaire.questions}
          teamAnswers={teamState.answers}
          relatedMilestones={teamState.relatedMilestones}
        />
      ) : (
        <TraineeQuestionnaireContent
          details={actionLog.details}
          teamState={actionLog.details}
          teamId={teamId}
          exerciseId={exerciseId}
          onClose={onClose}
        />
      )
    }
    case 'EmailType':
      return (
        <EmailContent
Loading