move move frontend to progress-test

This commit is contained in:
João Geonizeli
2022-07-21 21:16:59 -03:00
parent f8d5d08447
commit 386050d4ad
129 changed files with 159374 additions and 39 deletions

View File

@@ -0,0 +1,222 @@
import React, {FC, useState} from 'react'
import {useForm} from 'react-hook-form';
import {ExclamationCircleIcon} from '@heroicons/react/outline';
import {useHistory} from "react-router-dom";
import {useDispatch, useSelector} from "react-redux";
import {gql} from '@apollo/client';
import {Question, QuestionCreateInput, QuestionStatus} from '../../../__generated__/graphql-schema';
import {formatInput} from '../formatInputs';
import {validateQuestionInputs} from '../../../utils/questions/questionValidations';
import {RootState} from '../../../services/store';
import {FormProvider} from './FormContext'
import {SteppedForm, Step} from './SteppedForm'
import {
EnunciationFormStep,
EnunciationFragment,
AnswerFormStep,
AnswerFragment,
DistractorsFormStep,
DistractorsFragment,
FeaturesFormStep,
FeaturesFragment,
} from './steps'
import {
Button,
Dialog,
AlertV2Props,
AlertV2,
List,
ListItem,
} from '../../../components';
import {QuestionRoutePaths} from "../../../routes";
import {turnOff, turnOn} from "../../../services/store/unsavedChanges";
export const FormFragments = gql`
${EnunciationFragment}
${AnswerFragment}
${DistractorsFragment}
${FeaturesFragment}
fragment FormFields on Question {
...EnunciationFields
...AnswerFields
...DistractorsFields
...FeaturesFields
status
}
`
type Props = {
question?: Question
onSubmit?: (inputs: any) => void
onDraftSubmit?: (inputs: any) => void
alert?: AlertV2Props
}
export const Form: FC<Props> = ({question, onSubmit, onDraftSubmit, alert}) => {
const [validationErrors, setValidationErrors] = useState<string[]>([])
const [confirmSaveDialogIsOpen, setConfirmFinishDialogIsOpen] = useState(false)
const [leaveDialogIsOpen, setLeaveDialogIsOpen] = useState(false)
const {register, control, setValue, getValues, reset, formState} = useForm()
const [currentStep, setCurrentStep] = useState(0)
const unsavedChanges = useSelector((state: RootState) => state.unsavedChanges)
const history = useHistory()
const dispatch = useDispatch()
const minStep = 0
const maxStep = 3
const onFirstStep = currentStep === minStep
const onLastStep = currentStep === maxStep
if (formState.isDirty) {
dispatch(turnOn())
}
const handleNextStep = () => {
if (onLastStep) return
setCurrentStep(currentStep + 1)
}
const handlePreviousStep = () => {
if (onFirstStep) return
setCurrentStep(currentStep - 1)
}
const getFormattedInputValues = () => formatInput(getValues())
const handleCancel = () => {
if (unsavedChanges && !leaveDialogIsOpen) {
setLeaveDialogIsOpen(true)
} else {
history.push(QuestionRoutePaths.index)
}
}
const handleDraftSave = () => {
if (onDraftSubmit) {
onDraftSubmit({...getFormattedInputValues(), status: QuestionStatus.Draft} as QuestionCreateInput)
reset(getValues(), {
isDirty: false
})
dispatch(turnOff())
}
}
const handleSave = () => {
const inputs = {...getFormattedInputValues(), status: QuestionStatus.WaitingReview} as QuestionCreateInput
const errors = validateQuestionInputs(inputs)
setConfirmFinishDialogIsOpen(false)
if (onSubmit && !errors.length) {
dispatch(turnOff())
onSubmit(inputs)
} else {
setValidationErrors(errors)
}
reset(getValues(), {
isDirty: false
})
}
return (
<FormProvider props={{question, hooks: {register, control, setValue}}}>
{alert && (
<AlertV2 severity={alert.severity} text={alert.text}></AlertV2>
)}
<Dialog
isOpen={leaveDialogIsOpen}
setIsOpen={setLeaveDialogIsOpen}
onConfirmation={handleCancel}
title="Modificações não Salvas"
text="Todas as alterações serão descartadas. Deseja continuar?"
/>
<Dialog
isOpen={confirmSaveDialogIsOpen}
setIsOpen={setConfirmFinishDialogIsOpen}
onConfirmation={handleSave}
title="Modificações não Salvas"
text="Ao finalizar a questão, o revisor receberá uma notificação para revisá-la. Deseja continuar?"
/>
<Dialog
isOpen={!!validationErrors.length}
setIsOpen={() => setValidationErrors([])}
onConfirmation={() => setValidationErrors([])}
title="Falha de Validação"
type="notice"
text={
<>
<List>
{validationErrors?.map((errorMessage) => (
<ListItem
key={errorMessage}
icon={<ExclamationCircleIcon className="w-5 text-gray-800"/>}
text={errorMessage}
/>
))}
</List>
</>
}
/>
<form className="m-auto max-w-screen-md">
<SteppedForm
currentStep={currentStep}
className="mb-3"
>
<Step step={0}>
<EnunciationFormStep/>
</Step>
<Step step={1}>
<AnswerFormStep/>
</Step>
<Step step={2}>
<DistractorsFormStep/>
</Step>
<Step step={3}>
<FeaturesFormStep/>
</Step>
</SteppedForm>
<div
className="mx-3 sm:mx-0 flex justify-items-center flex-col-reverse sm:flex-row justify-end space-x-0 sm:space-x-2">
<Button
className={"mb-3 sm:mb-0"}
onClick={handleCancel}
>
Cancelar
</Button>
<Button
className={`mb-3 sm:mb-0 ${onFirstStep ? "hidden" : ""}`}
onClick={handlePreviousStep}
>
Retornar
</Button>
{(question?.status === QuestionStatus.Draft || question?.status === undefined) &&
<Button className={"mb-3 sm:mb-0"} onClick={handleDraftSave}>
Salvar Rascunho
</Button>
}
<Button
type="primary"
className={`mb-3 sm:mb-0 ${onLastStep ? "hidden" : ""}`}
onClick={handleNextStep}
>
Prosseguir
</Button>
{onLastStep &&
<Button
type="primary"
className="mb-3 sm:mb-0"
onClick={handleSave}
>
Finalizar
</Button>
}
</div>
</form>
</FormProvider>
)
}

