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,24 @@
import React, {FC,} from 'react'
import {DashboardProvider} from './DashboardContext'
import {
QuestionsBySubject,
QuestionByBloomTaxonomy,
QuestionsByDifficulty,
QuestionByCheckType,
} from './charts'
import {Filters} from './Filters'
export const Dashboard: FC = () => (
<DashboardProvider>
<main className="max-h-screen sm:px-8 gap-2 pt-2 sm:pt-4">
<Filters/>
<div className="pt-3 grid gap-2 grid-cols-1 md:grid-cols-2 2xl:grid-cols-3 3xl:grid-cols-4">
<QuestionsBySubject/>
<QuestionByBloomTaxonomy/>
<QuestionsByDifficulty/>
<QuestionByCheckType/>
</div>
</main>
</DashboardProvider>
)

View File

@@ -0,0 +1,47 @@
import React, {
createContext,
useState,
useMemo,
FC,
useContext,
Dispatch,
SetStateAction,
} from 'react'
import {QuestionWhereInput} from '../../__generated__/graphql-schema'
import {UserContext, useCurrentUser} from "../../contexts";
type ProviderValue = {
where: QuestionWhereInput
setWhere: Dispatch<SetStateAction<QuestionWhereInput>>
}
const DashboardContext = createContext<ProviderValue | null>(null)
export const useDashboardContext = () => {
const context = useContext(DashboardContext)
if (context === null) {
throw new Error('You probably forgot to put <DashboardProvider>.')
}
return context
}
export const whereDefaultState = (userContext: UserContext) => (
userContext.isOnlyTeacher ? {userId: userContext.user?.id} : {}
)
export const DashboardProvider: FC = ({children}) => {
const userContext = useCurrentUser()
const [where, setWhere] = useState<QuestionWhereInput>(whereDefaultState(userContext))
const providerValue = useMemo(() => ({where, setWhere}), [
where,
setWhere,
])
return (
<DashboardContext.Provider value={providerValue}>
{children}
</DashboardContext.Provider>
)
}

View File

@@ -0,0 +1,155 @@
import React, {FC, Fragment} from 'react'
import {Disclosure, Transition} from "@headlessui/react"
import {ChevronDownIcon, XIcon} from "@heroicons/react/outline"
import {useForm} from "react-hook-form"
import {QuestionWhereInput} from "../../__generated__/graphql-schema"
import {useDashboardContext, whereDefaultState} from "./DashboardContext"
import {useCurrentUser} from "../../contexts"
import {Button, Input} from "../../components"
type FilterBarForm = {
fromOtherUsers?: boolean
createDate: {
startAt: string
endAt: string
}
}
const startDateISO8601Date = '2021-01-01'
const currentISO8601Date = new Date().toISOString().split('T')[0]
const formDefaultValues: FilterBarForm = {
fromOtherUsers: false,
createDate: {
startAt: startDateISO8601Date,
endAt: currentISO8601Date
}
}
const mapFilter = (values: FilterBarForm, userId?: string): QuestionWhereInput => ({
userId: values.fromOtherUsers ? undefined : userId,
createDate: {
startAt: values.createDate.startAt.length ? values.createDate.startAt : startDateISO8601Date,
endAt: values.createDate.endAt.length ? values.createDate.endAt : currentISO8601Date,
}
})
const FiltersForm: FC = () => {
const {register, handleSubmit, reset, getValues, formState} = useForm({
defaultValues: formDefaultValues,
})
const {setWhere} = useDashboardContext()
const userContext = useCurrentUser()
const {user, isOnlyTeacher} = userContext
const onSubmit = (values: FilterBarForm) => {
reset(getValues(), {
isDirty: false
})
setWhere(mapFilter(values, user?.id))
}
const handleClean = () => {
reset(formDefaultValues)
setWhere(whereDefaultState(userContext))
}
return (
<form
onSubmit={handleSubmit(onSubmit)}
className={"flex justify-between"}
>
<span>
<label className={"pl-2 pt-2"}>Data de Criação</label>
<div className={"grid grid-cols-2 gap-2 border p-2 m-2 rounded-md border-gray-300"}>
<Input
type="date"
placeholder="createDate.startAt"
ref={register({
maxLength: 10,
minLength: 10,
})}
name={"createDate.startAt"}
label={"A Partir De"}
/>
<Input
type="date"
placeholder="createDate.endAt"
ref={register({
maxLength: 10,
minLength: 10,
})}
name={"createDate.endAt"}
label={"Até"}
/>
</div>
</span>
{!isOnlyTeacher && (
<span className={"flex items-center"}>
<label
htmlFor={"fromOtherUsers"}
children={"Apenas questões próprias?"}
className={"mr-3"}
/>
<input
id={"fromOtherUsers"}
type="checkbox"
placeholder="fromOtherUsers"
ref={register}
name={"fromOtherUsers"}
/>
</span>
)}
<div className={"grid grid-cols-2 gap-2 place-items-center"}>
<div>
<Button type={'tertiary'} onClick={handleClean}>
<span className={"flex"}>
<XIcon className={"w-5 h-5 text-gray-800"}/>
Limpar filtro
</span>
</Button>
</div>
<div>
<Button disabled={!formState.isDirty} type={'primary'} htmlType={"submit"} className={"w-full"}>
Filtar
</Button>
</div>
</div>
</form>
);
}
export const Filters: FC = () => (
<Disclosure>
{({open}) => (
<div className="m-auto bg-white rounded-md shadow-sm hover:shadow transition-shadow duration-300">
<Disclosure.Button as={Fragment}>
<button className="flex p-2 w-full justify-between">
<div className="grid place-items-center pl-4">
Filtros
</div>
<div className={"pr-4"}>
<ChevronDownIcon
className={`${open ? 'transform rotate-180' : ''} w-5 h-5 text-gray-800`}
/>
</div>
</button>
</Disclosure.Button>
<Transition
show={open}
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
>
<Disclosure.Panel className={"p-4"}>
<FiltersForm/>
</Disclosure.Panel>
</Transition>
</div>
)}
</Disclosure>
)

View File

@@ -0,0 +1,106 @@
import React, {FC} from 'react'
import {gql, useQuery} from '@apollo/client'
import {Query} from '../../../__generated__/graphql-schema'
import {Pie} from '../components/charts'
import {useDashboardContext} from "../DashboardContext";
type ResponsivePieData = {
id: string
label: string
value: number
}[]
type QuestionsByBloomTaxonomyCountQuery = {
remember: Query['questions']
understand: Query['questions']
apply: Query['questions']
analyze: Query['questions']
evaluate: Query['questions']
create: Query['questions']
}
const QuestionsByBloomTaxonomyCount = gql`
query QuestionsByBloomTaxonomyCount (
$rememberWhere: QuestionWhereInput!,
$understandWhere: QuestionWhereInput!,
$applyWhere: QuestionWhereInput!,
$analyzeWhere: QuestionWhereInput!,
$evaluateWhere: QuestionWhereInput!,
$createWhere: QuestionWhereInput!,
) {
remember: questions(where: $rememberWhere) {
totalCount
}
understand: questions(where: $understandWhere) {
totalCount
}
apply: questions(where: $applyWhere) {
totalCount
}
analyze: questions(where: $analyzeWhere) {
totalCount
}
evaluate: questions(where: $evaluateWhere) {
totalCount
}
create: questions(where: $createWhere) {
totalCount
}
}
`
export const QuestionByBloomTaxonomy: FC = () => {
const {where} = useDashboardContext()
const {loading, data} = useQuery<QuestionsByBloomTaxonomyCountQuery>(
QuestionsByBloomTaxonomyCount, {
variables: {
rememberWhere: {bloomTaxonomy: ['remember'], ...where},
understandWhere: {bloomTaxonomy: ['understand'], ...where},
applyWhere: {bloomTaxonomy: ['apply'], ...where},
analyzeWhere: {bloomTaxonomy: ['analyze'], ...where},
evaluateWhere: {bloomTaxonomy: ['evaluate'], ...where},
createWhere: {bloomTaxonomy: ['create'], ...where},
}
})
if (loading || !data) return null
const mappedData: ResponsivePieData = [
{
id: "Recordar",
label: "Recordar",
value: data.remember.totalCount ?? 0
},
{
id: "Compreender",
label: "Compreender",
value: data.understand.totalCount ?? 0
},
{
id: "Aplicar",
label: "Aplicar",
value: data.apply.totalCount ?? 0
},
{
id: "Analisar",
label: "Analisar",
value: data.analyze.totalCount ?? 0
},
{
id: "Avaliar",
label: "Avaliar",
value: data.evaluate.totalCount ?? 0
},
{
id: "Criar",
label: "Criar",
value: data.create.totalCount ?? 0
},
]
const filteredData = mappedData.filter(item => item.value)
return (
<Pie title="Questões por Habilidade Cognitiva" data={filteredData}/>
)
}

