Refactor submission state management so it is reusable

This commit is contained in:
Kacper Donat 2020-08-03 20:05:51 +02:00
parent 4aef2fd435
commit 9b64926675
8 changed files with 165 additions and 74 deletions

View File

@ -67,6 +67,10 @@ export interface Internship extends Identifiable {
office: BranchOffice;
}
export interface Plan extends Identifiable {
}
export interface Mentor {
name: string;
surname: string;

View File

@ -15,10 +15,11 @@ import { InternshipProposalActions, useDispatch } from "@/state/actions";
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
import { AppState } from "@/state/reducer";
import { useHistory } from "react-router-dom";
import { Link as RouterLink, useHistory } from "react-router-dom";
import { route } from "@/routing";
import { useProxyState } from "@/hooks";
import { getInternshipProposal } from "@/state/reducer/proposal";
import { Actions } from "@/components";
export type InternshipFormProps = {}
@ -161,7 +162,13 @@ export const InternshipForm: React.FunctionComponent<InternshipFormProps> = prop
<InternshipDurationForm internship={ internship } onChange={ setInternship }/>
<Typography variant="h3" className="section-header">Miejsce odbywania praktyki</Typography>
<CompanyForm internship={ internship } onChange={ setInternship }/>
<Button variant="contained" color="primary" onClick={ handleSubmit }>{ t("confirm") }</Button>
<Actions>
<Button variant="contained" color="primary" onClick={ handleSubmit }>{ t("confirm") }</Button>
<Button component={ RouterLink } to={ route("home") }>
{ t('go-back') }
</Button>
</Actions>
</div>
)
}

View File