View File

@@ -0,0 +1,39 @@
import React, { FC, useContext } from 'react'
import { Control, FieldValues } from 'react-hook-form';
import { Question } from '../../../__generated__/graphql-schema';
type FormContextHooks = {
register: any
setValue: Function
control: Control<FieldValues>
}
type FormContextProps = {
hooks: FormContextHooks
question?: Question
}
const FormContext = React.createContext<FormContextProps | null>(null);
export const useFormProvider = () => {
const context = useContext(FormContext)
if (context === null) {
throw new Error('You probably forgot to put <FormProvider>.')
}
return context
}
type Props = {
children?: any
props: FormContextProps
}
export const FormProvider: FC<Props> = ({ children, props }) => {
return (
<FormContext.Provider value={props}>
{children}
</FormContext.Provider>
)
}

View File

@@ -0,0 +1,34 @@
import React, { FC } from "react";
type StepProps = {
children: any
step: number
}
export const Step: FC<StepProps> = ({ children }) => (children);
type Props = {
children: any;
currentStep: number;
className?: string;
};
export const SteppedForm: FC<Props> = ({
children,
currentStep,
className = '',
}) => {
return (
<div className={className}>
{children?.map((x: any) => {
const visible = x.props.step === currentStep;
return (
<div key={x.props.step} className={visible ? "" : "hidden"}>
{x}
</div>
);
})}
</div>
);
};

View File

@@ -0,0 +1,55 @@
import React, { FC } from "react";
import { Controller } from "react-hook-form";
import CKEditor from "@ckeditor/ckeditor5-react";
import * as ClassicEditor from "ckeditor5-mathtype/build/ckeditor";
import { useFormProvider } from '../FormContext'
const toolbarOptions = [
"bold",
"italic",
"blockQuote",
"numberedList",
"bulletedList",
"imageUpload",
"insertTable",
"tableColumn",
"tableRow",
"mergeTableCells",
"|",
"MathType",
"ChemType",
"|",
"undo",
"redo",
];
type Props = {
name: string
defaultValue: string
}
export const TextEditor: FC<Props> = ({ name, defaultValue }) => {
const { hooks: { control } } = useFormProvider()
return (
<Controller
control={control}
name={name}
defaultValue={defaultValue}
render={({ onChange, value }) => (
<CKEditor
editor={ClassicEditor}
data={value}
config={{
toolbar: toolbarOptions,
ckfinder: {
uploadUrl: `${process.env.REACT_APP_BACKEND_URL}/uploads`,
},
}}
onChange={(_: any, editor: any) => onChange(editor.getData())}
/>
)}
/>
);
};