View File

@@ -0,0 +1,150 @@
import React, {FC} from 'react'
import {gql, useQuery} from '@apollo/client'
import {Query} from '../../../__generated__/graphql-schema'
import {Pie} from '../components/charts'
import {useDashboardContext} from "../DashboardContext";
type ResponsivePieData = {
id: string
label: string
value: number
}[]
type QuestionsByCheckTypeCountQuery = {
uniqueAnswer: Query['questions']
incompleteAffirmation: Query['questions']
multipleAnswer: Query['questions']
negativeFocus: Query['questions']
assertionAndReason: Query['questions']
gap: Query['questions']
interpretation: Query['questions']
association: Query['questions']
orderingOrRanking: Query['questions']
constantAlternatives: Query['questions']
}
const QuestionsByCheckTypeCount = gql`
query QuestionsByCheckTypeCount(
$uniqueAnswer: QuestionWhereInput!,
$incompleteAffirmation: QuestionWhereInput!,
$multipleAnswer: QuestionWhereInput!,
$negativeFocus: QuestionWhereInput!,
$assertionAndReason: QuestionWhereInput!,
$gap: QuestionWhereInput!,
$interpretation: QuestionWhereInput!,
$association: QuestionWhereInput!,
$orderingOrRanking: QuestionWhereInput!,
$constantAlternatives: QuestionWhereInput!,
) {
uniqueAnswer: questions(where: $uniqueAnswer) {
totalCount
}
incompleteAffirmation: questions(where: $incompleteAffirmation) {
totalCount
}
multipleAnswer: questions(where: $multipleAnswer) {
totalCount
}
negativeFocus: questions(where: $negativeFocus) {
totalCount
}
assertionAndReason: questions(where: $assertionAndReason) {
totalCount
}
gap: questions(where: $gap) {
totalCount
}
interpretation: questions(where: $interpretation) {
totalCount
}
association: questions(where: $association) {
totalCount
}
orderingOrRanking: questions(where: $orderingOrRanking) {
totalCount
}
constantAlternatives: questions(where: $constantAlternatives) {
totalCount
}
}
`
export const QuestionByCheckType: FC = () => {
const {where} = useDashboardContext()
const {loading, data} = useQuery<QuestionsByCheckTypeCountQuery>(
QuestionsByCheckTypeCount, {
variables: {
uniqueAnswer: {checkType: ['unique_answer'], ...where},
incompleteAffirmation: {checkType: ['incomplete_affirmation'], ...where},
multipleAnswer: {checkType: ['multiple_answer'], ...where},
negativeFocus: {checkType: ['negative_focus'], ...where},
assertionAndReason: {checkType: ['assertion_and_reason'], ...where},
gap: {checkType: ['gap'], ...where},
interpretation: {checkType: ['interpretation'], ...where},
association: {checkType: ['association'], ...where},
orderingOrRanking: {checkType: ['ordering_or_ranking'], ...where},
constantAlternatives: {checkType: ['constant_alternatives'], ...where},
}
})
if (loading || !data) return null
const mappedData: ResponsivePieData = [
{
id: "Asserção e Razão",
label: "Asserção e Razão",
value: data.assertionAndReason.totalCount ?? 0
},
{
id: "Associação",
label: "Associação",
value: data.association.totalCount ?? 0
},
{
id: "Alternativas Constantes",
label: "Alternativas Constantes",
value: data.constantAlternatives.totalCount ?? 0
},
{
id: "Lacuna",
label: "Lacuna",
value: data.gap.totalCount ?? 0
},
{
id: "Afirmação Incompleta",
label: "Afirmação Incompleta",
value: data.incompleteAffirmation.totalCount ?? 0
},
{
id: "Interpretação",
label: "Interpretação",
value: data.interpretation.totalCount ?? 0
},
{
id: "Resposta Múltipla",
label: "Resposta Múltipla",
value: data.multipleAnswer.totalCount ?? 0
},
{
id: "Foco Negativo",
label: "Foco Negativo",
value: data.negativeFocus.totalCount ?? 0
},
{
id: "Ordenação ou Seriação",
label: "Ordenação ou Seriação",
value: data.orderingOrRanking.totalCount ?? 0
},
{
id: "Resposta Única",
label: "Resposta Única",
value: data.uniqueAnswer.totalCount ?? 0
},
]
const filteredData = mappedData.filter(item => item.value)
return (
<Pie title="Questões por Tipo" data={filteredData}/>
)
}

View File

@@ -0,0 +1,73 @@
import React, {FC} from 'react'
import {gql, useQuery} from '@apollo/client'
import {Query} from '../../../__generated__/graphql-schema'
import {Pie} from '../components/charts'
import {useDashboardContext} from "../DashboardContext";
type ResponsivePieData = {
id: string
label: string
value: number
}[]
type QuestionsByDifficultyCountQuery = {
easy: Query['questions']
medium: Query['questions']
hard: Query['questions']
}
const QuestionsByDifficultyCount = gql`
query QuestionsByDifficultyCount(
$easyWhere: QuestionWhereInput!,
$mediumWhere: QuestionWhereInput!,
$hardWhere: QuestionWhereInput!,
) {
easy: questions(where: $easyWhere) {
totalCount
}
medium: questions(where: $mediumWhere) {
totalCount
}
hard: questions(where: $hardWhere) {
totalCount
}
}
`
export const QuestionsByDifficulty: FC = () => {
const {where} = useDashboardContext()
const {loading, data} = useQuery<QuestionsByDifficultyCountQuery>(
QuestionsByDifficultyCount, {
variables: {
easyWhere: {difficulty: ['easy'], ...where},
mediumWhere: {difficulty: ['medium'], ...where},
hardWhere: {difficulty: ['hard'], ...where},
},
})
if (loading || !data) return null
const mappedData: ResponsivePieData = [
{
id: "Fácil",
label: "Fácil",
value: data.easy.totalCount ?? 0
},
{
id: "Difícil",
label: "Difícil",
value: data.hard.totalCount ?? 0
},
{
id: "Média",
label: "Média",
value: data.medium.totalCount ?? 0
},
]
const filteredData = mappedData.filter(item => item.value)
return (
<Pie title="Questões por Dificuldade" data={filteredData}/>
)
}

View File

@@ -0,0 +1,50 @@
import React, {FC} from 'react'
import {gql, useQuery} from '@apollo/client'
import {Query} from '../../../__generated__/graphql-schema'
import {Pie} from '../components/charts'
import {useDashboardContext} from "../DashboardContext";
type ResponsivePieData = {
id: string
label: string
value: number
}[]
const QuestionsBySubjectCount = gql`
query QuestionsBySubjectCount($where: QuestionWhereInput!) {
subjects {
nodes {
id
name
questions(where: $where) {
totalCount
}
}
}
}
`
export const QuestionsBySubject: FC = () => {
const {where} = useDashboardContext()
const {loading, data} = useQuery<Query>(QuestionsBySubjectCount, {
variables: {
where,
},
})
if (loading) return null
const subjects = data?.subjects.nodes ?? []
const subjectWithQuestions = subjects.filter(subject => !!subject?.questions.totalCount)
const mappedData: ResponsivePieData = subjectWithQuestions.map(subject => ({
id: subject.name,
label: subject.name,
value: subject.questions.totalCount,
}))
const filteredData = mappedData.filter(item => item.value)
return (
<Pie title="Questões por Assunto" data={filteredData}/>
)
}

View File

@@ -0,0 +1,4 @@
export * from "./QuestionsBySubject";
export * from "./QuestionsByBloomTaxonomy";
export * from "./QuestionsByDifficulty";
export * from "./QuestionsByCheckType";