@ -10,13 +10,14 @@ import { getMissingStudentData, Student } from "@/data";
import { Deadlines, Edition, getEditionDeadlines } from "@/data/edition";
import { Description as DescriptionIcon } from "@material-ui/icons"
import { Actions, Step } from "@/components";
import { getInternshipProposalStatus, InternshipProposalState, InternshipProposalStatus } from "@/state/reducer/proposal";
import { InternshipProposalState } from "@/state/reducer/proposal";
import { createStyles, makeStyles } from "@material-ui/core/styles";
import { ClipboardEditOutline, CommentQuestion, FileFind } from "mdi-material-ui";
import { Alert, AlertTitle } from "@material-ui/lab";
import { getSubmissionStatus, SubmissionStatus } from "@/state/reducer/submission";
const getColorByStatus = (status: InternshipProposalStatus, theme: Theme) => {
const getColorByStatus = (status: SubmissionStatus, theme: Theme) => {
switch (status) {
case "awaiting":
return theme.palette.info.dark;
@ -45,7 +46,7 @@ const useStatusStyles = makeStyles((theme: Theme) => {
})
const ProposalStatus = () => {
const status = useSelector<AppState, InternshipProposalStatus>(state => getInternshipProposalStatus(state.proposal))
const status = useSelector<AppState, SubmissionStatus>(state => getSubmissionStatus(state.proposal))
const classes = useStatusStyles({ status });
const { t } = useTranslation();
@ -54,7 +55,7 @@ const ProposalStatus = () => {
}
const ProposalActions = () => {
const status = useSelector<AppState, InternshipProposalStatus>(state => getInternshipProposalStatus(state.proposal));
const status = useSelector<AppState, SubmissionStatus>(state => getSubmissionStatus(state.proposal));
const { t } = useTranslation();
const ReviewAction = (props: ButtonProps) =>
@ -96,17 +97,17 @@ export const ProposalComment = (props: HTMLProps<HTMLDivElement>) => {
const { comment, declined } = useSelector<AppState, InternshipProposalState>(state => state.proposal);
const { t } = useTranslation();
return <Alert severity={declined ? "error" : "warning"} {...props as any}>
return comment ? <Alert severity={declined ? "error" : "warning"} {...props as any}>
<AlertTitle>{ t('comments') }</AlertTitle>
{ comment }
</Alert>
</Alert> : null
}
const ProposalStep = (props: StepProps) => {
const { t } = useTranslation();
const { sent, comment, declined } = useSelector<AppState, InternshipProposalState>(state => state.proposal);
const status = useSelector<AppState, InternshipProposalStatus>(state => getInternshipProposalStatus(state.proposal));
const status = useSelector<AppState, SubmissionStatus>(state => getSubmissionStatus(state.proposal));
const deadlines = useSelector<AppState, Deadlines>(state => getEditionDeadlines(state.edition as Edition)); // edition cannot be null at this point
return <Step { ...props }

View File

@ -1,7 +1,7 @@
import { SerializationTransformer } from "@/serialization/types";
import moment, { Moment } from "moment";
export const momentSerializationTransformer: SerializationTransformer<Moment, string> = {
transform: (subject: Moment) => subject.toISOString(),
reverseTransform: (subject: string) => moment(subject),
export const momentSerializationTransformer: SerializationTransformer<Moment | null, string> = {
transform: (subject: Moment) => subject && subject.toISOString(),
reverseTransform: (subject: string) => subject ? moment(subject) : null,
}

View File

@ -1,5 +1,11 @@
import { Action } from "@/state/actions/base";
import { Internship } from "@/data";
import {
ReceiveSubmissionApproveAction,
ReceiveSubmissionDeclineAction,
ReceiveSubmissionUpdateAction,
SaveSubmissionAction,
SendSubmissionAction
} from "@/state/actions/submission";
export enum InternshipProposalActions {
Send = "SEND_PROPOSAL",
@ -9,23 +15,23 @@ export enum InternshipProposalActions {
Receive = "RECEIVE_PROPOSAL_STATE",
}
export interface SendProposalAction extends Action<InternshipProposalActions.Send> {
export interface SendProposalAction extends SendSubmissionAction<InternshipProposalActions.Send> {
internship: Internship;
}
export interface ReceiveProposalApproveAction extends Action<InternshipProposalActions.Approve> {
export interface ReceiveProposalApproveAction extends ReceiveSubmissionApproveAction<InternshipProposalActions.Approve> {
comment: string | null;
}
export interface ReceiveProposalDeclineAction extends Action<InternshipProposalActions.Decline> {
export interface ReceiveProposalDeclineAction extends ReceiveSubmissionDeclineAction<InternshipProposalActions.Decline> {
comment: string;
}
export interface ReceiveProposalUpdateAction extends Action<InternshipProposalActions.Receive> {
export interface ReceiveProposalUpdateAction extends ReceiveSubmissionUpdateAction<InternshipProposalActions.Receive> {
}
export interface SaveProposalAction extends Action<InternshipProposalActions.Save> {
export interface SaveProposalAction extends SaveSubmissionAction<InternshipProposalActions.Save> {
internship: Internship;
}

View File

@ -0,0 +1,26 @@
import { Action } from "@/state/actions/base";
export enum SubmissionAction {
Send = "SEND",
Save = "SAVE",
Approve = "RECEIVE_APPROVE",
Decline = "RECEIVE_DECLINE",
Receive = "RECEIVE_STATE",
}
export interface SendSubmissionAction<T extends string> extends Action<T> {
}
export interface ReceiveSubmissionApproveAction<T extends string> extends Action<T> {
comment: string | null;
}
export interface ReceiveSubmissionDeclineAction<T extends string> extends Action<T> {
comment: string;
}
export interface ReceiveSubmissionUpdateAction<T extends string> extends Action<T> {
}
export interface SaveSubmissionAction<T extends string> extends Action<T> {
}

View File

@ -1,80 +1,47 @@
import { DeanApproval } from "@/data/deanApproval";
import { InternshipProposalAction, InternshipProposalActions } from "@/state/actions";
import { Internship } from "@/data";
import moment from "moment";
import { Serializable } from "@/serialization/types";
import { internshipSerializationTransformer, momentSerializationTransformer } from "@/serialization";
import { internshipSerializationTransformer } from "@/serialization";
import {
createSubmissionReducer,
defaultDeanApprovalsState,
defaultSubmissionState,
MayRequireDeanApproval,
SubmissionState
} from "@/state/reducer/submission";
import { Reducer } from "react";
import { SubmissionAction } from "@/state/actions/submission";
export type InternshipProposalStatus = "draft" | "awaiting" | "accepted" | "declined";
export type InternshipProposalState = {
accepted: boolean;
sent: boolean;
sentOn: string | null;
declined: boolean;
requiredDeanApprovals: DeanApproval[];
export type InternshipProposalState = SubmissionState & MayRequireDeanApproval & {
proposal: Serializable<Internship> | null;
comment: string | null;
}
const defaultInternshipProposalState: InternshipProposalState = {
accepted: false,
declined: false,
sentOn: null,
...defaultDeanApprovalsState,
...defaultSubmissionState,
proposal: null,
requiredDeanApprovals: [],
sent: false,
comment: null
}
export const getInternshipProposalStatus = ({ accepted, declined, sent }: InternshipProposalState): InternshipProposalStatus => {
switch (true) {
case !sent:
return "draft";
case sent && accepted:
return "accepted"
case sent && declined:
return "declined"
case sent && (!accepted && !declined):
return "awaiting"
default:
throw new Error("Invalid proposal state " + JSON.stringify({ accepted, declined, sent }));
}
}
export const getInternshipProposal = ({ proposal }: InternshipProposalState): Internship | null =>
proposal && internshipSerializationTransformer.reverseTransform(proposal);
const internshipProposalSubmissionReducer: Reducer<InternshipProposalState, InternshipProposalAction> = createSubmissionReducer({
[InternshipProposalActions.Approve]: SubmissionAction.Approve,
[InternshipProposalActions.Decline]: SubmissionAction.Decline,
[InternshipProposalActions.Receive]: SubmissionAction.Receive,
[InternshipProposalActions.Save]: SubmissionAction.Save,
[InternshipProposalActions.Send]: SubmissionAction.Send,
})
const internshipProposalReducer = (state: InternshipProposalState = defaultInternshipProposalState, action: InternshipProposalAction): InternshipProposalState => {
state = internshipProposalSubmissionReducer(state, action);
switch (action.type) {
case InternshipProposalActions.Approve:
return {
...state,
accepted: true,
declined: false,
comment: action.comment,
}
case InternshipProposalActions.Decline:
return {
...state,
accepted: false,
declined: true,
comment: action.comment
}
case InternshipProposalActions.Save:
return {
...state,
proposal: internshipSerializationTransformer.transform(action.internship),
}
case InternshipProposalActions.Send:
return {
...state,
proposal: internshipSerializationTransformer.transform(action.internship),
sent: true,
sentOn: momentSerializationTransformer.transform(moment()),
accepted: false,
declined: false,
comment: null,
}
default:
return state;

View File

@ -0,0 +1,80 @@
import { DeanApproval } from "@/data/deanApproval";
import { Action } from "@/state/actions";
import { momentSerializationTransformer } from "@/serialization";
import moment from "moment";
import { ReceiveSubmissionApproveAction, ReceiveSubmissionDeclineAction, SubmissionAction } from "@/state/actions/submission";
export type SubmissionStatus = "draft" | "awaiting" | "accepted" | "declined";
export type SubmissionState = {
accepted: boolean;
sent: boolean;
sentOn: string | null;
declined: boolean;
comment: string | null;
}
export type MayRequireDeanApproval = {
requiredDeanApprovals: DeanApproval[],
}
export const defaultSubmissionState: SubmissionState = {
accepted: false,
sent: false,
sentOn: null,
declined: false,
comment: null,
}
export const defaultDeanApprovalsState: MayRequireDeanApproval = {
requiredDeanApprovals: [],
}
export const getSubmissionStatus = ({ accepted, declined, sent }: SubmissionState): SubmissionStatus => {
switch (true) {
case !sent:
return "draft";
case sent && accepted:
return "accepted"
case sent && declined:
return "declined"
case sent && (!accepted && !declined):
return "awaiting"
default:
throw new Error("Invalid submission state " + JSON.stringify({ accepted, declined, sent }));
}
}
export function createSubmissionReducer<TState, TActionType, TAction extends Action>(mapping: { [TAction in keyof TActionType]: SubmissionAction }) {
return (state: TState, action: TAction) => {
const mappedAction = mapping[action.type as keyof TActionType];
switch (mappedAction) {
case SubmissionAction.Approve:
return {
...state,
accepted: true,
declined: false,
comment: (action as ReceiveSubmissionApproveAction<any>).comment,
}
case SubmissionAction.Decline:
return {
...state,
accepted: false,
declined: true,
comment: (action as ReceiveSubmissionDeclineAction<any>).comment,
}
case SubmissionAction.Send:
return {
...state,
sent: true,
sentOn: momentSerializationTransformer.transform(moment()),
accepted: false,
declined: false,
comment: null,
}
default:
return state;
}
}
}