View File

@@ -0,0 +1 @@
export * from "./Form";

View File

@@ -0,0 +1,61 @@
import { gql } from "@apollo/client";
import React, { FC } from "react";
import { Controller } from "react-hook-form";
import { Card } from "../../../../components/Card/Card";
import { TextEditor } from "../components/TextEditor";
import { useFormProvider } from '../FormContext'
export const AnswerFragment = gql`
fragment AnswerFields on Question {
alternatives {
correct
text
}
explanation
references
}
`
export const AnswerFormStep: FC = () => {
const { question, hooks: { control } } = useFormProvider()
const alternativesMaped = question?.alternatives || [
{ text: "", correct: true },
];
const correctAlternative = alternativesMaped.find(
(alternative) => alternative.correct === true,
);
return (
<>
<Card title="Resposta Correta" className="mb-3">
<div className="flex flex-col">
<div className="w-full">
<TextEditor
name={`alternatives[0].text`}
defaultValue={correctAlternative?.text ?? ''}
/>
<Controller
name={`alternatives[0].correct`}
control={control}
defaultValue={true}
render={() => (<></>)}
/>
</div>
<div className="flex flex-col w-full border border-gray-300 rounded p-4 mt-4 shadow-sm">
<div>
<h2 className="text-xl font-medium">Explicação</h2>
<TextEditor name="explanation" defaultValue={question?.explanation ?? ''} />
</div>
<div>
<h2 className="text-xl font-medium">Referências</h2>
<TextEditor defaultValue={question?.references ?? ''} name="references" />
</div>
</div>
</div>
</Card>
</>
);
};

View File

@@ -0,0 +1,54 @@
import React, { FC } from "react";
import { Controller } from "react-hook-form";
import { gql } from "@apollo/client";
import { Card } from "../../../../components";
import { TextEditor } from "../components/TextEditor";
import { useFormProvider } from '../FormContext'
export const DistractorsFragment = gql`
fragment DistractorsFields on Question {
alternatives {
correct
text
}
}
`
export const DistractorsFormStep: FC = () => {
const { question, hooks: { control } } = useFormProvider()
const incorrectAnswers = question?.alternatives?.filter(
(alternative) => alternative.correct === false,
) || [
{ text: "", correct: false },
{ text: "", correct: false },
{ text: "", correct: false },
{ text: "", correct: false },
];
return (
<>
<Card title="Distratores">
<div className="flex flex-col">
<div className="">
{incorrectAnswers.map(({ text }, index) => (
<div className="w-full mb-3" key={index}>
<TextEditor
name={`alternatives[${index + 1}].text`}
defaultValue={text ?? ""}
/>
<Controller
name={`alternatives[${index + 1}].correct`}
control={control}
defaultValue={false}
render={() => (<></>)}
/>
</div>
))}
</div>
</div>
</Card>
</>
);
};

View File

@@ -0,0 +1,32 @@
import { gql } from "@apollo/client";
import React, { FC } from "react";
import { Card } from "../../../../components";
import { TextEditor } from '../components/TextEditor'
import { useFormProvider } from '../FormContext'
export const EnunciationFragment = gql`
fragment EnunciationFields on Question {
instruction
support
body
}
`
export const EnunciationFormStep: FC = () => {
const { question } = useFormProvider()
return (
<>
<Card className="h-full mb-3" title="Instrução (opcional)">
<TextEditor name="instruction" defaultValue={question?.instruction ?? ""} />
</Card>
<Card className="h-full mb-3" title="Suporte (opcional)">
<TextEditor name="support" defaultValue={question?.support ?? ""} />
</Card>
<Card className="h-full mb-3" title="Enunciado">
<TextEditor name="body" defaultValue={question?.body ?? ""} />
</Card>
</>
);
};