View File

@@ -0,0 +1,38 @@
import {ResponsivePie} from '@nivo/pie'
import React, {FC} from 'react'
type Props = {
title: string
data: {
id: string
label: string
value: number
}[]
}
export const Pie: FC<Props> = ({title, data}) => {
return (
<div
className="m-auto bg-white rounded-md p-4 shadow-sm hover:shadow transition-shadow duration-300"
style={{ height: '36rem', width: '36rem' }}
>
<h3 className="text-lg leading-6 font-medium text-gray-900">{title}</h3>
<ResponsivePie
data={data}
margin={{top: 40, right: 80, bottom: 80, left: 80}}
innerRadius={0.5}
padAngle={0.7}
cornerRadius={3}
activeOuterRadiusOffset={8}
borderWidth={1}
borderColor={{from: 'color', modifiers: [['darker', 0.2]]}}
arcLinkLabelsSkipAngle={10}
arcLinkLabelsTextColor="#333333"
arcLinkLabelsThickness={2}
arcLinkLabelsColor={{from: 'color'}}
arcLabelsSkipAngle={10}
arcLabelsTextColor={{from: 'color', modifiers: [['darker', 2]]}}
/>
</div>
)
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,127 @@
import React, {FC, useState} from 'react'
import {useHistory, useParams} from 'react-router';
import {gql, useMutation, useQuery} from '@apollo/client';
import {Mutation, Query, Question} from '../../../__generated__/graphql-schema';
import {AlertV2Props, Navigator} from '../../../components';
import {Form, FormFragments} from '../Form'
import {NodeId} from '../../../utils/graphql';
import {QuestionRoutePaths} from "../../../routes";
const GET_QUESTION = gql`
${FormFragments}
query Question ($id: ID!) {
node (id: $id) {
__typename
...on Question {
id
...FormFields
}
}
}
`;
const UPDATE_QUESTION_MUTATOIN = gql`
mutation($input: UpdateQuestionInput!) {
updateQuestion(input: $input) {
question {
id
}
errors
}
}
`
export const Edit: FC = () => {
const history = useHistory()
const [alert, setAlert] = useState<AlertV2Props>()
const params = useParams<{ id: string }>()
const [updateQuestion] = useMutation<Mutation>(UPDATE_QUESTION_MUTATOIN)
const {loading, data} = useQuery<Query>(
GET_QUESTION, {
variables: {
id: params.id,
fetchPolicy: "no-cache"
}
}
)
const question = data?.node as Question | null
if (loading || !question) return null
const recordId = NodeId.decode(question.id).id
const onSubmit = (inputs: any) => {
updateQuestion({
variables: {
input: {
question: {
...inputs,
id: recordId,
},
},
},
}).then(() => {
history.push(QuestionRoutePaths.index)
}).catch((error: string) => {
setAlert({
severity: "error",
text: `Erro ao atualizar questão. ${error}. Por favor, tente novamente.`,
});
setTimeout(
() => setAlert({severity: "error", text: ""}),
3000
);
})
}
const onDraftSubmit = (inputs: any) => {
updateQuestion({
variables: {
input: {
question: {
...inputs,
id: recordId,
},
},
}
}).then(() => {
setAlert({
severity: "success",
text: "Rascunho atualizado com sucesso",
});
setTimeout(() => {
setAlert(undefined)
}, 3000);
}).catch((error: string) => {
setAlert({
severity: "error",
text: `Erro ao atualizar rascunho. ${error}`,
});
setTimeout(
() => setAlert(undefined),
8000
);
})
}
return (
<>
<Navigator home/>
<div className="bg-gray-100 w-full my-2">
<main>
<Form
question={question}
onSubmit={onSubmit}
onDraftSubmit={onDraftSubmit}
alert={alert}
/>
</main>
</div>
</>
)
}

View File

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

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";

View File

@@ -0,0 +1,35 @@
import React, { FC, useState } from "react";
import { FaFilter } from "react-icons/fa";
import { Navigator } from "../../../components";
import { QuestionsFilter } from "./QuestionFilter";
import { QuestionsPainel } from "./QuestionsPainel";
import { FiltersProvider } from './QuestionFilter/QuestionsFilterProvider'
export const List: FC = () => {
const [filterOpen, setFilterOpen] = useState(false);
return (
<FiltersProvider>
<Navigator newQuestion={true}>
<li className={"hover:text-white ml-auto"}>
<button onClick={() => setFilterOpen(true)} className="flex">
<FaFilter className="my-auto" />
<span className="pl-3">Filtros</span>
</button>
</li>
</Navigator>
<QuestionsFilter
isOpen={filterOpen}
setIsOpen={setFilterOpen}
/>
<div className="bg-gray-100 w-full">
<main className="sm:px-8 rounded-t-xlg">
<div className="mx-2 sm:mx-0 sm:mr-4">
<QuestionsPainel />
</div>
</main>
</div>
</FiltersProvider>
);
};

View File

@@ -0,0 +1,49 @@
import React, { Dispatch, FC, SetStateAction } from "react";
type Props = {
setChanged: Dispatch<SetStateAction<boolean>>
register: any
}
export const AuthorshipFilter: FC<Props> = ({ setChanged, register }) => {
const options = [
{
label: "Qualquer",
value: 'null'
},
{
label: "Própria",
value: 'true'
},
{
label: "Terceiro",
value: 'false',
}
]
return (
<div className="mt-2 sm:mt-0 flex flex-col">
<h3 className="font-bold mb-1">Autoria</h3>
<div
className="grid grid-cols-2 sm:flex sm:flex-col"
key={`filter-group-authorship`}
>
{options.map(({ value, label }, index) => (
<span className="mr-1 mb-2 sm:mb-0 sm:mr-0" key={label}>
<input
ref={register}
type="radio"
name="authorship"
value={value}
id={value}
defaultChecked={!index}
/>
<label htmlFor={value} className="ml-2">
{label}
</label>
</span>
))}
</div>
</div>
)
}

View File

@@ -0,0 +1,39 @@
import React, { Dispatch, FC, SetStateAction } from 'react'
import { range } from '../../../../utils/math'
import { useFiltersProvider } from './QuestionsFilterProvider'
type Props = {
register: any
setChanged: Dispatch<SetStateAction<boolean>>
}
const CURRENT_YEAR = new Date().getFullYear()
const YEARS = range(1900, CURRENT_YEAR + 1).reverse()
export const QuestionsAuthorshipTypeFilter: FC<Props> = ({ register, setChanged }) => {
const { where } = useFiltersProvider()
return (
<div>
<select
ref={register}
className="w-full rounded p-1 border-gray-400 border shadow-sm"
name="authorshipYear"
defaultValue={where.authorshipYear ?? ""}
onClick={() => setChanged(true)}
>
<option value="" />
{YEARS.map((year) => (
<option
key={`questionYear-${year}`}
value={year}
>
{year}
</option>
))}
</select>
</div>
)
}

View File

@@ -0,0 +1,185 @@
import React, { Dispatch, FC, SetStateAction, useRef, useState } from "react";
import { useForm } from "react-hook-form";
import {
QuestionBloomTaxonomy,
QuestionCheckType,
QuestionDifficulty,
} from "../../../../__generated__/graphql-schema";
import { CHECK_TYPE, BLOOM_TAXONOMY, DIFFICULTY } from "../../../../utils/types";
import { Button, Modal } from "../../../../components";
import { useFiltersProvider } from "./QuestionsFilterProvider";
import { QuestionsSubjectFilter } from './QuestionsSubjectFilter'
import { QuestionsAuthorshipTypeFilter } from "./QuestionsAuthorshipTypeFilter";
import { AuthorshipFilter } from "./AuthorshipFilter";
type FilterGroupProps = {
title: string;
register: any;
options: {
value: string;
label: string;
}[];
selecteds: any[];
setChanged: Dispatch<SetStateAction<boolean>>;
};
const FilterGroup: FC<FilterGroupProps> = ({
title,
options,
register,
selecteds,
setChanged,
}) => (
<>
<div className="mt-2 sm:mt-0 flex flex-col">
<h3 className="font-bold mb-1">{title}</h3>
<div
className="grid grid-cols-2 sm:flex sm:flex-col"
key={`filter-group-${title}`}
>
{options.map(({ value, label }) => (
<span className="mr-1 mb-2 sm:mb-0 sm:mr-0" key={value}>
<input
type="checkbox"
name={value}
ref={register}
id={value}
defaultChecked={selecteds.includes(value)}
onClick={() => setChanged(true)}
/>
<label htmlFor={value} className="ml-2">
{label}
</label>
</span>
))}
</div>
</div>
</>
);
type Props = {
isOpen: boolean;
setIsOpen: (state: boolean) => void;
};
export const QuestionsFilter: FC<Props> = ({ isOpen, setIsOpen }) => {
const { handleSubmit, register, reset } = useForm();
const { where, setWhere } = useFiltersProvider();
const { difficulty, checkType, bloomTaxonomy } = where;
const [changed, setChanged] = useState(false);
const submitRef = useRef<HTMLInputElement>()
const onSubmit = (inputs: any) => {
const valuesFromCheckType = CHECK_TYPE.filter(
({ value }) => inputs[value]
).map(({ value }) => value) as QuestionCheckType[];
const valuesFromBloomTaxonomy = BLOOM_TAXONOMY.filter(
({ value }) => inputs[value]
).map(({ value }) => value) as QuestionBloomTaxonomy[];
const valuesFromDifficulty = DIFFICULTY.filter(
({ value }) => inputs[value]
).map(({ value }) => value) as QuestionDifficulty[];
const removeKeysWithUndefiend = (obj: any) => {
for (var propName in obj) {
if (obj[propName] === null || obj[propName] === undefined) {
delete obj[propName];
}
}
return obj;
};
setWhere({
unifesoAuthorship: inputs.authorship === 'null' ? null : inputs.authorship === 'true',
...removeKeysWithUndefiend({
checkType: valuesFromCheckType.length ? valuesFromCheckType : undefined,
bloomTaxonomy: valuesFromBloomTaxonomy.length
? valuesFromBloomTaxonomy
: undefined,
difficulty: valuesFromDifficulty.length
? valuesFromDifficulty
: undefined,
subjectId: inputs.subjectId === "" ? undefined : inputs.subjectId,
authorshipYear: inputs.authorshipYear === "" ? undefined : [inputs.authorshipYear],
}),
});
setChanged(false);
setIsOpen(false);
};
const handleClean = () => {
setChanged(false);
setWhere({});
reset();
};
return (
<Modal
title="Filtros"
setIsOpen={setIsOpen}
isOpen={isOpen}
buttons={
<>
<Button
onClick={() => handleClean()}
disabled={!changed}
className={`${changed ? 'opacity-1' : 'opacity-0'}`}
>
Limpar
</Button>
<Button onClick={() => setIsOpen(false)}>
Cancelar
</Button>
<Button type="primary" onClick={() => submitRef.current?.click()}>
Aplicar
</Button>
</>
}
>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="grid grid-cols-1 gap-4 sm:gap-8 lg:grid-cols-2">
<div className="mt-2 sm:mt-0 flex flex-col">
<h3 className="font-bold mb-1">Assunto</h3>
<div className="grid grid-cols-2 sm:flex sm:flex-col">
<QuestionsSubjectFilter register={register} setChanged={setChanged} />
</div>
</div>
<div className="mt-2 sm:mt-0 flex flex-col">
<h3 className="font-bold mb-1">Ano</h3>
<div className="grid grid-cols-2 sm:flex sm:flex-col">
<QuestionsAuthorshipTypeFilter register={register} setChanged={setChanged} />
</div>
</div>
<FilterGroup
title="Tipo"
register={register}
options={CHECK_TYPE}
selecteds={(checkType ?? []) as QuestionCheckType[]}
setChanged={setChanged}
/>
<FilterGroup
title="Habilidade Cognitiva"
register={register}
options={BLOOM_TAXONOMY}
selecteds={(bloomTaxonomy ?? []) as QuestionBloomTaxonomy[]}
setChanged={setChanged}
/>
<FilterGroup
title="Grau de Dificuldade"
register={register}
options={DIFFICULTY}
selecteds={(difficulty ?? []) as QuestionDifficulty[]}
setChanged={setChanged}
/>
<AuthorshipFilter register={register} setChanged={setChanged} />
<input hidden type="submit" ref={submitRef as any} />
</div>
</form>
</Modal >
);
};

View File

@@ -0,0 +1,43 @@
import React, {
createContext,
useState,
useMemo,
FC,
useContext,
Dispatch,
SetStateAction,
} from 'react'
import { QuestionWhereInput } from '../../../../__generated__/graphql-schema'
type ProviderValue = {
where: QuestionWhereInput
setWhere: Dispatch<SetStateAction<QuestionWhereInput>>
}
const FiltersContext = createContext<ProviderValue | null>(null)
export const useFiltersProvider = () => {
const context = useContext(FiltersContext)
if (context === null) {
throw new Error('You probably forgot to put <FiltersProvider>.')
}
return context
}
export const FiltersProvider: FC = ({ children }) => {
const [where, setWhere] = useState<QuestionWhereInput>({})
const providerValue = useMemo(() => ({ where, setWhere }), [
where,
setWhere,
])
return (
<FiltersContext.Provider value={providerValue}>
{children}
</FiltersContext.Provider>
)
}

View File

@@ -0,0 +1,52 @@
import React, { Dispatch, FC, SetStateAction } from 'react'
import { gql, useQuery } from '@apollo/client'
import { Query } from '../../../../__generated__/graphql-schema'
import { useFiltersProvider } from './QuestionsFilterProvider'
const SUBJECTS_QUERY = gql`
query {
subjects {
nodes {
id
name
}
}
}
`
type Props = {
register: any
setChanged: Dispatch<SetStateAction<boolean>>
}
export const QuestionsSubjectFilter: FC<Props> = ({ register, setChanged }) => {
const { where } = useFiltersProvider();
const { loading, data } = useQuery<Query>(SUBJECTS_QUERY)
if (loading) return null
const subjects = data?.subjects.nodes
return (
<div>
<select
ref={register}
className="w-full rounded p-1 border-gray-400 border shadow-sm"
name="subjectId"
defaultValue={where.subjectId ?? ""}
onClick={() => setChanged(true)}
>
<option value="" />
{subjects?.map((subject) => (
<option
key={`${subject?.name}-${subject?.id}`}
value={subject?.id}
>
{subject?.name}
</option>
))}
</select>
</div>
)
}

View File

@@ -0,0 +1,2 @@
export * from "./QuestionsFilter";
export * from "./QuestionsFilterProvider";

View File

@@ -0,0 +1,131 @@
import React, { FC, useState } from 'react'
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa';
import { MdModeEdit } from 'react-icons/md';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { Question, QuestionStatus } from '../../../__generated__/graphql-schema'
import { useCurrentUser } from '../../../contexts';
import { NodeId } from '../../../utils/graphql';
import { gql } from '@apollo/client';
const EditIcon = styled(MdModeEdit)`
margin: auto;
font-size: 1.5rem;
`;
type Props = {
questions: Question[]
title: string
pagination?: {
onNextPageClick: () => void
hasNextPage: boolean
hasPreviousPage: boolean
onPreviousPageClick: () => void
}
}
export const QuestionsListFragments = gql`
fragment QuestionFields on Question {
id
status
user {
id
}
updatedAt
}
`
export const QuestionsList: FC<Props> = ({ questions, title, pagination }) => {
const { user } = useCurrentUser()
const [pageCount, setPageCount] = useState(1)
const formatDate = (stringDate: string) => new Date(stringDate).toLocaleDateString()
const handleOnNextPageClick = () => {
if (pagination?.hasNextPage) {
pagination.onNextPageClick()
setPageCount(pageCount + 1)
}
}
const handleOnPreviousPageClick = () => {
if (pagination?.hasPreviousPage) {
pagination.onPreviousPageClick()
setPageCount(pageCount - 1)
}
}
return (
<div className="bg-gray-200 p-4 rounded my-2">
<div className="flex">
<h2 className="text-gray-500 font-medium text-xl">{title}</h2>
<div className="ml-auto text-sm sm:text-base text-gray-700">
<button
className="p-2"
onClick={handleOnPreviousPageClick}
style={{ visibility: (pagination?.hasPreviousPage ? 'visible' : 'hidden') }}
>
<FaArrowLeft />
</button>
Página: {pageCount}
<button
className="p-2"
onClick={handleOnNextPageClick}
style={{ visibility: (pagination?.hasNextPage ? 'visible' : 'hidden') }}
>
<FaArrowRight />
</button>
</div>
</div>
<hr className="border-t border-gray-400 m-px" />
<div className="p-2 text-sm">
{questions.length
? <div className="flex-col w-full sm:grid gap-4 sm:col-gap-8 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{questions.map((question) => (
<div
key={`question-${question.id}`}
className="mx-1 sm:mx-0 mb-4 sm:mb-0 border-l-8 border-primary-light flex bg-white hover:bg-unifeso-50 rounded shadow hover:shadow-md cursor-pointer group transition-all duration-500"
>
<Link
className="flex flex-col w-full px-3 py-2"
to={`/questions/${question.id}/${(question.user.id === user?.id ? '' : 'review')}`}
>
<h2>
{`# ${NodeId.decode(question.id).id}`}
</h2>
<div className="text-sm text-gray-700 flex flex-col flex-wrap justify-between">
<span>
Atualizada em:
{" "}
{formatDate(question.updatedAt)}
</span>
</div>
</Link>
{(question.user.id === user?.id && question.status !== QuestionStatus.Registered) &&
<div
className="flex flex-col relative flex-grow justify-center"
>
<Link
className="group-hover:block absolute bg-gray-300 hover:bg-primary-normal text-gray-500 hover:text-gray-100 hover:shadow-lg rounded-full p-2 cursor-pointer shadow-inner transition-all duration-500"
style={{ left: '-1.5rem' }}
to={`/questions/${question.id}/edit`}
>
<EditIcon />
</Link>
</div>
}
</div>
))}
</div>
: <div className="grid text-gray-800" style={{ placeItems: 'center' }}>
<div className="text-center">
<span className="text-sm sm:text-base">
Nenhuma questão.
</span>
</div>
</div>
}
</div>
</div>
)
}

