From 9b64926675ce1fa08a0a0979b20359fc69f63861 Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Mon, 3 Aug 2020 20:05:51 +0200 Subject: [PATCH] Refactor submission state management so it is reusable --- src/data/internship.ts | 4 ++ src/forms/internship.tsx | 11 ++++- src/pages/main.tsx | 15 ++++--- src/serialization/moment.ts | 6 +-- src/state/actions/proposal.ts | 18 +++++--- src/state/actions/submission.ts | 26 +++++++++++ src/state/reducer/proposal.ts | 79 ++++++++++---------------------- src/state/reducer/submission.ts | 80 +++++++++++++++++++++++++++++++++ 8 files changed, 165 insertions(+), 74 deletions(-) create mode 100644 src/state/actions/submission.ts create mode 100644 src/state/reducer/submission.ts diff --git a/src/data/internship.ts b/src/data/internship.ts index bc82578..ace8c57 100644 --- a/src/data/internship.ts +++ b/src/data/internship.ts @@ -67,6 +67,10 @@ export interface Internship extends Identifiable { office: BranchOffice; } +export interface Plan extends Identifiable { + +} + export interface Mentor { name: string; surname: string; diff --git a/src/forms/internship.tsx b/src/forms/internship.tsx index abd58f4..01b8689 100644 --- a/src/forms/internship.tsx +++ b/src/forms/internship.tsx @@ -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 = prop Miejsce odbywania praktyki - + + + + + ) } diff --git a/src/pages/main.tsx b/src/pages/main.tsx index 97f1016..21df4f3 100644 --- a/src/pages/main.tsx +++ b/src/pages/main.tsx @@ -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(state => getInternshipProposalStatus(state.proposal)) + const status = useSelector(state => getSubmissionStatus(state.proposal)) const classes = useStatusStyles({ status }); const { t } = useTranslation(); @@ -54,7 +55,7 @@ const ProposalStatus = () => { } const ProposalActions = () => { - const status = useSelector(state => getInternshipProposalStatus(state.proposal)); + const status = useSelector(state => getSubmissionStatus(state.proposal)); const { t } = useTranslation(); const ReviewAction = (props: ButtonProps) => @@ -96,17 +97,17 @@ export const ProposalComment = (props: HTMLProps) => { const { comment, declined } = useSelector(state => state.proposal); const { t } = useTranslation(); - return + return comment ? { t('comments') } { comment } - + : null } const ProposalStep = (props: StepProps) => { const { t } = useTranslation(); const { sent, comment, declined } = useSelector(state => state.proposal); - const status = useSelector(state => getInternshipProposalStatus(state.proposal)); + const status = useSelector(state => getSubmissionStatus(state.proposal)); const deadlines = useSelector(state => getEditionDeadlines(state.edition as Edition)); // edition cannot be null at this point return = { - transform: (subject: Moment) => subject.toISOString(), - reverseTransform: (subject: string) => moment(subject), +export const momentSerializationTransformer: SerializationTransformer = { + transform: (subject: Moment) => subject && subject.toISOString(), + reverseTransform: (subject: string) => subject ? moment(subject) : null, } diff --git a/src/state/actions/proposal.ts b/src/state/actions/proposal.ts index 9b13c85..20f4af6 100644 --- a/src/state/actions/proposal.ts +++ b/src/state/actions/proposal.ts @@ -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 { +export interface SendProposalAction extends SendSubmissionAction { internship: Internship; } -export interface ReceiveProposalApproveAction extends Action { +export interface ReceiveProposalApproveAction extends ReceiveSubmissionApproveAction { comment: string | null; } -export interface ReceiveProposalDeclineAction extends Action { +export interface ReceiveProposalDeclineAction extends ReceiveSubmissionDeclineAction { comment: string; } -export interface ReceiveProposalUpdateAction extends Action { +export interface ReceiveProposalUpdateAction extends ReceiveSubmissionUpdateAction { } -export interface SaveProposalAction extends Action { +export interface SaveProposalAction extends SaveSubmissionAction { internship: Internship; } diff --git a/src/state/actions/submission.ts b/src/state/actions/submission.ts new file mode 100644 index 0000000..e8338e4 --- /dev/null +++ b/src/state/actions/submission.ts @@ -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 extends Action { +} + +export interface ReceiveSubmissionApproveAction extends Action { + comment: string | null; +} + +export interface ReceiveSubmissionDeclineAction extends Action { + comment: string; +} + +export interface ReceiveSubmissionUpdateAction extends Action { +} + +export interface SaveSubmissionAction extends Action { +} diff --git a/src/state/reducer/proposal.ts b/src/state/reducer/proposal.ts index 890f543..783983c 100644 --- a/src/state/reducer/proposal.ts +++ b/src/state/reducer/proposal.ts @@ -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 | 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 = 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; diff --git a/src/state/reducer/submission.ts b/src/state/reducer/submission.ts new file mode 100644 index 0000000..864cf2f --- /dev/null +++ b/src/state/reducer/submission.ts @@ -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(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).comment, + } + case SubmissionAction.Decline: + return { + ...state, + accepted: false, + declined: true, + comment: (action as ReceiveSubmissionDeclineAction).comment, + } + case SubmissionAction.Send: + return { + ...state, + sent: true, + sentOn: momentSerializationTransformer.transform(moment()), + accepted: false, + declined: false, + comment: null, + } + default: + return state; + } + } +}