View File

@@ -0,0 +1,192 @@
import React, { FC, useState } from "react";
import { gql } from "@apollo/client";
import { Card } from "../../../../../components";
import { SubjectSelect, SubjectFragment } from "./SubjectSelect";
import { ReviewerSelect, ReviewerFragment } from "./ReviewSelect";
import { useFormProvider } from '../../FormContext'
import { BLOOM_TAXONOMY, CHECK_TYPE, DIFFICULTY } from "../../../../../utils/types";
import { Question } from "../../../../../__generated__/graphql-schema";
export const FeaturesFragment = gql`
${ReviewerFragment}
${SubjectFragment}
fragment FeaturesFields on Question {
... ReviewerFields
... SubjectFields
authorship
authorshipYear
difficulty
checkType
intention
bloomTaxonomy
}
`
export const FeaturesFormStep: FC = () => {
const { question, hooks: { setValue, register } } = useFormProvider();
const currentYear = new Date().getFullYear();
const {
authorship,
authorshipYear,
difficulty,
bloomTaxonomy,
checkType,
} = question || {} as Question
const [ownQuestion, setOwnQuestion] = useState<boolean>(authorship === "UNIFESO" || authorship === undefined || authorship === null);
const handleOwnCheck = (value: string) => {
if (value === 'UNIFESO') {
setOwnQuestion(true)
setValue("authorship", "UNIFESO");
setValue("authorshipYear", currentYear.toString());
} else {
setOwnQuestion(false)
setValue("authorship", "");
setValue("authorshipYear", "");
}
};
return (
<>
<Card title="Características">
<div className="grid grid-cols-2 col-gap-2">
<div className="flex">
<label htmlFor="own" className="mr-3 my-auto">
Autoria
</label>
<div className="my-auto">
<input
className="my-auto"
type="radio"
id="authorship-own"
checked={!!ownQuestion}
ref={register}
onChange={() => handleOwnCheck("UNIFESO")}
name="__nonused"
/>
<label htmlFor="authorship-own" className="ml-1">Própria</label>
</div>
<div className="my-auto ml-3">
<input
className="my-auto"
type="radio"
id="authorship-third"
checked={!ownQuestion}
ref={register}
onChange={() => handleOwnCheck("")}
name="__nonused"
/>
<label htmlFor="authorship-third" className="ml-1">Outro</label>
</div>
</div>
<div className="flex">
<div className="flex">
<h2 className="pr-2 my-auto">Fonte</h2>
<div className="w-full">
<div style={{ maxWidth: "194px" }}>
<input
className="block rounded p-1 w-full border-gray-400 border shadow-sm"
ref={register}
name="authorship"
defaultValue={authorship || (ownQuestion ? "UNIFESO" : "")}
readOnly={!!ownQuestion}
/>
</div>
</div>
</div>
<div className="flex">
<h2 className="pr-2 pl-3 my-auto">Ano</h2>
<div style={{ maxWidth: "62px" }}>
<input
className="w-full rounded p-1 border-gray-400 border shadow-sm"
ref={register}
type="number"
min="1999"
max={currentYear}
step="1"
name="authorshipYear"
defaultValue={authorshipYear ?? new Date().getFullYear().toString()}
readOnly={!!ownQuestion}
/>
</div>
</div>
</div>
</div>
<div className="grid grid-cols-2 col-gap-2 mt-3">
<div className="w-full grid grid-cols-1 row-gap-4">
<div className="flex flex-col">
<h2>Grau de Dificuldade</h2>
<select
ref={register}
className="w-full rounded p-1 border-gray-400 border shadow-sm"
name="difficulty"
defaultValue={difficulty ?? ""}
>
<option />
{DIFFICULTY.map((item, index) => (
<option key={index} value={item.value}>
{item.label}
</option>
))}
</select>
</div>
<div className="w-full">
<h2>Tipo</h2>
<select
ref={register}
className="w-full rounded p-1 border-gray-400 border shadow-sm"
name="checkType"
defaultValue={checkType ?? ""}
>
<option />
{CHECK_TYPE.map((item, index) => (
<option key={index} value={item.value}>
{item.label}
</option>
))}
</select>
</div>
<div className="w-full">
<h2>Habilidade Cognitiva</h2>
<select
ref={register}
className="w-full rounded p-1 border-gray-400 border shadow-sm"
name="bloomTaxonomy"
defaultValue={bloomTaxonomy ?? ""}
>
<option />
{BLOOM_TAXONOMY.map((item, index) => (
<option key={index} value={item.value}>
{item.label}
</option>
))}
</select>
</div>
</div>
<div className="w-full">
<SubjectSelect />
</div>
</div>
<div className="flex flex-col mt-4">
<h2>Intenção</h2>
<textarea
className="block rounded p-1 w-full border-gray-400 border shadow-sm"
ref={register}
name="intention"
defaultValue={question?.intention ?? ""}
/>
</div>
<div className="flex flex-col mt-4">
<h2>Revisor</h2>
<ReviewerSelect />
</div>
</Card>
</>
);
};