View File

@@ -0,0 +1,22 @@
import React, { FC } from 'react'
import { QuestionStatus } from '../../../__generated__/graphql-schema'
import { useFiltersProvider } from './QuestionFilter/QuestionsFilterProvider'
import { QuestionsQuery } from './QuestionsQuery'
import { QuestionsRevisedQuery } from './QuestionsRevisedQuery'
import { QuestionsWaitingReviewQuery } from './QuestionsWaitingReviewQuery'
export const QuestionsPainel: FC = () => {
const { where } = useFiltersProvider()
return (
<>
<QuestionsWaitingReviewQuery title="Aguardando seu Parecer" />
<QuestionsQuery title="Aguardando Parecer do Revisor" where={where} status={QuestionStatus.WaitingReview} />
<QuestionsQuery title="Pendentes de Alterações" where={where} status={QuestionStatus.WithRequestedChanges} />
<QuestionsQuery title="Rascunhos" where={where} status={QuestionStatus.Draft} />
<QuestionsQuery title="Aprovadas" where={where} status={QuestionStatus.Approved} />
<QuestionsQuery title="Registradas" where={where} status={QuestionStatus.Registered} />
<QuestionsRevisedQuery title="Revisadas por Você" />
</>
)
}

View File

@@ -0,0 +1,95 @@
import React, { FC, useState } from 'react'
import { PageInfo, Query, Question, QuestionWhereInput, QuestionStatus } from '../../../__generated__/graphql-schema';
import { gql, useQuery } from '@apollo/client';
import { QuestionsList, QuestionsListFragments } from './QuestionsList'
import { useCurrentUser } from '../../../contexts';
const QUESTIONS_QUERY = gql`
${QuestionsListFragments}
query QuestionsQuery($first: Int!, $after: String, $before: String, $where: QuestionWhereInput) {
questions (
first: $first,
after: $after,
before: $before,
where: $where
) {
nodes {
... QuestionFields
}
}
}
`
const PAGE_SIZE = 4
type Props = {
title: string
where?: QuestionWhereInput
status?: QuestionStatus
}
export const QuestionsQuery: FC<Props> = ({ title, where, status }) => {
const { user } = useCurrentUser()
const [questions, setQuestions] = useState<Question[]>([])
const [pageInfo, setPageInfo] = useState<PageInfo | undefined>()
const updateQuestions = (queryResult: Query) => {
const { questions: questionConnection } = queryResult
setQuestions(questionConnection.nodes as Question[])
setPageInfo(questionConnection.pageInfo)
}
const whereInput = {
status,
userId: user?.id,
...where
}
const { fetchMore } = useQuery<Query>(QUESTIONS_QUERY, {
onCompleted: (response) => {
updateQuestions(response)
},
variables: {
first: PAGE_SIZE,
where: whereInput,
},
fetchPolicy: "network-only",
})
const onNextPageClick = () => {
fetchMore({
variables: {
first: PAGE_SIZE,
after: pageInfo?.endCursor,
},
}).then(({ data }) => {
updateQuestions(data)
})
}
const onPreviousPageClick = () => {
fetchMore({
variables: {
first: PAGE_SIZE,
before: pageInfo?.startCursor,
},
}).then(({ data }) => {
updateQuestions(data)
})
}
return (
<QuestionsList
title={title}
questions={questions}
pagination={{
onNextPageClick: onNextPageClick,
hasNextPage: pageInfo?.hasNextPage ?? false,
hasPreviousPage: pageInfo?.hasPreviousPage ?? false,
onPreviousPageClick: onPreviousPageClick
}}
/>
)
};

