Refactor submission state management so it is reusable
This commit is contained in:
parent
4aef2fd435
commit
9b64926675
@ -67,6 +67,10 @@ export interface Internship extends Identifiable {
|
|||||||
office: BranchOffice;
|
office: BranchOffice;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Plan extends Identifiable {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
export interface Mentor {
|
export interface Mentor {
|
||||||
name: string;
|
name: string;
|
||||||
surname: string;
|
surname: string;
|
||||||
|
@ -15,10 +15,11 @@ import { InternshipProposalActions, useDispatch } from "@/state/actions";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { AppState } from "@/state/reducer";
|
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 { route } from "@/routing";
|
||||||
import { useProxyState } from "@/hooks";
|
import { useProxyState } from "@/hooks";
|
||||||
import { getInternshipProposal } from "@/state/reducer/proposal";
|
import { getInternshipProposal } from "@/state/reducer/proposal";
|
||||||
|
import { Actions } from "@/components";
|
||||||
|
|
||||||
export type InternshipFormProps = {}
|
export type InternshipFormProps = {}
|
||||||
|
|
||||||
@ -161,7 +162,13 @@ export const InternshipForm: React.FunctionComponent<InternshipFormProps> = prop
|
|||||||
<InternshipDurationForm internship={ internship } onChange={ setInternship }/>
|
<InternshipDurationForm internship={ internship } onChange={ setInternship }/>
|
||||||
<Typography variant="h3" className="section-header">Miejsce odbywania praktyki</Typography>
|
<Typography variant="h3" className="section-header">Miejsce odbywania praktyki</Typography>
|
||||||
<CompanyForm internship={ internship } onChange={ setInternship }/>
|
<CompanyForm internship={ internship } onChange={ setInternship }/>
|
||||||
|
<Actions>
|
||||||
<Button variant="contained" color="primary" onClick={ handleSubmit }>{ t("confirm") }</Button>
|
<Button variant="contained" color="primary" onClick={ handleSubmit }>{ t("confirm") }</Button>
|
||||||
|
|
||||||
|
<Button component={ RouterLink } to={ route("home") }>
|
||||||
|
{ t('go-back') }
|
||||||
|
</Button>
|
||||||
|
</Actions>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -10,13 +10,14 @@ import { getMissingStudentData, Student } from "@/data";
|
|||||||
import { Deadlines, Edition, getEditionDeadlines } from "@/data/edition";
|
import { Deadlines, Edition, getEditionDeadlines } from "@/data/edition";
|
||||||
import { Description as DescriptionIcon } from "@material-ui/icons"
|
import { Description as DescriptionIcon } from "@material-ui/icons"
|
||||||
import { Actions, Step } from "@/components";
|
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 { createStyles, makeStyles } from "@material-ui/core/styles";
|
||||||
import { ClipboardEditOutline, CommentQuestion, FileFind } from "mdi-material-ui";
|
import { ClipboardEditOutline, CommentQuestion, FileFind } from "mdi-material-ui";
|
||||||
import { Alert, AlertTitle } from "@material-ui/lab";
|
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) {
|
switch (status) {
|
||||||
case "awaiting":
|
case "awaiting":
|
||||||
return theme.palette.info.dark;
|
return theme.palette.info.dark;
|
||||||
@ -45,7 +46,7 @@ const useStatusStyles = makeStyles((theme: Theme) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const ProposalStatus = () => {
|
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 classes = useStatusStyles({ status });
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -54,7 +55,7 @@ const ProposalStatus = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ProposalActions = () => {
|
const ProposalActions = () => {
|
||||||
const status = useSelector<AppState, InternshipProposalStatus>(state => getInternshipProposalStatus(state.proposal));
|
const status = useSelector<AppState, SubmissionStatus>(state => getSubmissionStatus(state.proposal));
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const ReviewAction = (props: ButtonProps) =>
|
const ReviewAction = (props: ButtonProps) =>
|
||||||
@ -96,17 +97,17 @@ export const ProposalComment = (props: HTMLProps<HTMLDivElement>) => {
|
|||||||
const { comment, declined } = useSelector<AppState, InternshipProposalState>(state => state.proposal);
|
const { comment, declined } = useSelector<AppState, InternshipProposalState>(state => state.proposal);
|
||||||
const { t } = useTranslation();
|
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>
|
<AlertTitle>{ t('comments') }</AlertTitle>
|
||||||
{ comment }
|
{ comment }
|
||||||
</Alert>
|
</Alert> : null
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProposalStep = (props: StepProps) => {
|
const ProposalStep = (props: StepProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { sent, comment, declined } = useSelector<AppState, InternshipProposalState>(state => state.proposal);
|
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
|
const deadlines = useSelector<AppState, Deadlines>(state => getEditionDeadlines(state.edition as Edition)); // edition cannot be null at this point
|
||||||
|
|
||||||
return <Step { ...props }
|
return <Step { ...props }
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { SerializationTransformer } from "@/serialization/types";
|
import { SerializationTransformer } from "@/serialization/types";
|
||||||
import moment, { Moment } from "moment";
|
import moment, { Moment } from "moment";
|
||||||
|
|
||||||
export const momentSerializationTransformer: SerializationTransformer<Moment, string> = {
|
export const momentSerializationTransformer: SerializationTransformer<Moment | null, string> = {
|
||||||
transform: (subject: Moment) => subject.toISOString(),
|
transform: (subject: Moment) => subject && subject.toISOString(),
|
||||||
reverseTransform: (subject: string) => moment(subject),
|
reverseTransform: (subject: string) => subject ? moment(subject) : null,
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
import { Action } from "@/state/actions/base";
|
|
||||||
import { Internship } from "@/data";
|
import { Internship } from "@/data";
|
||||||
|
import {
|
||||||
|
ReceiveSubmissionApproveAction,
|
||||||
|
ReceiveSubmissionDeclineAction,
|
||||||
|
ReceiveSubmissionUpdateAction,
|
||||||
|
SaveSubmissionAction,
|
||||||
|
SendSubmissionAction
|
||||||
|
} from "@/state/actions/submission";
|
||||||
|
|
||||||
export enum InternshipProposalActions {
|
export enum InternshipProposalActions {
|
||||||
Send = "SEND_PROPOSAL",
|
Send = "SEND_PROPOSAL",
|
||||||
@ -9,23 +15,23 @@ export enum InternshipProposalActions {
|
|||||||
Receive = "RECEIVE_PROPOSAL_STATE",
|
Receive = "RECEIVE_PROPOSAL_STATE",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SendProposalAction extends Action<InternshipProposalActions.Send> {
|
export interface SendProposalAction extends SendSubmissionAction<InternshipProposalActions.Send> {
|
||||||
internship: Internship;
|
internship: Internship;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReceiveProposalApproveAction extends Action<InternshipProposalActions.Approve> {
|
export interface ReceiveProposalApproveAction extends ReceiveSubmissionApproveAction<InternshipProposalActions.Approve> {
|
||||||
comment: string | null;
|
comment: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReceiveProposalDeclineAction extends Action<InternshipProposalActions.Decline> {
|
export interface ReceiveProposalDeclineAction extends ReceiveSubmissionDeclineAction<InternshipProposalActions.Decline> {
|
||||||
comment: string;
|
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;
|
internship: Internship;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
26
src/state/actions/submission.ts
Normal file
26
src/state/actions/submission.ts
Normal 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> {
|
||||||
|
}
|
@ -1,80 +1,47 @@
|
|||||||
import { DeanApproval } from "@/data/deanApproval";
|
|
||||||
import { InternshipProposalAction, InternshipProposalActions } from "@/state/actions";
|
import { InternshipProposalAction, InternshipProposalActions } from "@/state/actions";
|
||||||
import { Internship } from "@/data";
|
import { Internship } from "@/data";
|
||||||
import moment from "moment";
|
|
||||||
import { Serializable } from "@/serialization/types";
|
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 = SubmissionState & MayRequireDeanApproval & {
|
||||||
|
|
||||||
export type InternshipProposalState = {
|
|
||||||
accepted: boolean;
|
|
||||||
sent: boolean;
|
|
||||||
sentOn: string | null;
|
|
||||||
declined: boolean;
|
|
||||||
requiredDeanApprovals: DeanApproval[];
|
|
||||||
proposal: Serializable<Internship> | null;
|
proposal: Serializable<Internship> | null;
|
||||||
comment: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultInternshipProposalState: InternshipProposalState = {
|
const defaultInternshipProposalState: InternshipProposalState = {
|
||||||
accepted: false,
|
...defaultDeanApprovalsState,
|
||||||
declined: false,
|
...defaultSubmissionState,
|
||||||
sentOn: null,
|
|
||||||
proposal: null,
|
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 =>
|
export const getInternshipProposal = ({ proposal }: InternshipProposalState): Internship | null =>
|
||||||
proposal && internshipSerializationTransformer.reverseTransform(proposal);
|
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 => {
|
const internshipProposalReducer = (state: InternshipProposalState = defaultInternshipProposalState, action: InternshipProposalAction): InternshipProposalState => {
|
||||||
|
state = internshipProposalSubmissionReducer(state, action);
|
||||||
|
|
||||||
switch (action.type) {
|
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:
|
case InternshipProposalActions.Save:
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
proposal: internshipSerializationTransformer.transform(action.internship),
|
|
||||||
}
|
|
||||||
case InternshipProposalActions.Send:
|
case InternshipProposalActions.Send:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
proposal: internshipSerializationTransformer.transform(action.internship),
|
proposal: internshipSerializationTransformer.transform(action.internship),
|
||||||
sent: true,
|
|
||||||
sentOn: momentSerializationTransformer.transform(moment()),
|
|
||||||
accepted: false,
|
|
||||||
declined: false,
|
|
||||||
comment: null,
|
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
|
80
src/state/reducer/submission.ts
Normal file
80
src/state/reducer/submission.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user