View File

@@ -0,0 +1,54 @@
import React, { FC } from "react";
import { gql, useQuery } from "@apollo/client";
import { useFormProvider } from '../../FormContext'
import { Query, QuestionStatus, User } from "../../../../../__generated__/graphql-schema";
export const ReviewerFragment = gql`
fragment ReviewerFields on Question {
reviewer {
id
}
}
`
const REVIEWERS_QUERY = gql`
query ReviwersQuery {
reviewers {
nodes {
id
name
email
}
}
}
`
type Props = {
reviewer?: User
}
export const ReviewerSelect: FC<Props> = () => {
const { question, hooks: { register } } = useFormProvider()
const { loading, data } = useQuery<Query>(REVIEWERS_QUERY);
if (loading) return null;
const reviewers = data?.reviewers.nodes
return (
<select
ref={register}
className="w-full rounded p-1 border-gray-400 border shadow-sm"
name="reviewerUserId"
defaultValue={question?.reviewer?.id}
>
{(question?.status === undefined || question?.status === QuestionStatus.Draft) && <option />}
{reviewers?.map((review, index) => (
<option key={index} value={review?.id}>
{review?.name}
</option>
))}
</select>
);
};

View File

@@ -0,0 +1,89 @@
import React, { FC, useState } from "react";
import { useQuery, gql } from "@apollo/client";
import { Query } from "../../../../../__generated__/graphql-schema";
import { useFormProvider } from '../../FormContext'
type Props = {
subjectId?: string
}
export const SubjectFragment = gql`
fragment SubjectFields on Question {
subject {
id
}
}
`
const SUBJECTS_QUERY = gql`
query SubjectQuery {
subjects {
nodes {
id
name
axis {
name
}
category {
name
}
}
}
}
`
export const SubjectSelect: FC<Props> = () => {
const { question, hooks: { register } } = useFormProvider()
const [selectedId, setSelectedId] = useState(question?.subject?.id);
const { loading, data } = useQuery<Query>(SUBJECTS_QUERY);
if (loading) return null;
const subjects = data?.subjects.nodes
const selectedSubject = data?.subjects.nodes?.find((subject) => subject?.id === selectedId);
return (
<div className="flex flex-col h-full">
<div>
<h2>Assunto</h2>
<select
ref={register}
className="w-full rounded p-1 border-gray-400 border shadow-sm"
name="subjectId"
defaultValue={question?.subject?.id ?? ""}
onChange={(e) => setSelectedId(e.target.value)}
>
<option value="" />
{subjects?.map((subject) => (
<option
key={`${subject?.name}-${subject?.id}`}
value={subject?.id}
>
{subject?.name}
</option>
))}
</select>
</div>
<span className="mt-4">
Eixo de Formação
<input
className="block rounded p-1 w-full border-gray-400 border shadow-sm"
disabled
value={selectedSubject?.axis.name}
/>
</span>
<span className="mt-4">
Categoria
<input
className="block rounded p-1 w-full border-gray-400 border shadow-sm"
disabled
value={selectedSubject?.category.name}
/>
</span>
</div>
);
};

View File

@@ -0,0 +1 @@
export * from "./FeaturesFormStep";

View File

@@ -0,0 +1,4 @@
export * from "./EnunciationFormStep";
export * from "./AnswerFormStep";
export * from "./DistractoresFormStep";
export * from "./FeaturesFromStep";