View File

@@ -0,0 +1,89 @@
import React, { FC, useState } from 'react'
import { PageInfo, Query, Question, ReviewRequest, User } from '../../../__generated__/graphql-schema';
import { gql, useQuery } from '@apollo/client';
import { QuestionsList, QuestionsListFragments } from './QuestionsList'
const QUESTIONS_QUERY = gql`
${QuestionsListFragments}
query QuestionsRevisedQuery($first: Int!, $after: String) {
currentUser {
id
inactiveReviewRequests(
first: $first,
after: $after
) {
nodes {
id
question {
... QuestionFields
}
}
}
}
}
`
const PAGE_SIZE = 4
type Props = {
title: string
}
export const QuestionsRevisedQuery: FC<Props> = ({ title }) => {
const [questions, setQuestions] = useState<Question[]>([])
const [pageInfo, setPageInfo] = useState<PageInfo | undefined>()
const updateQuestions = (queryResult: Query) => {
const { currentUser } = queryResult
const { inactiveReviewRequests } = currentUser as User
const reviewRequests = inactiveReviewRequests.nodes as ReviewRequest[]
setQuestions(reviewRequests.map(item => item.question))
setPageInfo(inactiveReviewRequests.pageInfo)
}
const { fetchMore } = useQuery<Query>(QUESTIONS_QUERY, {
onCompleted: (response) => {
updateQuestions(response)
},
variables: {
first: PAGE_SIZE,
},
fetchPolicy: "network-only"
})
const onNextPageClick = () => {
fetchMore({
variables: {
first: PAGE_SIZE,
after: pageInfo?.endCursor,
},
}).then(({ data }) => {
updateQuestions(data)
})
}
const onPreviousPageClick = () => {
fetchMore({
variables: {
first: PAGE_SIZE,
before: pageInfo?.startCursor,
},
}).then(({ data }) => {
updateQuestions(data)
})
}
return (
<QuestionsList
title={title}
questions={questions}
pagination={{
onNextPageClick: onNextPageClick,
hasNextPage: pageInfo?.hasNextPage ?? false,
hasPreviousPage: pageInfo?.hasPreviousPage ?? false,
onPreviousPageClick: onPreviousPageClick
}}
/>
)
};

View File

