diff --git a/package.json b/package.json index 306206a879bc623b05cf2c21827e0665a7556a27..390934c7f71ced1f3a1e98a75b026dee0e6d19d7 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "http-proxy-middleware": "^0.20.0", "immer": "^4.0.0", "jsonwebtoken": "^8.5.1", + "lodash": "^4.17.15", "material-table": "^1.40.0", "path": "^0.12.7", "query-string": "^6.8.3", @@ -64,6 +65,7 @@ "@types/dateformat": "^3.0.0", "@types/faker": "^4.1.5", "@types/jest": "24.0.16", + "@types/lodash": "^4.14.145", "@types/node": "12.6.9", "@types/react": "16.8.24", "@types/react-dom": "16.8.5", diff --git a/src/components/PhotoInSide.js b/src/components/PhotoInSide.js index b8c966fa1c807c0c4a406cb9fa922c6e78535607..85a534249e81e8c9496f60bb888a4fbf6d428cc1 100644 --- a/src/components/PhotoInSide.js +++ b/src/components/PhotoInSide.js @@ -10,7 +10,7 @@ const useStyles = makeStyles(theme => ({ }, image: { backgroundImage: - "url(https://lh3.googleusercontent.com/-M6eItc6QC1k/XS8Gew8sG8I/AAAAAAAANmo/_7-Tnmk8jKU6CSwcrB32-UAM0PnQMLMDQCK8BGAs/s0/2019-07-17.png)", + "url(https://lh3.googleusercontent.com/Ha2a4uO-rMRdrgPrc-7c609rHWk8W2b6I9_7j6Maz6BSBnUEED8vvadc3AmbvD1ipqLWOiweFJYV4EAurr-vl5Un9-AnVbLCNdzbrVi9gafMOg2KIHcbu9LBjKaQ_n0VpjGTdy67o_zh1Let8518vR0l2H9Iut814s7D8qBNDduJ6qsHuqOEftlSHTwveqBsNOOHmQ7HurrfIOBtZXaXKYd7u3In2DZSbDmMraKoQ1QssISRNc8C_nuGC2rQaK2KCftqsMNGAqrg0cOw7ayAEiS7W5XSCr9c0NvoIsPaMqiBqTfnM_k-5eb_rPXnQqeZ8FwUDvB-w9vubNTw_cP9l82MVGPmTXwTTQ79-dfIlG8HD1HTyDYCygg8BCyK-QI_mk-m-UngfUMPVeQ4M2AK1rvo_PKAjYdasSz2E6ZDxHlAv5q5rg0NXulua9SWaVUPifMs2vVwJJ8L7QvD_MOhAzCmF70F8xq390ugLKo9No8El-GrRk532BoIhoYIjhbTNbQ-b_HgEimX0V6yoGsf8p2gEYJbHzhqVWnD7vfgapv1Dp1XLtUE83kx77xYQca1J600eqUGe5bjTem00Q-qkBMO9IhRMABxdXUty1AK45vkV-EF62lBo__Z9OfUf0DpGvjbXOmrqCFelYe3HLgGtrEJmgSwRBN1aAQShNZTOkADX9o=w1478-h1213-no)", backgroundRepeat: "no-repeat", backgroundSize: "cover", backgroundPosition: "center" diff --git a/src/components/ProposalCompontentDatePicker.tsx b/src/components/ProposalCompontentDatePicker.tsx index 156f18fe44c413ab2e1c7ba6cefe32264264e3f2..2a71d5024730b3fbe9544e34ff84606c369ca94f 100644 --- a/src/components/ProposalCompontentDatePicker.tsx +++ b/src/components/ProposalCompontentDatePicker.tsx @@ -7,10 +7,12 @@ import { } from "@material-ui/pickers"; import { IBasicComponentProps } from "./IBasicComponentProps"; import { Field } from "formik"; +import { getIn } from "formik"; export function ProposalCompontentDatePicker(props: IBasicComponentProps) { - const { templateField, onComplete, errors } = props; + let { templateField, onComplete, touched, errors } = props; const { proposal_question_id, config, question } = templateField; - const isError = errors[proposal_question_id] ? true : false; + const fieldError = getIn(errors, proposal_question_id); + const isError = getIn(touched, proposal_question_id) && !!fieldError; return ( <FormControl error={isError}> @@ -19,7 +21,6 @@ export function ProposalCompontentDatePicker(props: IBasicComponentProps) { name={proposal_question_id} label={question} component={({ field, form, ...other }: { field: any; form: any }) => { - const currentError = form.errors[field.name]; return ( <KeyboardDatePicker clearable={true} @@ -27,7 +28,7 @@ export function ProposalCompontentDatePicker(props: IBasicComponentProps) { name={field.name} value={field.value || ""} format="dd/MMM/yyyy" - helperText={currentError} + helperText={isError && errors[proposal_question_id]} label={question} onChange={date => { templateField.value = date; diff --git a/src/components/ProposalContainer.tsx b/src/components/ProposalContainer.tsx index 7b8e4e0b2ea3a46ba683c5a24561fc2c8ab44bea..d37bd65aaa91fed4d72277ac7f31e7df40c72f0e 100644 --- a/src/components/ProposalContainer.tsx +++ b/src/components/ProposalContainer.tsx @@ -18,6 +18,7 @@ import ProposalInformationView from "./ProposalInformationView"; import { useLoadProposal } from "../hooks/useLoadProposal"; import Notification from "./Notification"; import { StepButton } from "@material-ui/core"; +import _ from "lodash"; export interface INotification { variant: "error" | "success"; @@ -46,6 +47,7 @@ export default function ProposalContainer(props: { variant: "success", message: "" }); + const isSubmitted = proposalInfo.status === ProposalStatus.SUBMITTED; const classes = makeStyles(theme => ({ paper: { marginTop: theme.spacing(3), @@ -59,6 +61,15 @@ export default function ProposalContainer(props: { }, stepper: { padding: theme.spacing(3, 0, 5) + }, + heading: { + textOverflow: "ellipsis", + width: "80%", + margin: "0 auto", + textAlign: "center", + minWidth: "450px", + whiteSpace: "nowrap", + overflow: "hidden" } }))(); @@ -67,7 +78,7 @@ export default function ProposalContainer(props: { ...proposalInfo, ...data }); - setStepIndex(stepIndex + 1); + setStepIndex(clampStep(stepIndex + 1)); setIsDirty(false); }; @@ -76,10 +87,13 @@ export default function ProposalContainer(props: { ...proposalInfo, ...data }); - setStepIndex(stepIndex - 1); + setStepIndex(clampStep(stepIndex - 1)); setIsDirty(false); }; + const clampStep = (step: number) => { + return _.clamp(step, 0, proposalSteps.length - 1); + }; /** * Returns true if reset was peformed, false otherwise */ @@ -115,40 +129,40 @@ export default function ProposalContainer(props: { <ProposalInformationView data={proposalInfo} setIsDirty={setIsDirty} + readonly={isSubmitted} /> ) ) ); allProposalSteps = allProposalSteps.concat( - questionary.steps.map( - (step, index, steps) => - new QuestionaryUIStep( - StepType.QUESTIONARY, - step.topic.topic_title, - step.isCompleted, - ( - <ProposalQuestionareStep - topicId={step.topic.topic_id} - data={proposalInfo} - setIsDirty={setIsDirty} - editable={ - (index === 0 && - proposalInfo.status !== ProposalStatus.BLANK) || - step.isCompleted || - (index > 0 && steps[index - 1].isCompleted === true) - } - key={step.topic.topic_id} - /> - ) + questionary.steps.map((step, index, steps) => { + let editable = + (index === 0 && proposalInfo.status !== ProposalStatus.BLANK) || + step.isCompleted || + (steps[index - 1] && steps[index - 1].isCompleted === true); + + return new QuestionaryUIStep( + StepType.QUESTIONARY, + step.topic.topic_title, + step.isCompleted, + ( + <ProposalQuestionareStep + topicId={step.topic.topic_id} + data={proposalInfo} + setIsDirty={setIsDirty} + readonly={!editable || isSubmitted} + key={step.topic.topic_id} + /> ) - ) + ); + }) ); allProposalSteps.push( new QuestionaryUIStep( StepType.REVIEW, "Review", proposalInfo.status === ProposalStatus.SUBMITTED, - <ProposalReview data={proposalInfo} /> + <ProposalReview data={proposalInfo} readonly={isSubmitted} /> ) ); return allProposalSteps; @@ -163,12 +177,13 @@ export default function ProposalContainer(props: { .find(step => step.completed === true); setStepIndex( - Math.max( + _.clamp( + lastFinishedStep ? proposalSteps.indexOf(lastFinishedStep) + 1 : 0, 0, - lastFinishedStep ? proposalSteps.indexOf(lastFinishedStep) + 1 : 0 + proposalSteps.length - 1 ) ); - }, [proposalInfo]); + }, [proposalInfo, isSubmitted]); const getStepContent = (step: number) => { if (!proposalSteps || proposalSteps.length === 0) { @@ -209,8 +224,13 @@ export default function ProposalContainer(props: { /> <FormApi.Provider value={api}> <Paper className={classes.paper}> - <Typography component="h1" variant="h4" align="center"> - {false ? "Update Proposal" : "New Proposal"} + <Typography + component="h1" + variant="h4" + align="center" + className={classes.heading} + > + {proposalInfo.title || "New Proposal"} </Typography> <Stepper nonLinear activeStep={stepIndex} className={classes.stepper}> {proposalSteps.map((step, index, steps) => ( @@ -245,15 +265,7 @@ export default function ProposalContainer(props: { </Step> ))} </Stepper> - {proposalInfo.status !== ProposalStatus.SUBMITTED ? ( - <React.Fragment>{getStepContent(stepIndex)}</React.Fragment> - ) : ( - <React.Fragment> - <Typography variant="h5" gutterBottom> - {false ? "Update Proposal" : "Sent Proposal"} - </Typography> - </React.Fragment> - )} + {getStepContent(stepIndex)} </Paper> </FormApi.Provider> </Container> diff --git a/src/components/ProposalInformationView.js b/src/components/ProposalInformationView.js index 019f15cbd170c13073c2c49f26683e5c32064af5..d09de616bafb1e4379d0ebdf125f843c29d2a731 100644 --- a/src/components/ProposalInformationView.js +++ b/src/components/ProposalInformationView.js @@ -9,6 +9,7 @@ import { useUpdateProposal } from "../hooks/useUpdateProposal"; import ProposalNavigationFragment from "./ProposalNavigationFragment"; import ProposalParticipants from "./ProposalParticipants"; import { useCreateProposal } from "../hooks/useCreateProposal"; +import { makeStyles } from "@material-ui/core" import { UserContext } from "../context/UserContextProvider"; export default function ProposalInformationView(props) { @@ -19,6 +20,13 @@ export default function ProposalInformationView(props) { const [userError, setUserError] = useState(false); const { user } = useContext(UserContext); + const classes = makeStyles({ + disabled: { + pointerEvents: "none", + opacity: 0.7 + } + })(); + return ( <Formik initialValues={{ @@ -34,7 +42,6 @@ export default function ProposalInformationView(props) { if (!id) { ({ id, status } = await createProposal()); } - await updateProposal({ id: id, status: status, @@ -67,7 +74,7 @@ export default function ProposalInformationView(props) { })} > {({ values, errors, touched, handleChange, submitForm }) => ( - <Form> + <Form className={props.readonly ? classes.disabled : undefined}> <Typography variant="h6" gutterBottom> General Information </Typography> @@ -121,7 +128,7 @@ export default function ProposalInformationView(props) { users={users} /> <ProposalNavigationFragment - disabled={props.disabled} + disabled={props.readonly} next={submitForm} isLoading={creatingProposal || updatingProposal} /> diff --git a/src/components/ProposalQuestionareStep.tsx b/src/components/ProposalQuestionareStep.tsx index 76dacdc14b503db9fb5e979e7d04678e4bd9acef..a0a6dfc95bbf7a891eb0656f9fd4f8d0320e5180 100644 --- a/src/components/ProposalQuestionareStep.tsx +++ b/src/components/ProposalQuestionareStep.tsx @@ -32,7 +32,7 @@ export default function ProposalQuestionareStep(props: { data: ProposalInformation; topicId: number; setIsDirty: (isDirty: boolean) => void; - editable: boolean; + readonly: boolean; }) { const { data, topicId } = props; const api = useContext(FormApi); @@ -44,6 +44,10 @@ export default function ProposalQuestionareStep(props: { const classes = makeStyles({ componentWrapper: { margin: "10px 0" + }, + disabled: { + pointerEvents: "none", + opacity: 0.7 } })(); @@ -115,7 +119,7 @@ export default function ProposalQuestionareStep(props: { enableReinitialize={true} > {({ errors, touched, handleChange, submitForm, validateForm }) => ( - <form> + <form className={props.readonly ? classes.disabled : undefined}> {activeFields.map(field => { return ( <div @@ -136,12 +140,12 @@ export default function ProposalQuestionareStep(props: { ); })} <ProposalNavigationFragment - disabled={!props.editable} + disabled={props.readonly} back={() => { submitFormAsync(submitForm, validateForm).then( (isValid: boolean) => { + saveStepData(isValid); if (isValid) { - saveStepData(isValid); (getQuestionaryStepByTopicId( props.data.questionary!, topicId diff --git a/src/components/ProposalQuestionaryReview.tsx b/src/components/ProposalQuestionaryReview.tsx index 57ce139299164fe4ae16ee088f762407740cadac..ae0882f3d19cd41fb975a7caad4c3ce23eec0941 100644 --- a/src/components/ProposalQuestionaryReview.tsx +++ b/src/components/ProposalQuestionaryReview.tsx @@ -1,4 +1,4 @@ -import React, { Fragment } from "react"; +import React, { Fragment, HTMLAttributes } from "react"; import { QuestionaryField } from "../models/ProposalModel"; import { ProposalInformation } from "../models/ProposalModel"; import { getAllFields } from "../models/ProposalModelFunctions"; @@ -11,9 +11,11 @@ import { makeStyles } from "@material-ui/core"; -export default function ProposaQuestionaryReview(props: { - data: ProposalInformation; -}) { +export default function ProposaQuestionaryReview( + props: HTMLAttributes<any> & { + data: ProposalInformation; + } +) { const questionary = props.data.questionary!; if (!props.data) { diff --git a/src/components/ProposalReview.tsx b/src/components/ProposalReview.tsx index 2cf76025d2907cf15df4842158bcf78f47283473..270ffb1e2ed04bdc8988426921e2980556b78c63 100644 --- a/src/components/ProposalReview.tsx +++ b/src/components/ProposalReview.tsx @@ -1,6 +1,5 @@ import React, { useContext } from "react"; import { makeStyles } from "@material-ui/styles"; -import ProposalInformationView from "./ProposalInformationView"; import { FormApi } from "./ProposalContainer"; import { useSubmitProposal } from "../hooks/useSubmitProposal"; import { ProposalStatus } from "../models/ProposalModel"; @@ -15,21 +14,27 @@ const useStyles = makeStyles({ display: "flex", justifyContent: "flex-end" }, + disabled: { + pointerEvents: "none", + opacity: 0.7 + }, button: { marginTop: "30px", marginLeft: "10px", backgroundColor: "#00C851", - color: "#ffff" , + color: "#ffff", "&:hover": { - backgroundColor: "#007E33", - }, + backgroundColor: "#007E33" + } } }); export default function ProposalReview({ - data + data, + readonly }: { data: ProposalInformation; + readonly: boolean; }) { const api = useContext(FormApi); const classes = useStyles(); @@ -38,24 +43,35 @@ export default function ProposalReview({ return ( <> - <ProposaQuestionaryReview data={data} /> + <ProposaQuestionaryReview + data={data} + className={readonly ? classes.disabled : undefined} + /> <div className={classes.buttons}> <ProposalNavigationFragment - back={() => api.back(data)} - backLabel="Back" - next={() => { - submitProposal(data.id).then(isSubmitted => { - data.status = ProposalStatus.SUBMITTED; - api.next(data); - }); - }} + back={undefined} + next={ + readonly + ? undefined + : () => { + submitProposal(data.id).then(isSubmitted => { + data.status = ProposalStatus.SUBMITTED; + api.next(data); + }); + } + } reset={undefined} nextLabel={"Submit"} isLoading={isLoading} disabled={false} /> - <Button className={classes.button} onClick={() => downloadPDFProposal(data.id)}>Download PDF</Button> - + <Button + className={classes.button} + onClick={() => downloadPDFProposal(data.id)} + variant="contained" + > + Download PDF + </Button> </div> </> );