From c13d880baa5f5e5b831b83a1b2b0f4ca625d2fba Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Wed, 11 Nov 2020 15:04:59 +0100 Subject: [PATCH] Allow overriding of state for testing purposes. --- src/api/dto/internship-registration.ts | 4 +- src/api/internship.ts | 37 ++++++++++++-- src/app.tsx | 2 +- src/components/proposalPreview.tsx | 13 ++++- src/forms/internship.tsx | 71 +++++++++++++++++--------- src/pages/fallback.tsx | 20 ++++---- src/pages/internship/proposal.tsx | 2 +- src/state/actions/proposal.ts | 1 - src/state/reducer/plan.ts | 6 +++ src/state/reducer/proposal.ts | 6 ++- src/state/reducer/submission.ts | 5 ++ translations/pl.yaml | 6 +++ 12 files changed, 130 insertions(+), 43 deletions(-) diff --git a/src/api/dto/internship-registration.ts b/src/api/dto/internship-registration.ts index 0bb694a..acb869a 100644 --- a/src/api/dto/internship-registration.ts +++ b/src/api/dto/internship-registration.ts @@ -6,6 +6,7 @@ import { InternshipTypeDTO, internshipTypeDtoTransformer } from "@/api/dto/type" import { Moment } from "moment-timezone"; import { sampleStudent } from "@/provider/dummy"; import { UploadType } from "@/api/upload"; +import { ProgramEntryDTO, programEntryDtoTransformer } from "@/api/dto/edition"; export enum SubmissionState { Draft = "Draft", @@ -48,6 +49,7 @@ export interface InternshipRegistrationDTO extends Identifiable { company: Company, branchAddress: Office, declaredHours: number, + subjects: { subject: ProgramEntryDTO }[], } export interface InternshipDocument extends Identifiable { @@ -99,7 +101,7 @@ export const internshipRegistrationDtoTransformer: OneWayTransformer programEntryDtoTransformer.transform(subject.subject)), intern: sampleStudent, // fixme }; } diff --git a/src/api/internship.ts b/src/api/internship.ts index 961c789..ba09dfa 100644 --- a/src/api/internship.ts +++ b/src/api/internship.ts @@ -1,14 +1,43 @@ -import { InternshipInfoDTO, InternshipRegistrationUpdate } from "@/api/dto/internship-registration"; +import { InternshipInfoDTO, InternshipRegistrationUpdate, SubmissionState } from "@/api/dto/internship-registration"; import { axios } from "@/api/index"; import { Nullable } from "@/helpers"; const INTERNSHIP_REGISTRATION_ENDPOINT = '/internshipRegistration'; const INTERNSHIP_ENDPOINT = '/internship'; -export async function update(internship: Nullable): Promise { - await axios.put(INTERNSHIP_REGISTRATION_ENDPOINT, internship); +export type ValidationMessage = { + key: string; + parameters: { [name: string]: string }, +} - return true; +export class ValidationError extends Error { + public readonly messages: ValidationMessage[]; + + constructor(messages: ValidationMessage[], message: string = "There were validation errors.") { + super(message); + Object.setPrototypeOf(this, ValidationError.prototype); + + this.messages = messages; + } +} + +interface UpdateResponse { + status: SubmissionState; + errors?: string[]; +} + +export async function update(internship: Nullable): Promise { + const response = (await axios.put(INTERNSHIP_REGISTRATION_ENDPOINT, internship)).data; + + if (response.status == SubmissionState.Draft) { + throw new ValidationError( + response.errors?.map( + msg => ({ key: msg, parameters: {} }) + ) || [] + ); + } + + return response.status; } export async function get(): Promise { diff --git a/src/app.tsx b/src/app.tsx index 3179805..607090a 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -88,7 +88,7 @@ function App() { diff --git a/src/components/proposalPreview.tsx b/src/components/proposalPreview.tsx index d11ea6f..d169680 100644 --- a/src/components/proposalPreview.tsx +++ b/src/components/proposalPreview.tsx @@ -1,12 +1,13 @@ import { Internship } from "@/data"; import React from "react"; -import { Typography } from "@material-ui/core"; +import { List, Typography, ListItem, ListItemIcon, ListItemText } from "@material-ui/core"; import { useTranslation } from "react-i18next"; import classNames from "classnames"; import { useVerticalSpacing } from "@/styles"; import moment from "moment-timezone"; import { Label, Section } from "@/components/section"; import { StudentPreview } from "@/pages/user/profile"; +import { Check, StickerCheck } from "mdi-material-ui"; export type ProposalPreviewProps = { proposal: Internship; @@ -42,6 +43,16 @@ export const ProposalPreview = ({ proposal }: ProposalPreviewProps) => { { proposal.type.label.pl } +
+ + + { proposal.program.map(subject => + + { subject.description } + ) } + +
+
diff --git a/src/forms/internship.tsx b/src/forms/internship.tsx index 781a2e4..f5bfe04 100644 --- a/src/forms/internship.tsx +++ b/src/forms/internship.tsx @@ -1,16 +1,16 @@ -import React, { HTMLProps, useEffect, useMemo, useState } from "react"; +import React, { HTMLProps, useEffect, useMemo, useRef, useState } from "react"; import { Button, + Checkbox, Dialog, DialogActions, DialogContent, DialogContentText, + FormControlLabel, + FormGroup, Grid, TextField, - Typography, - FormGroup, - FormControlLabel, - Checkbox + Typography } from "@material-ui/core"; import { KeyboardDatePicker as DatePicker } from "@material-ui/pickers"; import { CompanyForm } from "@/forms/company"; @@ -18,11 +18,11 @@ import { StudentForm } from "@/forms/student"; import { sampleStudent } from "@/provider/dummy/student"; import { Company, Internship, InternshipProgramEntry, InternshipType, Office, Student } from "@/data"; import { Nullable } from "@/helpers"; -import moment, { Moment } from "moment-timezone"; +import { Moment } from "moment-timezone"; import { computeWorkingHours } from "@/utils/date"; -import { Autocomplete } from "@material-ui/lab"; +import { Alert, AlertTitle, Autocomplete } from "@material-ui/lab"; import { emptyInternship } from "@/provider/dummy/internship"; -import { useDispatch } from "@/state/actions"; +import { InternshipProposalActions, useDispatch } from "@/state/actions"; import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; import { AppState } from "@/state/reducer"; @@ -38,7 +38,7 @@ import { useCurrentEdition, useCurrentStudent, useInternshipTypes, useUpdateEffe import { internshipRegistrationUpdateTransformer } from "@/api/dto/internship-registration"; import api from "@/api"; import FormLabel from "@material-ui/core/FormLabel"; -import { CheckBox } from "@material-ui/icons"; +import { ValidationError, ValidationMessage } from "@/api/internship"; export type InternshipFormValues = { startDate: Moment | null; @@ -145,7 +145,7 @@ const InternshipProgramForm = () => { { possibleProgramEntries.map( entry => } - checked={ selectedProgramEntries.includes(entry) } + checked={ selectedProgramEntries.find(cur => entry.id == cur.id) !== undefined } onChange={ handleProgramEntryChange(entry) } label={ entry.description } key={ entry.id } @@ -182,17 +182,21 @@ const InternshipDurationForm = () => { return ( - setFieldValue("startDate", value) } - format="DD MMMM yyyy" + setFieldValue("startDate", value) } + format="DD.MM.yyyy" disableToolbar fullWidth - variant="inline" label={ t("forms.internship.fields.start-date") } + variant="inline" + label={ t("forms.internship.fields.start-date") } /> - setFieldValue("endDate", value) } - format="DD MMMM yyyy" + setFieldValue("endDate", value) } + format="DD.MM.yyyy" disableToolbar fullWidth - variant="inline" label={ t("forms.internship.fields.end-date") } + variant="inline" + label={ t("forms.internship.fields.end-date") } minDate={ startDate } /> @@ -287,7 +291,12 @@ const converter: Transformer, InternshipFormValues, Interns } export const InternshipForm: React.FunctionComponent = () => { - const student = useCurrentStudent(); + const student = useCurrentStudent(); + const history = useHistory(); + const root = useRef(null); + const dispatch = useDispatch(); + + const [errors, setErrors] = useState([]); const initialInternship = useSelector>(state => getInternshipProposal(state.proposal) || { ...emptyInternship, @@ -336,17 +345,25 @@ export const InternshipForm: React.FunctionComponent = () => { const values = converter.transform(initialInternship); - const handleSubmit = (values: InternshipFormValues) => { + const handleSubmit = async (values: InternshipFormValues) => { setConfirmDialogOpen(false); const internship = converter.reverseTransform(values, { internship: initialInternship as Internship }); const update = internshipRegistrationUpdateTransformer.transform(internship); - console.log(update); + try { + await api.internship.update(update); + dispatch({ type: InternshipProposalActions.Send }); - api.internship.update(update); - - // history.push(route("home")) + history.push(route("home")) + } catch (error) { + if (error instanceof ValidationError) { + setErrors(error.messages); + root.current?.scrollIntoView({ behavior: "smooth" }) + } else { + throw error; + } + } } const InnerForm = () => { @@ -366,10 +383,16 @@ export const InternshipForm: React.FunctionComponent = () => { setConfirmDialogOpen(false); } - return
+ return + { errors.length > 0 && + { t('internship.validation.has-errors') } +
    + { errors.map(message =>
  • { t(`internship.validation.${message.key}`, message.parameters) }
  • ) } +
+
} { t('internship.sections.intern-info') } - { t('internship.sections.kind' )} + { t('internship.sections.kind') } { t('internship.sections.duration') } diff --git a/src/pages/fallback.tsx b/src/pages/fallback.tsx index ff1584f..c383002 100644 --- a/src/pages/fallback.tsx +++ b/src/pages/fallback.tsx @@ -5,6 +5,8 @@ import React, { useMemo } from "react"; import { route } from "@/routing"; import { useAsync } from "@/hooks"; import api from "@/api"; +import { Loading } from "@/components/loading"; +import { useSpacing } from "@/styles"; export const FallbackPage = () => { const location = useLocation(); @@ -13,23 +15,23 @@ export const FallbackPage = () => { const { isLoading, value, error } = useAsync(promise); - console.log({ isLoading, value, error, location }); - if (isLoading) { - return + return
} if (error) { return - 404 - Strona nie została znaleziona + + 404 + Strona nie została znaleziona - - + + + + + - - } diff --git a/src/pages/internship/proposal.tsx b/src/pages/internship/proposal.tsx index 1b3a814..59ae0ee 100644 --- a/src/pages/internship/proposal.tsx +++ b/src/pages/internship/proposal.tsx @@ -156,7 +156,7 @@ export const InternshipProposalPreviewPage = () => { { t("internship.accept.title") } { t("internship.accept.info") } - setComment(ev.target.value) } fullWidth label={ t("comments") }/> + setComment(ev.target.value) } fullWidth label={ t("comments") } rows={3}/>