@@ -0,0 +1,89 @@
import React, { FC, useState } from 'react'
import { PageInfo, Query, Question, ReviewRequest, User } from '../../../__generated__/graphql-schema';
import { gql, useQuery } from '@apollo/client';
import { QuestionsList, QuestionsListFragments } from './QuestionsList'
const QUESTIONS_QUERY = gql`
${QuestionsListFragments}
query QuestionsWaitingReviewQuery($first: Int!, $after: String) {
currentUser {
id
activeReviewRequests(
first: $first,
after: $after
) {
nodes {
id
question {
... QuestionFields
}
}
}
}
}
`
const PAGE_SIZE = 4
type Props = {
title: string
}
export const QuestionsWaitingReviewQuery: FC<Props> = ({ title }) => {
const [questions, setQuestions] = useState<Question[]>([])
const [pageInfo, setPageInfo] = useState<PageInfo | undefined>()
const updateQuestions = (queryResult: Query) => {
const { currentUser } = queryResult
const { activeReviewRequests } = currentUser as User
const reviewRequests = activeReviewRequests.nodes as ReviewRequest[]
setQuestions(reviewRequests.map(item => item.question))
setPageInfo(activeReviewRequests.pageInfo)
}
const { fetchMore } = useQuery<Query>(QUESTIONS_QUERY, {
onCompleted: (response) => {
updateQuestions(response)
},
variables: {
first: PAGE_SIZE,
},
fetchPolicy: "network-only"
})
const onNextPageClick = () => {
fetchMore({
variables: {
first: PAGE_SIZE,
after: pageInfo?.endCursor,
},
}).then(({ data }) => {
updateQuestions(data)
})
}
const onPreviousPageClick = () => {
fetchMore({
variables: {
first: PAGE_SIZE,
before: pageInfo?.startCursor,
},
}).then(({ data }) => {
updateQuestions(data)
})
}
return (
<QuestionsList
title={title}
questions={questions}
pagination={{
onNextPageClick: onNextPageClick,
hasNextPage: pageInfo?.hasNextPage ?? false,
hasPreviousPage: pageInfo?.hasPreviousPage ?? false,
onPreviousPageClick: onPreviousPageClick
}}
/>
)
};

View File

@@ -0,0 +1 @@
export { List } from "./List";

View File

@@ -0,0 +1,87 @@
import React, {useState} from "react";
import {useHistory} from "react-router";
import {gql, useMutation} from "@apollo/client";
import {AlertV2Props, Navigator} from "../../../components";
import {Form} from '../Form'
import {Mutation} from "../../../__generated__/graphql-schema";
import {QuestionRoutePaths} from "../../../routes";
const CREATE_QUESTION_MUTATION = gql`
mutation($input: CreateQuestionInput!) {
createQuestion(input: $input) {
question {
id
}
}
}
`
export const New = () => {
const history = useHistory()
const [alert, setAlert] = useState<AlertV2Props>();
const [createQuestion] = useMutation<Mutation>(CREATE_QUESTION_MUTATION)
const onSubmit = (inputs: any) => {
createQuestion({
variables: {
input: {
question: inputs,
},
},
}).then(() => {
history.push(QuestionRoutePaths.index)
}).catch((error: string) => {
setAlert({
severity: "error",
text: `Erro ao criar questão. ${error}. Por favor, tente novamente.`,
});
setTimeout(
() => setAlert({severity: "error", text: ""}),
8000
);
})
}
const onDraftSubmit = (inputs: any) => {
createQuestion({
variables: {
input: {
question: inputs,
},
}
}).then(({data}) => {
setAlert({
severity: "success",
text: "Rascunho criado com sucesso",
});
setTimeout(() => {
const id = data?.createQuestion?.question?.id
history.push(QuestionRoutePaths.edit.replace(':id', id ?? ''))
}, 3000);
}).catch((error: string) => {
setAlert({
severity: "error",
text: `Erro ao criar rascunho. ${error}`,
});
setTimeout(
() => setAlert(undefined),
8000
);
})
}
return (
<>
<Navigator home={true}/>
<div className="bg-gray-100 w-full my-2">
<main>
<Form onSubmit={onSubmit} onDraftSubmit={onDraftSubmit} alert={alert}/>
</main>
</div>
</>
)
};

View File

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

View File

@@ -0,0 +1,53 @@
import React, { FC } from "react";
import { useParams } from "react-router-dom";
import { gql, useQuery } from "@apollo/client";
import { ViewMode, ViewModeFragments, ReviewMessages, ReviewMessagesFragments } from "../shared";
import { Navigator } from "../../../components";
import { Query, Question } from "../../../__generated__/graphql-schema";
export const GET_QUESTION = gql`
${ViewModeFragments}
${ReviewMessagesFragments}
query Question($id: ID!) {
node(id: $id) {
__typename
... on Question {
id
...QuestionReadOnlyFields
...ReviewMessages_question
}
}
}
`
export const Review: FC = () => {
const { id } = useParams<{ id: string }>()
const { loading, data, refetch } = useQuery<Query>(GET_QUESTION, {
variables: {
id,
},
fetchPolicy: "network-only"
})
const question = data?.node as Question | null
if (loading || !question) return null;
return (
<>
<Navigator home />
<div className="bg-gray-100 h-full w-full my-2">
<main className="flex px-5 max-w-screen-xl m-auto">
<div className="w-3/5">
<ViewMode questionData={question} />
</div>
<div className="w-2/5 ml-3">
<div className="my-3" />
<ReviewMessages question={question} refetch={refetch}/>
</div>
</main>
</div>
</>
);
};

View File

@@ -0,0 +1 @@
export { Review } from "./Review";

View File

@@ -0,0 +1,188 @@
import React, {FC, useState} from "react";
import {useParams, useHistory} from "react-router-dom";
import {MdDeleteForever, MdEdit, MdSave} from "react-icons/md";
import {useQuery, useMutation, gql} from "@apollo/client";
import {ViewMode, ViewModeFragments, ReviewMessages, ReviewMessagesFragments} from "../shared";
import {Navigator, Dialog} from "../../../components";
import {Mutation, Query, Question, QuestionStatus} from "../../../__generated__/graphql-schema";
import {AlertV2Props, AlertV2} from "../../../components/AlertV2";
import {NodeId} from "../../../utils/graphql";
import {QuestionRoutePaths} from "../../../routes";
export const GET_QUESTION = gql`
${ViewModeFragments}
${ReviewMessagesFragments}
query Question($id: ID!) {
node(id: $id) {
__typename
... on Question {
id
...QuestionReadOnlyFields
...ReviewMessages_question
}
}
}
`
const FINISH_QUESTION = gql`
mutation FinishQuestion($id: ID!) {
finishQuestion (
input: {
questionId: $id
}
) {
question {
id
status
}
errors
}
}
`
const DESTROY_QUESTION = gql`
mutation DestroyQuestion($id: ID!) {
destroyQuestion(
input: {
questionId: $id
}
) {
deletedQuestionId
errors
}
}
`
export const Show: FC = () => {
const history = useHistory();
const {id} = useParams<{ id: string }>();
const [confirmRegister, setConfirmRegister] = useState(false)
const [confirmDestroy, setConfirmDestroy] = useState(false)
const [alert, setAlert] = useState<AlertV2Props>()
const [finishQuestion] = useMutation<Mutation>(FINISH_QUESTION)
const [destroyQuestion] = useMutation<Mutation>(DESTROY_QUESTION)
const {loading, data, refetch} = useQuery<Query>(GET_QUESTION, {
variables: {
id,
},
fetchPolicy: "network-only",
});
const question = data?.node as Question | null
if (loading || !question) return null;
const recordId = NodeId.decode(question.id).id
const confirmEditQuestion = () => {
history.push(`/questions/${id}/edit`);
};
const handleEditQuestion = () => {
confirmEditQuestion()
};
const handleRegisterQuestion = async () => {
try {
await finishQuestion({variables: {id: recordId}})
setAlert({
text: 'Questão registrada com sucesso!',
severity: 'success',
})
} catch(error){
setAlert({
text: 'Algo inesperado aconteceu ao tentar registrar a questão.',
severity: 'error',
})
}
setConfirmRegister(false)
};
const handleDestroyQuestion = async () => {
const {data: questionDestroyData } = await destroyQuestion({variables: {id: recordId}})
if (questionDestroyData?.destroyQuestion?.deletedQuestionId) {
history.push(QuestionRoutePaths.index)
} else {
setAlert({
text: 'Algo inesperado aconteceu ao tentar excluir a questão.',
severity: 'error',
})
setConfirmDestroy(false)
}
};
const ACTIONS = {
edit: {
icon: <MdEdit className="my-auto"/>,
label: "Editar",
action: handleEditQuestion,
},
register: {
icon: <MdSave className="my-auto"/>,
label: "Registrar",
action: () => setConfirmRegister(true),
},
destroy: {
icon: <MdDeleteForever className="my-auto"/>,
label: 'Excluir',
action: () => setConfirmDestroy(true),
}
}
const options = (() => {
switch (question.status) {
case QuestionStatus.Registered:
return ([]);
case QuestionStatus.Approved:
return ([ACTIONS.edit, ACTIONS.register, ACTIONS.destroy])
default:
return ([ACTIONS.edit, ACTIONS.destroy])
}
})()
return (
<>
<Dialog
isOpen={confirmDestroy}
setIsOpen={(value) => setConfirmDestroy(value)}
title="Confirmação de Exclusão"
text="Após a exclusão, a questão não poderá ser recuperada. Deseja continuar?"
onConfirmation={handleDestroyQuestion}
/>
<Dialog
isOpen={confirmRegister}
setIsOpen={(value) => setConfirmRegister(value)}
title="Confirmação de Registro"
text="Após o registro, a questão estará disponível para uso e não poderá mais ser editada ou excluída. Deseja continuar?"
onConfirmation={handleRegisterQuestion}
/>
<Navigator home>
{options.map((option, index) => (
<div key={`navigation-item-${index}`} className={`hover:text-white ${index === 0 ? "ml-auto" : ""}`}>
<button onClick={option.action} className="flex pl-4">
{option.icon}
<span className="pl-2">{option.label}</span>
</button>
</div>
))}
</Navigator>
<div className="bg-gray-100 w-full my-2">
<main className="max-w-screen-xl m-auto">
<div className="flex">
{alert && <AlertV2 severity={alert.severity} text={alert.text}/>}
</div>
<div className="flex px-5">
<div className="w-3/5">
<ViewMode questionData={question}/>
</div>
<div className="w-2/5 ml-3">
<ReviewMessages question={question} refetch={refetch}/>
</div>
</div>
</main>
</div>
</>
);
};

View File

@@ -0,0 +1 @@
export { Show } from "./Show";

View File

@@ -0,0 +1,15 @@
import { Question } from "../../__generated__/graphql-schema";
export const formatInput = (inputs: any) =>
({
...inputs,
bloomTaxonomy:
inputs.bloomTaxonomy === "" ? undefined : inputs.bloomTaxonomy,
difficulty: inputs.difficulty === "" ? undefined : inputs.difficulty,
checkType: inputs.checkType === "" ? undefined : inputs.checkType,
subjectId: inputs.subjectId === "" ? undefined : inputs.subjectId,
reviewerUserId:
inputs.reviewerUserId === "" ? undefined : inputs.reviewerUserId,
alternatives: inputs.alternatives,
__nonused: undefined,
} as Question);

View File

@@ -0,0 +1,5 @@
export * from "./New";
export * from "./Show";
export * from "./Review";
export * from "./Edit";
export * from "./List";

View File

@@ -0,0 +1,77 @@
import { ApolloQueryResult, gql, OperationVariables } from "@apollo/client";
import {
CheckCircleIcon,
DocumentRemoveIcon
} from '@heroicons/react/outline';
import React, { FC } from "react";
import { Card } from "../../../../components";
import { Query, Question, ReviewMessage, ReviewMessageFeedbackType } from "../../../../__generated__/graphql-schema";
import { ReviewMessageForm, ReviewMessageFormFragments } from "./ReviewMessagesForm";
const feedbackIcon = {
[ReviewMessageFeedbackType.Answer]: null,
[ReviewMessageFeedbackType.Approve]: <CheckCircleIcon className="w-5 text-green-800" />,
[ReviewMessageFeedbackType.RequestChanges]: <DocumentRemoveIcon className="w-5 text-red-800" />,
};
const ReviewMessageTitle: FC<{
feedback: ReviewMessage
}> = ({ feedback }) => (
<p className="flex">
{feedback.user.name}{' '} - {' '}
<span className="text-gray-700 pr-2">
{new Date(feedback.createdAt).toLocaleString()}
</span>
{feedbackIcon[feedback.feedbackType]}
</p>
)
export const ReviewMessagesFragments = gql`
${ReviewMessageFormFragments}
fragment ReviewMessages_question on Question {
id
...ReviewMessageForm_question
user {
id
}
reviewMessages {
nodes {
id
feedbackType
text
user {
name
avatarUrl
}
createdAt
}
}
}
`
export const ReviewMessages: FC<{
question: Question
refetch: (variables?: Partial<OperationVariables> | undefined) => Promise<ApolloQueryResult<Query>>
}> = ({ question, refetch }) => {
const reviewMessages = question.reviewMessages.nodes
const hasFeebacks = !!reviewMessages.length
return (
<div>
<Card className="mb-3" title="Histórico de Pareceres">
{hasFeebacks
? reviewMessages.map((item) => (
<div key={item.id}>
<ReviewMessageTitle feedback={item} />
<p className="p-2">
{item.text}
</p>
</div>
))
: 'Essa questão não tem nenhum parecer ainda.'}
</Card>
<ReviewMessageForm question={question} refetch={refetch} />
</div>
)
};

View File

@@ -0,0 +1,139 @@
import { ApolloQueryResult, gql, OperationVariables, useMutation } from "@apollo/client";
import React, { FC, useState } from "react";
import { useForm } from "react-hook-form";
import { Prompt, useHistory } from "react-router";
import { Button, Card } from "../../../../components";
import { useCurrentUser } from "../../../../contexts";
import { NodeId } from "../../../../utils/graphql";
import { Mutation, Query, Question, ReviewMessageFeedbackType } from "../../../../__generated__/graphql-schema";
export const REVIEW_FEEDBACK = [
{
label: "Aprovada",
description: "O revisor sugere que as observações enviadas no parecer sejam consideradas.",
value: ReviewMessageFeedbackType.Approve,
},
{
label: "Pendente de Alterações",
description: "O autor deve efetuar as alterações solicitadas no parecer e reenviar a questão ao revisor.",
value: ReviewMessageFeedbackType.RequestChanges,
},
];
export const ReviewMessageFormFragments = gql`
fragment ReviewMessageForm_question on Question {
id
status
user {
id
}
}
`
const CREATE_REVIEW_MESSAGE_MUTATION = gql`
mutation($questionId: ID!, $feedbackType: ReviewMessageFeedbackType!, $text: String!) {
createReviewMessage(
input: {
message: {
questionId: $questionId
feedbackType: $feedbackType
text: $text
}
}
) {
reviewMessage {
id
}
}
}
`
export const ReviewMessageForm: FC<{
question: Question
refetch: (variables?: Partial<OperationVariables> | undefined) => Promise<ApolloQueryResult<Query>>
}> = ({ question, refetch }) => {
const [isChangesSaved, setIsChangesSaved] = useState(true)
const { register, handleSubmit } = useForm()
const history = useHistory();
const { user } = useCurrentUser()
const [createReviewMessage] = useMutation<Mutation['createReviewMessage']>(CREATE_REVIEW_MESSAGE_MUTATION)
const hasFeebacks = !!question.reviewMessages.nodes.length
const questionIsFromCurrentUser = user?.id === question.user.id
const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
if (e.target.value !== '') {
setIsChangesSaved(false)
} else {
setIsChangesSaved(true)
}
}
const handleSubmitClick = () => {
setIsChangesSaved(true)
}
const formSubmit = async (inputs: {
feedbackType: ReviewMessageFeedbackType
text: string
}) => {
await createReviewMessage({
variables: {
text: inputs.text,
feedbackType: questionIsFromCurrentUser ? ReviewMessageFeedbackType.Answer : inputs.feedbackType,
questionId: NodeId.decode(question.id).id,
},
});
await refetch()
history.push('/questions')
};
if (!hasFeebacks && questionIsFromCurrentUser) return null
return (
<>
<Prompt
when={!isChangesSaved}
message='O parecer ainda não foi enviado, deseja continuar?'
/>
<Card title="Parecer" className="max-w-screen-md mx-auto">
<form onSubmit={handleSubmit(formSubmit)}>
<textarea
onChange={(e) => handleTextChange(e)}
className="w-full h-32 p-2 border-solid border-2 border-gray-700 rounded-md"
ref={register}
name="text"
/>
{!questionIsFromCurrentUser && REVIEW_FEEDBACK.map((item, index) => (
<div key={index} className="flex mb-2">
<input
type="radio"
id={item.value}
name="feedbackType"
ref={register({ required: true })}
value={item.value}
className="my-auto"
defaultChecked={index === 0}
/>
<label
htmlFor={item.value}
className="flex flex-col pl-2 w-full"
>
{item.label}
<p className="text-gray-700 text-sm">{item.description}</p>
</label>
</div>
))}
<div className="justify-end flex">
<Button type="primary" htmlType="submit" className="mt-4" onClick={handleSubmitClick}>
{questionIsFromCurrentUser ? 'Responder Parecer' : 'Enviar Parecer'}
</Button>
</div>
</form>
</Card>
</>
)
};

View File

@@ -0,0 +1 @@
export * from './ReviewMessages'

View File

@@ -0,0 +1,171 @@
import React, { FC } from "react";
import { gql } from "@apollo/client";
import { Card } from "../../../components";
import { Question } from "../../../__generated__/graphql-schema";
import { loadWIRISplugin } from "../../../utils/plugins";
import { BLOOM_TAXONOMY, DIFFICULTY } from "../../../utils/types";
export const ViewModeFragments = gql`
fragment QuestionReadOnlyFields on Question {
intention
instruction
support
body
alternatives {
correct
text
}
explanation
references
authorship
authorshipYear
difficulty
checkType
bloomTaxonomy
subject {
name
axis {
name
}
category {
name
}
}
status
reviewer {
id
name
}
updatedAt
}
`
type Props = {
questionData?: Question
}
export const ViewMode: FC<Props> = ({ questionData: question }) => {
if (!question) return null;
const { alternatives } = question;
const correctAlternative = alternatives?.find(
(alternative) => alternative.correct === true,
);
const incorrectAnswers = alternatives?.filter(
(alternative) => alternative.correct === false,
);
function formatDate(stringDate: string) {
return new Date(stringDate).toLocaleDateString();
}
const { instruction, support, body } = question;
const difficulty = DIFFICULTY.find((item) => question.difficulty === item.value)?.label
const bloomTaxonomy = BLOOM_TAXONOMY.find((item) => question.bloomTaxonomy === item.value)?.label
loadWIRISplugin()
return (
<div className="max-w-screen-lg">
<Card className="mb-3" title="Características">
<div className="grid grid-cols-2">
<div>
<span className="text-gray-700">Grau de Dificuldade: </span>
{difficulty ?? ''}
</div>
<div>
<span className="text-gray-700">Habilidade Cognitiva: </span>
{bloomTaxonomy ?? ''}
</div>
<div>
<span className="text-gray-700">Ano: </span>
{question.authorshipYear}
</div>
<div>
<span className="text-gray-700">Autoria: </span>
{question.authorship === "UNIFESO" ? "Própria" : `Terceiro - ${question.authorship}`}
</div>
<div>
<span className="text-gray-700">Atualizada em: </span>
{formatDate(question.updatedAt)}
</div>
<div>
<span className="text-gray-700">Assunto: </span>
{question.subject?.name}
</div>
<div>
<span className="text-gray-700">Categoria: </span>
{question.subject?.category?.name}
</div>
<div>
<span className="text-gray-700">Revisor: </span>
{question.reviewer?.name}
</div>
<div>
<span className="text-gray-700">Eixo de Formação: </span>
{question.subject?.axis?.name}
</div>
</div>
</Card>
{!!question.intention?.length && (
<Card className="mb-3" title="Intenção">
<div className="ck-content" dangerouslySetInnerHTML={{ __html: question.intention }} />
</Card>
)}
{instruction && (
<Card className="mb-3" title="Instrução">
<div className="ck-content" dangerouslySetInnerHTML={{ __html: instruction }} />
</Card>
)}
{support && (
<Card className="mb-3" title="Suporte">
<div className="ck-content" dangerouslySetInnerHTML={{ __html: support }} />
</Card>
)}
{body && (
<Card className="mb-3" title="Enunciado">
<div className="ck-content" dangerouslySetInnerHTML={{ __html: body }} />
</Card>
)}
<Card className="mb-3" title="Resposta Correta">
<div className="ck-content" dangerouslySetInnerHTML={{ __html: correctAlternative?.text ?? '' }} />
<div className="flex flex-col w-full border border-gray-300 rounded p-4 mt-4 shadow-sm">
<div>
<h2 className="text-base font-medium mb-3">Explicação</h2>
<div
className="ck-content ml-2"
dangerouslySetInnerHTML={{ __html: question.explanation ?? '' }}
/>
</div>
<div className="bg-gray-400 w-full my-3" style={{ height: "1px" }} />
<div>
<h2 className="text-base font-medium mb-3">Referências</h2>
<div
className="ck-content ml-2"
dangerouslySetInnerHTML={{ __html: question.references ?? '' }}
/>
</div>
</div>
</Card>
<Card className="mb-3" title="Distratores">
{incorrectAnswers?.map(({ text }, index) => (
<div key={`question-alternative-${index}`}>
{index !== 0 && (
<div
className="bg-gray-400 w-full my-3"
style={{ height: "1px" }}
/>
)}
<div className="ck-content" dangerouslySetInnerHTML={{ __html: text ?? '' }} />
</div>
))}
</Card>
</div>
);
};

View File

@@ -0,0 +1,2 @@
export * from "./ViewMode";
export * from "./ReviewMessages";

View File

@@ -0,0 +1,39 @@
import React, { useState } from "react";
import { useCurrentUser } from "../../contexts";
import { AvatarEditor, Navigator, CurrentUserAvatar } from "../../components";
export const Profile = () => {
const [avatarEditorIsOpen, setAvatarEditorIsOpen] = useState(false);
const { user } = useCurrentUser();
return (
<>
<AvatarEditor
isOpen={avatarEditorIsOpen}
setIsOpen={setAvatarEditorIsOpen}
/>
<Navigator home />
<div className="bg-gray-100 w-full my-3">
<main>
<div className="flex items-center flex-col max-w-4xl m-auto">
<div className="bg-white shadow border border-gray-100 flex flex-col items-center rounded p-4 w-full mt-12 mb-4 relative">
<div
className="w-20 absolute cursor-pointer"
style={{ top: "-3.10rem" }}
onClick={() => setAvatarEditorIsOpen(true)}
>
<CurrentUserAvatar />
</div>
<div className="mt-8 text-center">
<h2 className="font-bold">{user?.name || user?.email}</h2>
<h2 className="py-4">Centro: CCT</h2>
<h2 className="">TODO: Cargo</h2>
</div>
</div>
</div>
</main>
</div>
</>
);
};

View File

@@ -0,0 +1,23 @@
import React from "react";
import { Button } from "../../components";
export const SignIn = () => {
const handleLogin = async () => {
}
return (
<div className="w-screen h-screen bg-primary-normal grid place-items-center">
<div>
<img
alt="Logo do Unifeso"
src={'unifesoLogo'}
style={{ width: "85%", margin: "auto" }}
/>
<div className="grid place-items-center">
<Button onClick={handleLogin}>Faça login no Google</Button>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,23 @@
import React from "react";
import { Button } from "../../components";
export const UnauthorizedAccess = () => {
const handleSignOut = () => {
}
return (
<div className="w-screen h-screen bg-primary-normal grid place-items-center">
<div className="flex flex-col h-32 justify-between">
<h1 className="text-white text-xl">
{'currentUser?.email'}
{" "}
não possui permissão para acessar a plataforma!
</h1>
<div className="grid place-items-center">
<Button onClick={handleSignOut}>Encerrar Sessão</Button>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,3 @@
export { SignIn } from "./SignIn";
export { Profile } from "./Profile";
export { UnauthorizedAccess } from "./UnauthorizedAccess";

View File

@@ -0,0 +1,11 @@
import React, { FC } from "react";
export const Loading: FC = () => {
return (
<div className="grid h-screen w-screen bg-primary-dark place-items-center">
<div className="text-white text-lg">
Carregando...
</div>
</div>
)
}

View File

@@ -0,0 +1 @@
export * from './Loading'