From af85dad61c1daf1dc462b6b11c22ac3607ce0013 Mon Sep 17 00:00:00 2001 From: Yvan CARRE Date: Sat, 7 Dec 2024 01:40:28 +0200 Subject: [PATCH 01/17] journal improvement --- api/app/responses/api.py | 2 +- webapp/src/components/EventStatus.js | 23 +- .../pages/ReponseDetails/ReponseDetails.css | 408 +++++ .../pages/ReponseDetails/ReponseDetails.js | 161 ++ .../ReponseDetails/components/ReviewModal.js | 121 ++ webapp/src/pages/ResponsePage/ResponsePage.js | 1572 +++++++++-------- .../components/ApplicationForm.js | 1051 +++++++---- webapp/src/pages/eventHome/EventHome.js | 24 + .../pages/reviewForm/components/ReviewForm.js | 2 +- .../applicationForm.service.js | 1 + 10 files changed, 2308 insertions(+), 1057 deletions(-) create mode 100644 webapp/src/pages/ReponseDetails/ReponseDetails.css create mode 100644 webapp/src/pages/ReponseDetails/ReponseDetails.js create mode 100644 webapp/src/pages/ReponseDetails/components/ReviewModal.js diff --git a/api/app/responses/api.py b/api/app/responses/api.py index b628b9a7..f222595c 100644 --- a/api/app/responses/api.py +++ b/api/app/responses/api.py @@ -146,7 +146,7 @@ def post(self): user = user_repository.get_by_id(user_id) responses = response_repository.get_all_for_user_application(user_id, application_form_id) - if not application_form.nominations and len(responses) > 0: + if not application_form.nominations and len(responses) < 0: return errors.RESPONSE_ALREADY_SUBMITTED response = Response(application_form_id, user_id, language) diff --git a/webapp/src/components/EventStatus.js b/webapp/src/components/EventStatus.js index 0ed604f9..2616c1a1 100644 --- a/webapp/src/components/EventStatus.js +++ b/webapp/src/components/EventStatus.js @@ -65,6 +65,12 @@ class EventStatus extends Component { applicationStatus = (event) => { const applyLink = `${event.key}/apply` const submissionLink = `${event.key}/apply/new` + const viewLink=`${event.key}/apply/view` + console.log(event); + console.log(event.status.application_status); + console.log(event.key); + + if (event.status.application_status === "Submitted") { if (event.event_type === "JOURNAL") { return { @@ -72,7 +78,7 @@ class EventStatus extends Component { longText: this.props.t("You have submitted your article."), shortText: this.props.t("View Submission(s)"), linkClass: 'btn-secondary', - link: applyLink, + link: viewLink, submissionShortText: this.props.t("New submission"), submissionLinkClass: "btn-primary", submissionLink: submissionLink @@ -91,6 +97,19 @@ class EventStatus extends Component { } else if (event.status.application_status === "Withdrawn") { + if (event.event_type === "JOURNAL"){ + return { + titleClass: "text-success", + longText: this.props.t("You have submitted your article."), + shortText: this.props.t("View Submission(s)"), + linkClass: 'btn-secondary', + link: viewLink, + submissionShortText: this.props.t("New submission"), + submissionLinkClass: "btn-primary", + submissionLink: submissionLink + } + } + else{ return { title: this.props.t("Application Withdrawn"), titleClass: "text-danger", @@ -99,7 +118,7 @@ class EventStatus extends Component { linkClass: "btn-warning", link: applyLink }; - } + }} else if (event.status.application_status === "Not Submitted") { return { title: this.props.t("In Progress"), diff --git a/webapp/src/pages/ReponseDetails/ReponseDetails.css b/webapp/src/pages/ReponseDetails/ReponseDetails.css new file mode 100644 index 00000000..a6617a2d --- /dev/null +++ b/webapp/src/pages/ReponseDetails/ReponseDetails.css @@ -0,0 +1,408 @@ +/* Universal */ +.table-wrapper.response-page { + width: 87vw; + margin-top: 20px; +} + + +.table-wrapper.response-page h2 { + color: hsl(0, 0%, 25%); + font-weight: 300; + margin-top: 2px; + text-align: left; + margin-bottom: 5px; +} + +.response-details h4 { + color: hsl(213deg 16% 49%);; + font-weight: 300; + padding-left: 7px; + margin-top: 5px; + text-align: left; +} + +.table-wrapper.response-page button { + margin-bottom: 10px; +} + +.table-wrapper.response-page label { + font-size: 12px; + color: grey; + margin: 0 1px; +} + + +/* Flex Box Classes */ +.flex { + display: flex; +} + +.baseline { + align-items: baseline; +} + +.center { + justify-content: center; +} + + +/* General and User Details Heading */ +.headings-lower { + display: flex; + width: 100%; + justify-content: space-between; + margin-top: 8px; + flex-wrap: wrap; + padding: 2%; + box-shadow: 1px 2px 6px hsl(0, 0%, 53%); + border-radius: 4px; +} + +.user-details { + padding: 0 2px +} + +.outcome { + display: flex; + flex-direction: column; + align-content: center; + grid-auto-rows: auto; +} + +.user-details.right { + padding: 0 10px +} + +html .headings-lower label { + color: hsl(0deg 0% 55%); +} + +.user-details { + display: flex; + text-align: left; + flex-direction: column; + justify-content: center; +} + + +.user-details.right { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.user-details h4 { + color: hsl(0, 0%, 44%); + margin: 0; +} + +.tags .btn-light { + font-size: 13px; + background: hsl(0deg 0% 100%); + border: 1px solid hsl(0deg, 1%, 74%); +} + +.badge { + margin: 2px; +} + +.badge-info { + background: hsl(188deg, 100%, 23%); + font-size: 14px; +} + +.fa-trash-alt { + padding: 0 5px; +} + +/* Tag List */ +.tag-response { + opacity: 0; + pointer-events: none; + overflow: scroll; + transition: 300ms; + height: 1px; + margin-top: 2px; +} + +.tag-response.show { + opacity: 1; + pointer-events: all; + transition: 300ms; + height: 180px; +} + +.badge.add-tags { + background: hsl(208, 74%, 57%); + color: white; +} + +.badge.add-tags.active { + background: hsl(120, 0%, 57%); +} + +.tag-item { + display: flex; + margin: 5px 0; + opacity: 1; + pointer-events: none; +} + +.btn.tag { + background: none; + border: 1px solid hsl(0, 0%, 75%); + pointer-events: all; + opacity: 1; +} + +.badge-light { + border: 1px solid grey; +} + +.tags { + display: flex; + flex-wrap: wrap; + min-width: 1px; +} + +.tags .btn.badge-primary:hover { + background-color: hsl(0, 64%, 61%); + transition: 300ms; + cursor: pointer; +} + +.tags .btn.badge-add:hover { + background-color: hsl(110, 76%, 31%); + transition: 300ms; + cursor: pointer; +} + +.tags .btn.badge-add { + background: hsl(0deg 0% 32%); + color: hsl(0, 0%, 100%); +} + +/* New Tag Modal */ +.new-tag-inputs { + margin: 5px 0; + display: flex; + flex-direction: column; + width: 100%; +} + +.new-tag-inputs label { + text-align: left; +} + +.new-tag-inputs input { + width: 100%; +} + +/* Reviewers Section */ +.reviewers-section .list { + display: flex; + flex-wrap: wrap; + align-items: center; + margin: 12px 0; +} + +.reviewers-section .list .reviewer { + box-shadow: 1px 1px 5px hsl(0deg, 0%, 75%); + margin-right: 20px; + padding: 3px 15px; + border-radius: 8px; + margin: 10px 5px; + height: 3.5em; +} + +.reviewers-section .list .add-reviewer .btn { + margin: 0 10px; + font-size: 13px; + border: 1px solid hsl(0, 0%, 91%); +} + +.reviewers-section p { + margin: 0 8px 0 0; + font-size: 13px; +} + +.reviewers-section h4 { + font-size: 17px; + font-weight: bold; +} + +.reviewers-section h6 { + font-size: 15px; + color: grey; + margin: 0; + padding-left: 7px; + padding-bottom: 4px; + padding-right: 4px; +} + +.reviewers-section h5 { + margin-top: 11px; + color: grey; +} + +.reviewer div { + display: flex; +} + +.divider { + height: 1px; + background: lightgrey; + width: 100%; + margin: 35px auto; +} + +/* Review Modal */ + +.review-select { + background: none; + border: none; + outline: none; + text-align: left; + box-shadow: 1px 2px 6px hsl(0, 0%, 53%); + cursor: pointer; + border-radius: 10px; + padding: 5px 30px 5px 15px; +} + +.review-select label { + color: hsl(208deg, 84%, 59%) +} + + +.review-select:hover { + background: hsl(0, 0%, 96%); + color: hsl(0, 0%, 19%); +} + +.review-select div { + display: flex; +} + +html button:focus { + border: none; + outline: none; +} + + +.review-select.active { + background:hsl(208deg, 84%, 59%); + border: none; + outline: none; +} + +.trash-review { + background: none; + border: none; + outline: none +} + +.review-select p { +color: hsl(0, 0%, 38%); +font-size: 12px; +margin-right: 8px; +margin-bottom: 4px; +} + +.review-select.active label { +color: white; +font-size: 15px; +} + +.review-select.active p { + color: hsl(0, 0%, 100%); +} + +.review-completed { + color: hsl(120, 41%, 47%); +} + +.review-not-started { + color: hsl(0, 64%, 61%); +} + +.review-started { + color: #e8a64a; +} + + +/* Response Details Section and Sub-Sections */ +.response-details { + margin-top: 20px; + width: 100%; + padding: 2%; + background: white; + box-shadow: 1px 2px 6px hsl(0, 0%, 53%); + min-height: 40px; + padding-bottom: 40px; + text-align: left; + border-radius: 4px; +} + +.section { + padding: 10px 0; +} + +.review-select .reviewer-email { + color: grey; + font-size: 12px; + margin-bottom: 0.75em; +} + +/* Q&A Sections */ +.Q-A { + display: flex; + flex-wrap: wrap; +} + +.question-answer-block { + width: 100%; + padding: 10px; + border: 1px solid #e8e8e8; + margin: 5px; + background: hsl(0deg 0% 98%); + border-radius: 2px; +} + +.question-answer-block p { + color: grey; + font-size: 17px; +} + +.question-answer-block p.answer { + color: #212529; + font-size: 17px; + white-space: pre-wrap; +} + + html .answerer { + font-weight: bold; + font-size: 13px; +} + +.answer.file { + color: skyblue; + text-decoration: underline; +} + + +/* Response Details Type Face */ +.response-details h3 { + color: hsl(213deg 16% 49%); + margin: 0; + padding-left: 7px; + padding-bottom: 4px; + padding-right: 4px; +} + +.question-answer-block .question-description { + font-size: 0.9em; +} + +.question-answer-block .question-headline { + color: #666; +} \ No newline at end of file diff --git a/webapp/src/pages/ReponseDetails/ReponseDetails.js b/webapp/src/pages/ReponseDetails/ReponseDetails.js new file mode 100644 index 00000000..ef022728 --- /dev/null +++ b/webapp/src/pages/ReponseDetails/ReponseDetails.js @@ -0,0 +1,161 @@ +import React, { Component } from "react"; +import { withRouter } from "react-router"; +import { Link } from "react-router-dom"; +import { applicationFormService } from "../../services/applicationForm"; + +import Loading from "../../components/Loading"; +import _ from "lodash"; +import { withTranslation } from "react-i18next"; +import AnswerValue from "../../components/answerValue"; +import { eventService } from "../../services/events"; + +class ReponseDetails extends Component { + constructor(props) { + super(props); + this.state = { + applicationData: null, + applicationForm: null, + isLoading: true, + }; + } + + componentDidMount() { + const eventId = this.props.event ? this.props.event.id : 0; + Promise.all([ + applicationFormService.getForEvent(eventId), + applicationFormService.getResponse(eventId), + eventService.getEvent(eventId), + ]).then((responses) => { + console.log(responses); + this.setState({ + applicationForm: responses[0].formSpec, + applicationData: responses[1].response.find( + (item) => item.id === parseInt(this.props.match.params.id) + ), + isLoading: false, + }); + }); + } + + renderSections() { + const applicationForm = this.state.applicationForm; + const applicationData = this.state.applicationData; + let html = []; + + // main function + if (applicationForm && applicationData) { + applicationForm.sections.forEach((section) => { + html.push( +
+ {/*Heading*/} +
+

{section.name}

+
+ {/*Q & A*/} +
{this.renderResponses(section)}
+
+ ); + }); + } + + return html; + } + + renderResponses(section) { + const applicationData = this.state.applicationData; + const questions = section.questions.map((q) => { + const a = applicationData.answers.find((a) => a.question_id === q.id); + if (q.type === "information") { + return

{q.headline}

; + } + return ( +
+

+ {q.headline} + {q.description && ( + +
+ {q.description} +
+ )} +

+
+ +
+
+ ); + }); + return questions; + } + goBack() { + this.props.history.goBack(); + } + + render() { + const { applicationData, isLoading } = this.state; + if (isLoading) { + return ; + } + + return ( +
+ {/* API Error + {error && +
+

{JSON.stringify(error)}

+
+ } + + {this.renderReviewerModal()} + {this.renderDeleteTagModal()} + {this.renderDeleteReviewerModal()} */} + + {/* Headings */} + {/* {applicationData && +
+
+

{applicationData.user_title} {applicationData.firstname} {applicationData.lastname}

+

{t("Language")}: {applicationData.language}

+
+ {this.renderTags()} + this.setTagSelectorVisible()}>{t("Add tag")} + +
+ +
+ + {/* User details Right Tab */} + {/*
+
+

{this.applicationStatus()}

+

{this.outcomeStatus()}

+ +
+ +
+
+ } */} + + {/*Response Data*/} + {applicationData && ( +
+ + {this.renderSections()} +
+
+ +
+ +
+ )} +
+ ); + } +} + +export default withRouter(withTranslation()(ReponseDetails)); diff --git a/webapp/src/pages/ReponseDetails/components/ReviewModal.js b/webapp/src/pages/ReponseDetails/components/ReviewModal.js new file mode 100644 index 00000000..bbd33c2f --- /dev/null +++ b/webapp/src/pages/ReponseDetails/components/ReviewModal.js @@ -0,0 +1,121 @@ +import React, { Component } from "react"; + +class ReviewModal extends Component { + constructor(props) { + super(props); + this.state = { + selectedReviewer: null, + }; + } + + handlePost(reviewers) { + this.props.handlePost(reviewers); + + this.setState({ + selectedReviewer: null, + }); + } + + handleSelect(reviewer) { + this.setState({ + selectedReviewer: reviewer, + }); + } + + renderModal() { + const { selectedReviewer } = this.state; + + const { reviewers, t } = this.props; + + if (this.props.event) { + return ( + + ); + } + } + + render() { + const modal = this.renderModal(); + return
{modal}
; + } +} + +export default ReviewModal; diff --git a/webapp/src/pages/ResponsePage/ResponsePage.js b/webapp/src/pages/ResponsePage/ResponsePage.js index d91af183..d4d63ea0 100644 --- a/webapp/src/pages/ResponsePage/ResponsePage.js +++ b/webapp/src/pages/ResponsePage/ResponsePage.js @@ -1,735 +1,917 @@ -import React, { Component } from 'react' +import React, { Component } from "react"; import "react-table/react-table.css"; -import './ResponsePage.css' -import { withTranslation } from 'react-i18next'; -import ReviewModal from './components/ReviewModal'; -import { eventService } from '../../services/events/events.service'; -import { applicationFormService } from '../../services/applicationForm/applicationForm.service'; -import { reviewService } from '../../services/reviews/review.service'; -import { outcomeService } from '../../services/outcome/outcome.service'; -import { tagsService } from '../../services/tags/tags.service'; -import { responsesService } from '../../services/responses/responses.service'; +import "./ResponsePage.css"; +import { withTranslation } from "react-i18next"; +import ReviewModal from "./components/ReviewModal"; +import { eventService } from "../../services/events/events.service"; +import { applicationFormService } from "../../services/applicationForm/applicationForm.service"; +import { reviewService } from "../../services/reviews/review.service"; +import { outcomeService } from "../../services/outcome/outcome.service"; +import { tagsService } from "../../services/tags/tags.service"; +import { responsesService } from "../../services/responses/responses.service"; import AnswerValue from "../../components/answerValue"; import { ConfirmModal } from "react-bootstrap4-modal"; -import moment from 'moment' -import { getDownloadURL } from '../../utils/files'; -import TagSelectorDialog from '../../components/TagSelectorDialog'; +import moment from "moment"; +import { getDownloadURL } from "../../utils/files"; +import TagSelectorDialog from "../../components/TagSelectorDialog"; +import Loading from "../../components/Loading"; class ResponsePage extends Component { - constructor(props) { - super(props); - - this.assignable_tag_types = ["RESPONSE"] - - this.state = { - error: false, - eventLanguages: [], - isLoading: true, - removeTagModalVisible: false, - removeReviewerModalVisible: false, - tagToRemove: null, - reviewToRemove: null, - tagSelectorVisible : false, - filteredTagList: [], - tagList: [], - assignableTagTypes: ["RESPONSE"], - reviewResponses: [], - outcome: {'status':null,'timestamp':null}, - } + constructor(props) { + super(props); + + this.assignable_tag_types = ["RESPONSE"]; + + this.state = { + error: false, + eventLanguages: [], + isLoading: true, + removeTagModalVisible: false, + removeReviewerModalVisible: false, + tagToRemove: null, + reviewToRemove: null, + tagSelectorVisible: false, + filteredTagList: [], + tagList: [], + assignableTagTypes: ["RESPONSE"], + reviewResponses: [], + outcome: { status: null, timestamp: null }, + confirmModalVisible: false, + pendingOutcome: "", + confirmationMessage: "", }; + } + + componentDidMount() { + Promise.all([ + eventService.getEvent(this.props.event.id), + applicationFormService.getForEvent(this.props.event.id), + responsesService.getResponseDetail( + this.props.match.params.id, + this.props.event.id + ), + tagsService.getTagList(this.props.event.id), + reviewService.getReviewAssignments(this.props.event.id), + ]).then((responses) => { + this.setState( + { + eventLanguages: responses[0].event + ? Object.keys(responses[0].event.name) + : null, + event_type: responses[0].event.event_type, + applicationForm: responses[1].formSpec, + applicationData: responses[2].detail, + tagList: responses[3].tags, + availableReviewers: responses[4].reviewers, + error: + responses[0].error || + responses[1].error || + responses[2].error || + responses[3].error || + responses[4].error, + }, + () => { + this.getOutcome(); + this.getReviewResponses(responses[2].detail); + this.filterTagList(); + } + ); + }); + } + + getReviewResponses(applicationData) { + reviewService + .getResponseReviewAdmin(applicationData.id, this.props.event.id) + .then((resp) => { + if (resp.error) { + this.setState({ + error: resp.error, + }); + } + if (resp.form) { + this.setState({ + reviewResponses: resp.form.review_responses, + reviewForm: resp.form.review_form, + isLoading: false, + error: resp.error, + }); + } + }); + } - componentDidMount() { - - Promise.all([ - eventService.getEvent(this.props.event.id), - applicationFormService.getForEvent(this.props.event.id), - responsesService.getResponseDetail(this.props.match.params.id, this.props.event.id), - tagsService.getTagList(this.props.event.id), - reviewService.getReviewAssignments(this.props.event.id) - ]).then(responses => { - this.setState({ - eventLanguages: responses[0].event ? Object.keys(responses[0].event.name) : null, - event_type: responses[0].event.event_type, - applicationForm: responses[1].formSpec, - applicationData: responses[2].detail, - tagList: responses[3].tags, - availableReviewers: responses[4].reviewers, - error: responses[0].error || responses[1].error || responses[2].error || responses[3].error || responses[4].error, - }, () => { - this.getOutcome(); - this.getReviewResponses(responses[2].detail); - this.filterTagList(); - }); - }); - }; + // Misc Functions - getReviewResponses(applicationData) { - reviewService.getResponseReviewAdmin(applicationData.id, this.props.event.id) - .then(resp => { - if (resp.error) { - this.setState({ - error: resp.error - }); - } - if(resp.form) { - this.setState( { - reviewResponses: resp.form.review_responses, - reviewForm: resp.form.review_form, - isLoading: false, - error: resp.error - }); - } - }); - } + goBack() { + this.props.history.goBack(); + } - // Misc Functions + // Render Page HTML + // Generate Applciation Status - goBack() { - this.props.history.goBack(); - }; + formatDate = (dateString) => { + return moment(dateString).format("D MMM YYYY, H:mm:ss [(UTC)]"); + }; - // Render Page HTML - // Generate Applciation Status + applicationStatus() { + const data = this.state.applicationData; + if (data) { + const unsubmitted = !data.is_submitted && !data.is_withdrawn; + const submitted = data.is_submitted; + const withdrawn = data.is_withdrawn; - formatDate = (dateString) => { - return moment(dateString).format('D MMM YYYY, H:mm:ss [(UTC)]'); + if (unsubmitted) { + return ( + + Unsubmitted{" "} + {this.formatDate(data.started_timestamp)} + + ); + } + if (submitted) { + return ( + + Submitted{" "} + {this.formatDate(data.started_timestamp)} + + ); + } + if (withdrawn) { + return ( + + Withdrawn{" "} + {this.formatDate(data.started_timestamp)} + + ); + } } + } - applicationStatus() { - const data = this.state.applicationData; - if (data) { - const unsubmitted = !data.is_submitted && !data.is_withdrawn; - const submitted = data.is_submitted; - const withdrawn = data.is_withdrawn; - - if (unsubmitted) { - return Unsubmitted {this.formatDate(data.started_timestamp)} - }; - if (submitted) { - return Submitted {this.formatDate(data.started_timestamp)} - }; - if (withdrawn) { - return Withdrawn {this.formatDate(data.started_timestamp)} - }; - }; - }; - - renderReviewResponse(review_response, section) { - const questions = section.review_questions.map(q => { - const a = review_response.scores.find(a => a.review_question_id === q.id); + renderReviewResponse(review_response, section) { + const questions = section.review_questions.map((q) => { + const a = review_response.scores.find( + (a) => a.review_question_id === q.id + ); - if (a) { - return
+ if (a) { + return ( +
-

{q.headline} - {q.description && a &&
{q.description}
} -

-
+

+ {q.headline} + {q.description && a && ( + +
+ {q.description} +
+ )} +

+
+ +
+
+ ); + } + }); + return questions; + } + + // Render Reviews + renderCompleteReviews() { + if (this.state.reviewResponses.length) { + const reviews = this.state.reviewResponses.map((val) => { + return ( +
+

+ {this.props.t( + val.reviewer_user_firstname + " " + val.reviewer_user_lastname + )} +

+ {this.renderReviewResponse( + val, + this.state.reviewForm.review_sections[0] + )} +
+ ); + }); + return reviews; + } + } + + getOutcome() { + outcomeService + .getOutcome(this.props.event.id, this.state.applicationData.user_id) + .then((response) => { + if (response.status === 200) { + const newOutcome = { + timestamp: response.outcome.timestamp, + status: response.outcome.status, + }; + this.setState({ + outcome: newOutcome, + isLoading: false, + }); + } else { + this.setState({ + error: response.error, + isLoading: false, + }); + } + }); + } + + submitOutcome(selectedOutcome) { + outcomeService + .assignOutcome( + this.state.applicationData.user_id, + this.props.event.id, + selectedOutcome + ) + .then((response) => { + if (response.status === 201) { + const newOutcome = { + timestamp: response.outcome.timestamp, + status: response.outcome.status, + }; + + this.setState({ + outcome: newOutcome, + confirmModalVisible: false, + }); + } else { + this.setState({ erorr: response.error }); + } + }); + } + + openConfirmationModal = (outcome, message) => { + this.setState({ + confirmModalVisible: true, + pendingOutcome: outcome, + confirmationMessage: message, + }); + }; + + handleConfirmation = (outcome, message) => { + this.setState({ + confirmModalVisible: true, + pendingOutcome: outcome, + confirmationMessage: message, + }); + }; + + handleConfirmationOK = (event) => { + this.setState({ + confirmModalVisible: false, + }); + this.submitOutcome(this.state.pendingOutcome); + }; + + handleConfirmationCancel = (event) => { + this.setState({ + confirmModalVisible: false, + }); + }; + + renderConfirmationButton(outcome, label, className, message) { + return ( + + ); + } + + outcomeStatus() { + const data = this.state.applicationData; + + if (data) { + if (this.state.outcome.status && this.state.outcome.status !== "REVIEW") { + const badgeClass = + this.state.outcome.status === "ACCEPTED" + ? "badge-success" + : this.state.outcome.status === "REJECTED" + ? "badge-danger" + : "badge-warning"; + + return ( + + + {this.state.outcome.status} + {" "} + {this.formatDate(this.state.outcome.timestamp)} + + ); + } + + const { event_type } = this.state; + const buttons = []; + + if (event_type === "JOURNAL" || event_type === "CALL" || event_type === "EVENT") { + buttons.push( + this.renderConfirmationButton( + "ACCEPTED", + "Accept", + "btn-success", + "Are you sure you want to ACCEPT this submission?" + ) + ); + + if (event_type === "JOURNAL") { + buttons.push( + this.renderConfirmationButton( + "ACCEPT_W_REVISION", + "Accept with Minor Revision", + "btn-warning", + "Are you sure you want to ACCEPT WITH MINOR REVISION?" + ) + ); + buttons.push( + this.renderConfirmationButton( + "REJECT_W_ENCOURAGEMENT", + "Reject with Encouragement to Resubmit", + "btn-warning", + "Are you sure you want to REJECT WITH ENCOURAGEMENT TO RESUBMIT??" + ) + ); + } + + buttons.push( + this.renderConfirmationButton( + "REJECTED", + "Reject", + "btn-danger", + "Are you sure you want to REJECT this submission?" + ) + ); + + if (event_type === "EVENT") { + buttons.push( + this.renderConfirmationButton( + "WAITLIST", + "Waitlist", + "btn-warning", + "Are you sure you want to WAITLIST this submission?" + ) + ); + } + } + + return ( +
+ {buttons.map((button, index) => ( +
+ {button}
- } - - - }); - return questions - }; - - // Render Reviews - renderCompleteReviews(){ - if (this.state.reviewResponses.length) { - const reviews = this.state.reviewResponses.map(val => { - return
-

- {this.props.t(val.reviewer_user_firstname + " " + val.reviewer_user_lastname)} -

- {this.renderReviewResponse(val, this.state.reviewForm.review_sections[0])} -
- }); - return reviews - } + ))} + +

{this.props.t(this.state.confirmationMessage)}

+
+
+ ); } - - getOutcome() { - outcomeService.getOutcome(this.props.event.id, this.state.applicationData.user_id).then(response => { - if (response.status === 200) { - const newOutcome = { - timestamp: response.outcome.timestamp, - status: response.outcome.status, - }; - this.setState( - { - outcome: newOutcome, - isLoading: false - } - ); - } - else{ - this.setState({ - error: response.error, - isLoading: false - }); - } - }); - }; - - submitOutcome(selectedOutcome) { - outcomeService.assignOutcome(this.state.applicationData.user_id, this.props.event.id, selectedOutcome).then(response => { - if (response.status === 201) { - const newOutcome = { - timestamp: response.outcome.timestamp, - status: response.outcome.status, - }; - - this.setState({ - outcome: newOutcome - }); - } else { - this.setState({erorr: response.error}); - } - }); + } + + + // Render Sections + renderSections() { + const applicationForm = this.state.applicationForm; + const applicationData = this.state.applicationData; + + let html = []; + + // main function + if (applicationForm && applicationData) { + applicationForm.sections.forEach((section) => { + html.push( +
+ {/*Heading*/} +
+

{section.name}

+
+ {/*Q & A*/} +
{this.renderResponses(section)}
+
+ ); + }); } - outcomeStatus() { - const data = this.state.applicationData; - - if (data) { - - if (this.state.outcome.status && this.state.outcome.status !== 'REVIEW') { - if (this.state.outcome.status === 'ACCEPTED') { - return {this.state.outcome.status} {this.formatDate(this.state.outcome.timestamp)} - } else if (this.state.outcome.status === 'REJECTED') { - return {this.state.outcome.status} {this.formatDate(this.state.outcome.timestamp)} - } else { - return {this.state.outcome.status} {this.formatDate(this.state.outcome.timestamp)} - } - }; - - if (this.state.event_type === 'JOURNAL') { - return
-
- -
-
- -
-
- -
-
- -
-
- } - else if (this.state.event_type === 'CALL') { - return
-
- -
-
- -
-
- } - else if (this.state.event_type === 'EVENT') { - return
-
- -
-
- -
-
- } - }; - }; - - // Render Sections - renderSections() { - const applicationForm = this.state.applicationForm; - const applicationData = this.state.applicationData; - let html = []; - - // main function - if (applicationForm && applicationData) { - applicationForm.sections.forEach(section => { - html.push(
- { /*Heading*/} -

{section.name}

- { /*Q & A*/} -
- {this.renderResponses(section)} -
-
) - }); - }; - - return html - }; - - filterTagList() { - const tagList = this.state.tagList - const responseTagList = tagList.filter(t => this.state.assignableTagTypes.includes(t.tag_type)); - const applicationData = this.state.applicationData; - const filteredTagList = responseTagList.filter(tag => { - return !applicationData.tags.some(t => t.id === tag.id) - }); - this.setState({ - filteredTagList: filteredTagList - }); - }; - - // Render questions and answers - renderResponses(section) { - const applicationData = this.state.applicationData; - const questions = section.questions.map(q => { - const a = applicationData.answers.find(a => a.question_id === q.id); - if (q.type === "information") { - return

{q.headline}

- }; - return
-

{q.headline} - {q.description &&
{q.description}
} -

-
+ return html; + } + + filterTagList() { + const tagList = this.state.tagList; + const responseTagList = tagList.filter((t) => + this.state.assignableTagTypes.includes(t.tag_type) + ); + const applicationData = this.state.applicationData; + const filteredTagList = responseTagList.filter((tag) => { + return !applicationData.tags.some((t) => t.id === tag.id); + }); + this.setState({ + filteredTagList: filteredTagList, + }); + } + + // Render questions and answers + renderResponses(section) { + const applicationData = this.state.applicationData; + const questions = section.questions.map((q) => { + const a = applicationData.answers.find((a) => a.question_id === q.id); + if (q.type === "information") { + return

{q.headline}

; + } + return ( +
+

+ {q.headline} + {q.description && ( + +
+ {q.description} +
+ )} +

+
+ +
+
+ ); + }); + return questions; + } + + // Render Answers + renderAnswer(id, type, headline, options) { + const applicationData = this.state.applicationData; + const baseUrl = process.env.REACT_APP_API_URL; + + const a = applicationData.answers.find((a) => a.question_id === id); + if (!a) { + return this.props.t("No answer provided."); + } else { + // file + if (type == "file") { + return ( + + {this.props.t("Uploaded File")} + + ); + } + // multi-file + else if (type == "multi-file") { + const answerFiles = JSON.parse(a.value); + let files = []; + if (Array.isArray(answerFiles) && answerFiles.length > 0) { + files = answerFiles.map((file) => ( +
+ + {file.name} +
+ )); + } else { + files = "No files uploaded"; + } + return
{files}
; + } + // choice + else if (type.includes("choice")) { + let choices = []; + options.forEach((opt) => { + if (a.value == opt.value) { + choices.push(
{opt.label}
); + } }); - return questions - }; - - // Render Answers - renderAnswer(id, type, headline, options) { - const applicationData = this.state.applicationData; - const baseUrl = process.env.REACT_APP_API_URL; - - const a = applicationData.answers.find(a => a.question_id === id); - if (!a) { - return this.props.t("No answer provided."); + return
{choices}
; + } + // other + else { + return ( +
+

{a.value}

+
+ ); + } + } + } + + // render Delete Modal + renderDeleteTagModal() { + const t = this.props.t; + + return ( + +

{t("Are you sure you want to remove this tag?")}

+
+ ); + } + + onSelectTag = (tag) => { + responsesService + .tagResponse(this.state.applicationData.id, tag.id, this.props.event.id) + .then((resp) => { + if (resp.status === 201) { + this.setState( + { + tagSelectorVisible: false, + applicationData: { + ...this.state.applicationData, + tags: [...this.state.applicationData.tags, tag], + }, + }, + this.filterTagList + ); + } else { + this.setState({ + tagSelectorVisible: false, + error: resp.error, + }); } - else { - // file - if (type == "file") { - return {this.props.t("Uploaded File")} - } - // multi-file - else if (type == "multi-file") { - const answerFiles = JSON.parse(a.value); - let files = []; - if (Array.isArray(answerFiles) && answerFiles.length > 0) { - files = answerFiles.map(file =>
{file.name}
) - } - else { - files = "No files uploaded"; - } - return
{files}
- } - // choice - else if (type.includes("choice")) { - let choices = []; - options.forEach(opt => { - if (a.value == opt.value) { - choices.push(
{opt.label}
) - }; - }); - return
{choices}
- } - // other - else { - return

{a.value}

- }; - }; - }; - - // render Delete Modal - renderDeleteTagModal() { - const t = this.props.t; - - return -

- {t('Are you sure you want to remove this tag?')} -

-
- - }; - - onSelectTag = (tag) => { - responsesService.tagResponse(this.state.applicationData.id, tag.id, this.props.event.id) - .then(resp => { - if (resp.status === 201) { - this.setState({ - tagSelectorVisible: false, - applicationData: { - ...this.state.applicationData, - tags: [...this.state.applicationData.tags, tag] - }}, this.filterTagList); - } - else { - this.setState({ - tagSelectorVisible: false, - error: resp.error - }); - } - }) + }); + }; + + // Del Tag + removeTag(tag_id) { + this.setState({ + removeTagModalVisible: true, + tagToRemove: tag_id, + }); + } + + confirmRemoveTag = () => { + const { applicationData, tagToRemove } = this.state; + + responsesService + .removeTag(applicationData.id, tagToRemove, this.props.event.id) + .then((resp) => { + if (resp.status === 200) { + this.setState( + { + removeTagModalVisible: false, + applicationData: { + ...this.state.applicationData, + tags: this.state.applicationData.tags.filter( + (t) => t.id !== tagToRemove + ), + }, + }, + this.filterTagList + ); + } else { + this.setState({ + error: resp.error, + confirmRemoveTagVisible: false, + }); + } + }); + }; + + cancelRemoveTag = () => { + this.setState({ + tagToRemove: null, + removeTagModalVisible: false, + }); + }; + + renderTags() { + const data = this.state.applicationData; + + if (data) { + let tags = data.tags + ? data.tags.map((tag) => { + return ( + this.removeTag(tag.id)} + key={`tag_${tag.id}`} + > + {tag.name} + + ); + }) + : null; + return tags; } - - // Del Tag - removeTag(tag_id) { - this.setState({ - removeTagModalVisible: true, - tagToRemove: tag_id - }); - }; - - confirmRemoveTag = () => { - const { applicationData, tagToRemove } = this.state; - - responsesService.removeTag(applicationData.id, tagToRemove, this.props.event.id) - .then(resp => { - console.log(resp); - if (resp.status === 200) { - this.setState({ - removeTagModalVisible: false, - applicationData: { - ...this.state.applicationData, - tags: this.state.applicationData.tags.filter(t => t.id !== tagToRemove) - }}, this.filterTagList); - } - else { - this.setState({ - error: resp.error, - confirmRemoveTagVisible: false - }); + } + + // Reviews + // Render Reviews + renderReviews() { + if (this.state.applicationData) { + if (this.state.applicationData.reviewers) { + const reviews = this.state.applicationData.reviewers.map( + (val, index) => { + let num = index + 1; + // {"reviewer_user_id": 4, "user_title": "Mr", "firstname": "Joe", "lastname": "Soap", "status": "completed"}, + return ( +
+ +
+

+ {val.user_title} {val.firstname} {val.lastname} +

+ + {val.status === "completed" && ( +

+ {this.props.t("Completed")} +

+ )} + {val.status === "started" && ( +

+ {this.props.t("In Progress")} +

+ )} + {val.status === "not_started" && ( +

+ {this.props.t("Not Started")} + +

+ )} +
+
+ ); } - }) - } + ); - cancelRemoveTag = () => { - this.setState({ - tagToRemove: null, + return reviews; + } + } + } + + // Remove Reviewer + removeReview(reviewer_user_id) { + this.setState({ + removeReviewerModalVisible: true, + reviewToRemove: reviewer_user_id, + }); + } + + removeReviewerService() { + const { applicationData, reviewToRemove } = this.state; + const { event } = this.props; + + reviewService + .deleteResponseReviewer(event.id, applicationData.id, reviewToRemove) + .then((response) => { + if (response.error) { + this.setState({ + error: response.error, removeTagModalVisible: false, - }); - }; - - renderTags() { - const data = this.state.applicationData; - - if (data) { - let tags = data.tags ? data.tags.map(tag => { - return this.removeTag(tag.id)} key={`tag_${tag.id}`}>{tag.name} - }) - : - null - return tags - }; - }; - - // Reviews - // Render Reviews - renderReviews() { - - if (this.state.applicationData) { - if (this.state.applicationData.reviewers) { - const reviews = this.state.applicationData.reviewers.map((val, index) => { - let num = index + 1; - // {"reviewer_user_id": 4, "user_title": "Mr", "firstname": "Joe", "lastname": "Soap", "status": "completed"}, - return
- -
-

{val.user_title} {val.firstname} {val.lastname}

- - {val.status === "completed" &&

{this.props.t("Completed")}

} - {val.status === "started" &&

{this.props.t("In Progress")}

} - {val.status === "not_started" && -

- {this.props.t("Not Started")} - -

- } -
-
- }); - - return reviews - }; - } - - }; - - // Remove Reviewer - removeReview(reviewer_user_id) { - this.setState({ - removeReviewerModalVisible: true, - reviewToRemove: reviewer_user_id - }); - }; - - removeReviewerService() { - const { applicationData, reviewToRemove } = this.state; - const { event } = this.props; - - reviewService.deleteResponseReviewer(event.id, applicationData.id, reviewToRemove) - .then(response => { - if (response.error) { - this.setState({ - error: response.error, - removeTagModalVisible: false, - removeReviewerModalVisible: false, - tagToRemove: null, - reviewToRemove: null - }) - } else { - const newReviewers = applicationData.reviewers.filter(r => r === null || r.reviewer_user_id !== reviewToRemove); - - this.setState({ - applicationData: { - ...applicationData, - reviewers: newReviewers - }, - removeReviewerModalVisible: false - }); - } - }) - }; - - cancelRemoveReviewer() { - this.setState({ removeReviewerModalVisible: false, - reviewToRemove: null - }); - }; - - postReviewerService(reviewer) { - const { applicationData } = this.state; - - reviewService.assignResponsesToReviewer(this.props.event.id, [applicationData.id], reviewer.email) - .then(response => { - if (response.error) { - this.error(response.error); - } - else { - const newReviewer = { - "reviewer_user_id": reviewer.reviewer_user_id, - "user_title": reviewer.user_title, - "firstname": reviewer.firstname, - "lastname": reviewer.lastname, - "completed": false - } - - const newReviewers = [...applicationData.reviewers, newReviewer]; - - this.setState({ - applicationData: { - ...applicationData, - reviewers: newReviewers - } - }); - } - }); - }; - - // Render Review Modal - renderReviewerModal() { - if (!this.state.reviewResponses || !this.state.applicationData) { - return
+ tagToRemove: null, + reviewToRemove: null, + }); + } else { + const newReviewers = applicationData.reviewers.filter( + (r) => r === null || r.reviewer_user_id !== reviewToRemove + ); + + this.setState({ + applicationData: { + ...applicationData, + reviewers: newReviewers, + }, + removeReviewerModalVisible: false, + }); } - return < ReviewModal - handlePost={(data) => this.postReviewerService(data)} - response={this.state.applicationData} - reviewers={this.state.availableReviewers.filter(r => !this.state.applicationData.reviewers.some(rr => rr.reviewer_user_id === r.reviewer_user_id))} - event={this.props.event} - t={this.props.t} - /> - }; - - renderDeleteReviewerModal() { - const t = this.props.t; - - return this.removeReviewerService(e)} - onCancel={(e) => this.cancelRemoveReviewer(e)} - okText={t("Yes")} - cancelText={t("No")}> -

- {t('Are you sure you want to delete this reviewer?')} -

-
- }; + }); + } + + cancelRemoveReviewer() { + this.setState({ + removeReviewerModalVisible: false, + reviewToRemove: null, + }); + } + + postReviewerService(reviewer) { + const { applicationData } = this.state; + + reviewService + .assignResponsesToReviewer( + this.props.event.id, + [applicationData.id], + reviewer.email + ) + .then((response) => { + if (response.error) { + this.error(response.error); + } else { + const newReviewer = { + reviewer_user_id: reviewer.reviewer_user_id, + user_title: reviewer.user_title, + firstname: reviewer.firstname, + lastname: reviewer.lastname, + completed: false, + }; + + const newReviewers = [...applicationData.reviewers, newReviewer]; + + this.setState({ + applicationData: { + ...applicationData, + reviewers: newReviewers, + }, + }); + } + }); + } - addTag = (response) => { - console.log(response); - const tagIds = response.tags.map(t=>t.id); - this.setState({ - tagSelectorVisible: true, - filteredTags: this.state.tags.filter(t=>!tagIds.includes(t.id)) - }) + // Render Review Modal + renderReviewerModal() { + if (!this.state.reviewResponses || !this.state.applicationData) { + return
; } - - setTagSelectorVisible = () => { - this.setState({ - tagSelectorVisible: true - }) + return ( + this.postReviewerService(data)} + response={this.state.applicationData} + reviewers={this.state.availableReviewers.filter( + (r) => + !this.state.applicationData.reviewers.some( + (rr) => rr.reviewer_user_id === r.reviewer_user_id + ) + )} + event={this.props.event} + t={this.props.t} + /> + ); + } + + renderDeleteReviewerModal() { + const t = this.props.t; + + return ( + this.removeReviewerService(e)} + onCancel={(e) => this.cancelRemoveReviewer(e)} + okText={t("Yes")} + cancelText={t("No")} + > +

{t("Are you sure you want to delete this reviewer?")}

+
+ ); + } + + addTag = (response) => { + const tagIds = response.tags.map((t) => t.id); + this.setState({ + tagSelectorVisible: true, + filteredTags: this.state.tags.filter((t) => !tagIds.includes(t.id)), + }); + }; + + setTagSelectorVisible = () => { + this.setState({ + tagSelectorVisible: true, + }); + }; + + render() { + const { applicationData, error, isLoading } = this.state; + // Translation + const t = this.props.t; + + if (isLoading) { + return ; } - render() { - const { applicationData, error, isLoading } = this.state; - // Translation - const t = this.props.t; + return ( +
+ {/* API Error */} + {error && ( +
+

{JSON.stringify(error)}

+
+ )} + + {this.renderReviewerModal()} + {this.renderDeleteTagModal()} + {this.renderDeleteReviewerModal()} + + {/* Headings */} + {applicationData && ( +
+
+

+ {applicationData.user_title} {applicationData.firstname}{" "} + {applicationData.lastname} +

+

+ {t("Language")}: {applicationData.language} +

+
+ {this.renderTags()} + this.setTagSelectorVisible()} + > + {t("Add tag")} + +
+
- if (isLoading) { - return ( -
-
- Loading... + {/* User details Right Tab */} +
+
+ {" "} +

{this.applicationStatus()}

+ {" "} +

{this.outcomeStatus()}

+ +
+
+
+ )} + + {/*Response Data*/} + {applicationData && ( +
+ {/* Reviews */} +
+

{t("Reviewers")}

+
+ {this.renderReviews()} +
+
- ); - } - - return ( -
- {/* API Error */} - {error && -
-

{JSON.stringify(error)}

-
- } - - {this.renderReviewerModal()} - {this.renderDeleteTagModal()} - {this.renderDeleteReviewerModal()} - - {/* Headings */} - {applicationData && -
-
-

{applicationData.user_title} {applicationData.firstname} {applicationData.lastname}

-

{t("Language")}: {applicationData.language}

-
- {this.renderTags()} - this.setTagSelectorVisible()}>{t("Add tag")} - -
- -
- - {/* User details Right Tab */} -
-
-

{this.applicationStatus()}

-

{this.outcomeStatus()}

- -
- -
-
- } - - - {/*Response Data*/} - {applicationData && -
- {/* Reviews */} -
-

{t('Reviewers')}

-
- {this.renderReviews()} -
- -
-
-
-
- - {this.renderSections()} - { - this.state.reviewResponses.length > 0 && ( -
-
-
-

{t('Reviewer Feedback')}

-
{t('Only feedback for the active review stage is shown below.')}
- {this.renderCompleteReviews()} -
-
- - )} -
- } - - this.setState({ tagSelectorVisible: false })} - onSelectTag={this.onSelectTag} - /> +
- ) - }; -}; + + {this.renderSections()} + {this.state.reviewResponses.length > 0 && ( +
+
+
+

{t("Reviewer Feedback")}

+
+ {t( + "Only feedback for the active review stage is shown below." + )} +
+ {this.renderCompleteReviews()} +
+
+ )} +
+ )} + + this.setState({ tagSelectorVisible: false })} + onSelectTag={this.onSelectTag} + /> +
+ ); + } +} export default withTranslation()(ResponsePage); diff --git a/webapp/src/pages/applicationForm/components/ApplicationForm.js b/webapp/src/pages/applicationForm/components/ApplicationForm.js index c20ed56a..f0c54c2d 100644 --- a/webapp/src/pages/applicationForm/components/ApplicationForm.js +++ b/webapp/src/pages/applicationForm/components/ApplicationForm.js @@ -7,7 +7,7 @@ import FormSelect from "../../../components/form/FormSelect"; import FormTextArea from "../../../components/form/FormTextArea"; import FormDate from "../../../components/form/FormDate"; import MarkdownRenderer from "../../../components/MarkdownRenderer"; -import FormMultiFile from '../../../components/form/FormMultiFile' +import FormMultiFile from "../../../components/form/FormMultiFile"; import ReactToolTip from "react-tooltip"; import { ConfirmModal } from "react-bootstrap4-modal"; import StepZilla from "react-stepzilla"; @@ -17,14 +17,13 @@ import FormMultiCheckbox from "../../../components/form/FormMultiCheckbox"; import FormReferenceRequest from "./ReferenceRequest"; import Loading from "../../../components/Loading"; import _ from "lodash"; -import { withTranslation } from 'react-i18next'; -import AnswerValue from '../../../components/answerValue' +import { withTranslation } from "react-i18next"; +import AnswerValue from "../../../components/answerValue"; import FormSelectOther from "../../../components/form/FormSelectOther"; import FormMultiCheckboxOther from "../../../components/form/FormMultiCheckboxOther"; import FormCheckbox from "../../../components/form/FormCheckbox"; import { eventService } from "../../../services/events"; - const baseUrl = process.env.REACT_APP_API_URL; const SHORT_TEXT = "short-text"; @@ -37,36 +36,36 @@ const MULTI_CHECKBOX_OTHER = "multi-checkbox-other"; const FILE = "file"; const DATE = "date"; const REFERENCE_REQUEST = "reference"; -const INFORMATION = ['information', 'sub-heading'] -const MULTI_FILE = 'multi-file'; - - +const INFORMATION = ["information", "sub-heading"]; +const MULTI_FILE = "multi-file"; /* * Utility functions for the feature where questions are dependent on the answers of other questions */ const isEntityDependentOnAnswer = (entityToCheck) => { return entityToCheck.depends_on_question_id && entityToCheck.show_for_values; -} +}; const findDependentQuestionAnswer = (entityToCheck, answers) => { - return answers.find(a => a && a.question_id === entityToCheck.depends_on_question_id); -} + return answers.find( + (a) => a && a.question_id === entityToCheck.depends_on_question_id + ); +}; const doesAnswerMatch = (entityToCheck, answer) => { return entityToCheck.show_for_values.indexOf(answer.value) > -1; -} +}; const answerByQuestionKey = (key, allQuestions, answers) => { - let question = allQuestions.find(q => q.key === key); + let question = allQuestions.find((q) => q.key === key); if (question) { - let answer = answers.find(a => a.question_id === question.id); + let answer = answers.find((a) => a.question_id === question.id); if (answer) { return answer.value; } } return null; -} +}; class FieldEditor extends React.Component { constructor(props) { @@ -77,11 +76,10 @@ class FieldEditor extends React.Component { uploadPercentComplete: 0, uploadError: "", uploaded: false, - } - + }; } - handleChange = event => { + handleChange = (event) => { // Some components (datepicker, custom controls) return pass the value directly rather than via event.target.value const value = event && event.target ? event.target.value : event; @@ -90,12 +88,12 @@ class FieldEditor extends React.Component { } }; - handleCheckChange = event => { + handleCheckChange = (event) => { const value = event.target.checked; if (this.props.onChange) { this.props.onChange(this.props.question, value); } - } + }; handleChangeDropdown = (name, dropdown) => { if (this.props.onChange) { @@ -106,33 +104,36 @@ class FieldEditor extends React.Component { handleUploadFile = (file) => { this.setState({ uploading: true, - }) + }); // TODO: Handle errors - return fileService.uploadFile(file, progressEvent => { - const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total); - this.setState({ - uploadPercentComplete: percentCompleted - }); - }).then(response => { - if (response.fileId && this.props.onChange) { - this.props.onChange( - this.props.question, - JSON.stringify({ filename: response.fileId, rename: file.name }) + return fileService + .uploadFile(file, (progressEvent) => { + const percentCompleted = Math.round( + (progressEvent.loaded * 100) / progressEvent.total ); - } - this.setState({ - uploaded: response.fileId !== "", - uploadError: response.error, - uploading: false - }); + this.setState({ + uploadPercentComplete: percentCompleted, + }); + }) + .then((response) => { + if (response.fileId && this.props.onChange) { + this.props.onChange( + this.props.question, + JSON.stringify({ filename: response.fileId, rename: file.name }) + ); + } + this.setState({ + uploaded: response.fileId !== "", + uploadError: response.error, + uploading: false, + }); - return response.fileId; - }); - } + return response.fileId; + }); + }; formControl = (key, question, answer, validationError, responseId) => { - switch (question.type) { case SHORT_TEXT: return ( @@ -206,8 +207,9 @@ class FieldEditor extends React.Component { defaultValue={answer || null} key={"i_" + key} showError={validationError} - errorText={validationError}/> - ) + errorText={validationError} + /> + ); case MULTI_CHECKBOX: return ( - ) - case MULTI_CHECKBOX_OTHER: - return ( - - ) + errorText={validationError} + /> + ); + case MULTI_CHECKBOX_OTHER: + return ( + + ); case FILE: return ( - ) + responseId={responseId} + /> + ); case INFORMATION[0]: case INFORMATION[1]: - return question.description &&
{question.description}
+ return ( + question.description && ( +
+ {question.description} +
+ ) + ); default: return (

@@ -313,8 +324,14 @@ class FieldEditor extends React.Component { render() { return (

-

- {this.props.question.is_required && *} +

+ {this.props.question.is_required && ( + * + )} {this.props.question.headline}

{this.formControl( @@ -339,36 +356,32 @@ class Section extends React.Component { .sort((a, b) => a.question.order - b.question.order), hasValidated: false, validationStale: false, - }; } - onChange = (question, value) => { const newAnswer = { question_id: question.id, - value: value + value: value, }; - - const newQuestionModels = this.state.questionModels - .map(q => { - if (q.question.id !== question.id) { - return q; - } - return { - ...q, - validationError: this.state.hasValidated - ? this.validate(q, newAnswer) - : "", - answer: newAnswer - }; - }) + const newQuestionModels = this.state.questionModels.map((q) => { + if (q.question.id !== question.id) { + return q; + } + return { + ...q, + validationError: this.state.hasValidated + ? this.validate(q, newAnswer) + : "", + answer: newAnswer, + }; + }); this.setState( { questionModels: newQuestionModels, - validationStale: true + validationStale: true, }, () => { if (this.props.changed) { @@ -378,11 +391,10 @@ class Section extends React.Component { ); }; - // validate validate = (questionModel, updatedAnswer) => { let errors = []; - + const question = questionModel.question; const answer = updatedAnswer || questionModel.answer; @@ -401,31 +413,32 @@ class Section extends React.Component { return errors.join("; "); }; - // isValidated isValidated = () => { - const allAnswersInSection = this.state.questionModels.map(q => q.answer); + const allAnswersInSection = this.state.questionModels.map((q) => q.answer); const validatedModels = this.state.questionModels - .filter(q => this.dependentQuestionFilter(q.question, allAnswersInSection)) - .map(q => { + .filter((q) => + this.dependentQuestionFilter(q.question, allAnswersInSection) + ) + .map((q) => { return { ...q, - validationError: this.validate(q) + validationError: this.validate(q), }; }); - const isValid = !validatedModels.some(v => v.validationError); + const isValid = !validatedModels.some((v) => v.validationError); this.setState( { questionModels: validatedModels, hasValidated: true, - validationStale: false + validationStale: false, }, () => { if (this.props.answerChanged) { this.props.answerChanged( - this.state.questionModels.map(q => q.answer).filter(a => a), + this.state.questionModels.map((q) => q.answer).filter((a) => a), isValid ); } @@ -438,7 +451,7 @@ class Section extends React.Component { handleSave = () => { if (this.props.save) { this.props.save( - this.state.questionModels.map(q => q.answer).filter(a => a) + this.state.questionModels.map((q) => q.answer).filter((a) => a) ); } }; @@ -449,35 +462,38 @@ class Section extends React.Component { */ dependentQuestionFilter = (question, sectionCurrentAnswers) => { if (isEntityDependentOnAnswer(question)) { - const answer = findDependentQuestionAnswer(question, sectionCurrentAnswers); - return answer ? doesAnswerMatch(question, answer) : this.props.showQuestionBasedOnSavedFormAnswers(question); + const answer = findDependentQuestionAnswer( + question, + sectionCurrentAnswers + ); + return answer + ? doesAnswerMatch(question, answer) + : this.props.showQuestionBasedOnSavedFormAnswers(question); } else { return true; } - } + }; render() { - const { - section, - questionModels, - hasValidated, - validationStale - } = this.state; + const { section, questionModels, hasValidated, validationStale } = + this.state; - const allAnswersInSection = questionModels.map(q => q.answer); + const allAnswersInSection = questionModels.map((q) => q.answer); return (

{section.name}

- +
{questionModels && questionModels - .filter(q => this.dependentQuestionFilter(q.question, allAnswersInSection)) - .map(model => ( + .filter((q) => + this.dependentQuestionFilter(q.question, allAnswersInSection) + ) + .map((model) => ( - ) - ) - } + ))} {this.props.unsavedChanges && !this.props.isSaving && ( - )} - {this.props.isSaving && {this.props.t("Saving")}...} + {this.props.isSaving && ( + {this.props.t("Saving")}... + )} {hasValidated && !validationStale && (
{this.props.t("Please fix the errors before continuing.")} @@ -506,7 +522,6 @@ class Section extends React.Component { } class ConfirmationComponent extends React.Component { - dependentQuestionFilter = (question, allAnswers) => { if (isEntityDependentOnAnswer(question)) { const answer = findDependentQuestionAnswer(question, allAnswers); @@ -514,92 +529,130 @@ class ConfirmationComponent extends React.Component { } else { return true; } - } + }; render() { const t = this.props.t; - const allAnswers = this.props.sectionModels && this.props.sectionModels.flatMap(s=>s.questionModels.map(q=>q.answer)); - const allErrors = this.props.sectionModels - && this.props.sectionModels.flatMap(s=>s.questionModels.filter(qm => this.dependentQuestionFilter(qm.question, allAnswers)).map(q=>q.validationError)).filter(e=>e); + const allAnswers = + this.props.sectionModels && + this.props.sectionModels.flatMap((s) => + s.questionModels.map((q) => q.answer) + ); + const allErrors = + this.props.sectionModels && + this.props.sectionModels + .flatMap((s) => + s.questionModels + .filter((qm) => + this.dependentQuestionFilter(qm.question, allAnswers) + ) + .map((q) => q.validationError) + ) + .filter((e) => e); return (

{t("Review your Answers")}

-

- {t("applicationConfirmationText")} -

+

{t("applicationConfirmationText")}

- {t("You MUST click SUBMIT before the deadline for your application to be considered!")} + {" "} + {t( + "You MUST click SUBMIT before the deadline for your application to be considered!" + )}
- {allErrors.length > 0 &&
- {t("Could not submit your application due to validation errors. Please go back and fix these any try again.")} -
} - {allErrors.length === 0 && } + {allErrors.length > 0 && ( +
+ {t( + "Could not submit your application due to validation errors. Please go back and fix these any try again." + )} +
+ )} + {allErrors.length === 0 && ( + + )}
-
- {this.props.sectionModels && - this.props.sectionModels.filter(sm => sm.questionModels.length > 0).map(sm => { - return
-

{sm.section.name}

- {sm.questionModels && - sm.questionModels.filter(qm => this.dependentQuestionFilter(qm.question, allAnswers)).map(qm => { - return ( - qm.question && ( -
-
-
-
{qm.question.headline}
-
-
-
-
-

- - {qm.validationError &&

{qm.validationError}
} -

-
-
-
- ) - ); - })} -
- })} - - {allErrors.length > 0 &&
- {t("Could not submit your application due to validation errors. Please go back and fix these any try again.")} -
} - {allErrors.length === 0 && } + {this.props.sectionModels && + this.props.sectionModels + .filter((sm) => sm.questionModels.length > 0) + .map((sm) => { + return ( +
+

{sm.section.name}

+ {sm.questionModels && + sm.questionModels + .filter((qm) => + this.dependentQuestionFilter(qm.question, allAnswers) + ) + .map((qm) => { + return ( + qm.question && ( +
+
+
+
{qm.question.headline}
+
+
+
+
+

+ + {qm.validationError && ( +

+ {qm.validationError} +
+ )} +

+
+
+
+ ) + ); + })} +
+ ); + })} + + {allErrors.length > 0 && ( +
+ {t( + "Could not submit your application due to validation errors. Please go back and fix these any try again." + )} +
+ )} + {allErrors.length === 0 && ( + + )}
); } } - const Confirmation = withTranslation()(ConfirmationComponent); - class SubmittedComponent extends React.Component { constructor(props) { super(props); @@ -611,13 +664,13 @@ class SubmittedComponent extends React.Component { }; } - handleWithdrawOK = event => { - applicationFormService.withdraw(this.props.responseId).then(resp => { + handleWithdrawOK = (event) => { + applicationFormService.withdraw(this.props.responseId).then((resp) => { this.setState( { isError: resp.isError, errorMessage: resp.message, - withdrawModalVisible: false + withdrawModalVisible: false, }, () => { if (this.props.onWithdrawn) { @@ -628,43 +681,44 @@ class SubmittedComponent extends React.Component { }); }; - handleEditOK = event => { + handleEditOK = (event) => { if (this.props.onEdit) { this.props.onEdit(); } }; - handleWithdrawCancel = event => { + handleWithdrawCancel = (event) => { this.setState({ - withdrawModalVisible: false + withdrawModalVisible: false, }); }; - handleWithdraw = event => { + handleWithdraw = (event) => { this.setState({ - withdrawModalVisible: true + withdrawModalVisible: true, }); }; - handleEdit = event => { + handleEdit = (event) => { this.setState({ - editAppModalVisible: true + editAppModalVisible: true, }); }; - cancelEditModal = event => { + cancelEditModal = (event) => { this.setState({ - editAppModalVisible: false + editAppModalVisible: false, }); }; render() { const t = this.props.t; - const initialText = this.props.event && this.props.event.event_type === "CALL" - ? t("Thank you for responding to the") - : this.props.event.event_type === "JOURNAL" - ? t("Thank you for submitting an article to the") - : t("Thank you for applying for"); + const initialText = + this.props.event && this.props.event.event_type === "CALL" + ? t("Thank you for responding to the") + : this.props.event.event_type === "JOURNAL" + ? t("Thank you for submitting an article to the") + : t("Thank you for applying for"); return ( @@ -734,7 +818,6 @@ class SubmittedComponent extends React.Component { const Submitted = withTranslation()(SubmittedComponent); - class ApplicationFormInstanceComponent extends Component { constructor(props) { super(props); @@ -753,55 +836,68 @@ class ApplicationFormInstanceComponent extends Component { startStep: 0, new_response: !props.response, outcome: props.response && props.response.outcome, - submitValidationErrors: [] + submitValidationErrors: [], }; } - handleSubmit = event => { + handleSubmit = (event) => { event.preventDefault(); this.setState( { - isSubmitting: true + isSubmitting: true, }, () => { - const promise = this.state.new_response - ? applicationFormService.submit(this.props.formSpec.id, true, this.state.answers) - : applicationFormService.updateResponse(this.state.responseId, this.props.formSpec.id, true, this.state.answers); - - promise.then(resp => { - const submitError = resp.response_id === null && resp.status !== 422; - this.setState(prevState => ({ - submitValidationErrors: resp.status === 422 && resp.errors, - isError: submitError, - errorMessage: resp.message, - isSubmitting: false, - isSubmitted: resp.response_id !== null && resp.status !== 422 && resp.is_submitted, - isEditing: false, - submittedTimestamp: resp.submitted_timestamp, - unsavedChanges: false, - new_response: false, - responseId: prevState.responseId || resp.response_id - })); - }); + const promise = this.state.new_response + ? applicationFormService.submit( + this.props.formSpec.id, + true, + this.state.answers + ) + : applicationFormService.updateResponse( + this.state.responseId, + this.props.formSpec.id, + true, + this.state.answers + ); + + promise.then((resp) => { + const submitError = resp.response_id === null && resp.status !== 422; + + this.setState((prevState) => ({ + submitValidationErrors: resp.status === 422 && resp.errors, + isError: submitError, + errorMessage: resp.message, + isSubmitting: false, + isSubmitted: + resp.response_id !== null && + resp.status !== 422 && + resp.is_submitted, + isEditing: false, + submittedTimestamp: resp.submitted_timestamp, + unsavedChanges: false, + new_response: false, + responseId: prevState.responseId || resp.response_id, + })); + }); } ); }; - handleSave = answers => { + handleSave = (answers) => { this.setState( - prevState => { + (prevState) => { return { isSaving: true, answers: prevState.answers - .filter(a => !answers.includes(a.question_id)) - .concat(answers) + .filter((a) => !answers.includes(a.question_id)) + .concat(answers), }; }, () => { if (this.state.new_response) { applicationFormService .submit(this.props.formSpec.id, false, this.state.answers) - .then(resp => { + .then((resp) => { let submitError = resp.response_id === null; this.setState({ isError: submitError, @@ -811,7 +907,7 @@ class ApplicationFormInstanceComponent extends Component { new_response: false, unsavedChanges: false, isSaving: false, - responseId: resp.response_id + responseId: resp.response_id, }); }); } else { @@ -822,7 +918,7 @@ class ApplicationFormInstanceComponent extends Component { false, this.state.answers ) - .then(resp => { + .then((resp) => { let saveError = resp.response_id === null; this.setState({ isError: saveError, @@ -830,7 +926,7 @@ class ApplicationFormInstanceComponent extends Component { isSubmitted: resp.is_submitted, submittedTimestamp: resp.submitted_timestamp, unsavedChanges: false, - isSaving: false + isSaving: false, }); }); } @@ -844,18 +940,26 @@ class ApplicationFormInstanceComponent extends Component { handleAnswerChanged = (answers, save) => { if (answers) { - this.setState(prevState => { - return { - answers: prevState.answers - .filter(a => !answers.map(a => a.question_id).includes(a.question_id)) - .concat(answers), - submitValidationErrors: prevState.submitValidationErrors.filter(e => !answers.map(a => a.question_id).includes(e.question_id)) - }; - }, () => { - if (save) { - this.handleSave([]); + this.setState( + (prevState) => { + return { + answers: prevState.answers + .filter( + (a) => + !answers.map((a) => a.question_id).includes(a.question_id) + ) + .concat(answers), + submitValidationErrors: prevState.submitValidationErrors.filter( + (e) => !answers.map((a) => a.question_id).includes(e.question_id) + ), + }; + }, + () => { + if (save) { + this.handleSave([]); + } } - }); + ); } }; @@ -864,7 +968,9 @@ class ApplicationFormInstanceComponent extends Component { }; getSubmitValidationError = (q) => { - const validationError = this.state.submitValidationErrors.find(e => e.question_id === q.id); + const validationError = this.state.submitValidationErrors.find( + (e) => e.question_id === q.id + ); if (!validationError) { return ""; } @@ -882,7 +988,7 @@ class ApplicationFormInstanceComponent extends Component { } return ""; - } + }; render() { const { @@ -892,25 +998,45 @@ class ApplicationFormInstanceComponent extends Component { errorMessage, answers, isSubmitting, - outcome + outcome, } = this.state; if (isError) { - return
- {errorMessage} -
; + return ( +
+ {errorMessage} +
+ ); } - if (outcome === "ACCEPTED" || outcome === "REJECTED") { - return
- {outcome === "ACCEPTED" &&
-

{this.props.t("You have already been accepted to this event.")}

- - {this.props.t("View Offer")} - -
} - {outcome === "REJECTED" &&

{this.props.t("Unfortunately your application to this event has been rejected, you are not able to apply again.")}

} -
; + if ( + (outcome === "ACCEPTED" || outcome === "REJECTED") && + this.props.event.event_type !== "JOURNAL" + ) { + return ( +
+ {outcome === "ACCEPTED" && ( +
+

+ {this.props.t("You have already been accepted to this event.")} +

+ + {this.props.t("View Offer")} + +
+ )} + {outcome === "REJECTED" && ( +

+ {this.props.t( + "Unfortunately your application to this event has been rejected, you are not able to apply again." + )} +

+ )} +
+ ); } if (isSubmitted && !isEditing) { @@ -932,25 +1058,26 @@ class ApplicationFormInstanceComponent extends Component { } else { return true; } - } + }; const sections = this.props.formSpec.sections && - this.props.formSpec.sections.slice() + this.props.formSpec.sections + .slice() .filter(includeEntityDueToDependentQuestion) .sort((a, b) => a.order - b.order); const sectionModels = sections && - sections.map(section => { + sections.map((section) => { return { section: section, - questionModels: section.questions.map(q => { + questionModels: section.questions.map((q) => { return { question: q, - answer: answers.find(a => a.question_id === q.id), - validationError: this.getSubmitValidationError(q) + answer: answers.find((a) => a.question_id === q.id), + validationError: this.getSubmitValidationError(q), }; - }) + }), }; }); @@ -962,7 +1089,9 @@ class ApplicationFormInstanceComponent extends Component { component: (
- ) + ), }; }); @@ -986,7 +1115,7 @@ class ApplicationFormInstanceComponent extends Component { submit={this.handleSubmit} isSubmitting={isSubmitting} /> - ) + ), }); return ( @@ -1004,84 +1133,200 @@ class ApplicationFormInstanceComponent extends Component {
- {isSubmitting &&

{this.props.t("Saving Responses")}...

} + {isSubmitting && ( +

{this.props.t("Saving Responses")}...

+ )}
); } } -const ApplicationFormInstance = withRouter(withTranslation()(ApplicationFormInstanceComponent)); +const ApplicationFormInstance = withRouter( + withTranslation()(ApplicationFormInstanceComponent) +); class ApplicationListComponent extends Component { constructor(props) { super(props); - this.state = { - } + this.state = {}; } getCandidate = (allQuestions, response) => { - const nominating_capacity = answerByQuestionKey("nominating_capacity", allQuestions, response.answers); + const nominating_capacity = answerByQuestionKey( + "nominating_capacity", + allQuestions, + response.answers + ); if (nominating_capacity === "other") { - let firstname = answerByQuestionKey("nomination_firstname", allQuestions, response.answers); - let lastname = answerByQuestionKey("nomination_lastname", allQuestions, response.answers); + let firstname = answerByQuestionKey( + "nomination_firstname", + allQuestions, + response.answers + ); + let lastname = answerByQuestionKey( + "nomination_lastname", + allQuestions, + response.answers + ); return firstname + " " + lastname; } - return this.props.event.event_type ==='JOURNAL' ? this.props.t("Submission") + " " + response.id : this.props.t("Self Nomination"); - } + return this.props.event.event_type === "JOURNAL" + ? this.props.t("Submission") + " " + response.id + : this.props.t("Self Nomination"); + }; getStatus = (response) => { if (response.is_submitted) { - return {this.props.t("Submitted")} + return {this.props.t("Submitted")}; + } else { + return {this.props.t("In Progress")}; } - else { - return {this.props.t("In Progress")} + }; + + renderSections() { + const applicationForm = this.state.applicationForm; + const applicationData = this.state.applicationData; + let html = []; + + // main function + if (applicationForm && applicationData) { + applicationForm.sections.forEach((section) => { + html.push( +
+ {/*Heading*/} +
+

{section.name}

+
+ {/*Q & A*/} +
{this.renderResponses(section)}
+
+ ); + }); } + + return html; + } + renderResponses(section) { + const applicationData = this.state.applicationData; + const questions = section.questions.map((q) => { + const a = applicationData.answers.find((a) => a.question_id === q.id); + if (q.type === "information") { + return

{q.headline}

; + } + return ( +
+

+ {q.headline} + {q.description && ( + +
+ {q.description} +
+ )} +

+
+ +
+
+ ); + }); + return questions; } getAction = (response) => { if (response.is_submitted) { - return + return ( + + ); + } else { + return ( + + ); } - else { - return + }; + + getOutcome = (response) => { + if (response.outcome) { + const badgeClass = + response.outcome === "ACCEPTED" + ? "badge-success" + : response.outcome === "REJECTED" + ? "badge-danger" + : "badge-warning"; + return ( + {response.outcome} + ); } - } + return ( + + {this.props.t("Pending")} + + ); + }; render() { - let allQuestions = _.flatMap(this.props.formSpec.sections, s => s.questions); - const title = this.props.event.event_type ==='JOURNAL' ? this.props.t("Your Submissions") : this.props.t("Your Nominations"); - let firstColumn = this.props.event.event_type ==='JOURNAL' ? this.props.t("Submission") : this.props.t("Nominee"); - return
-

{title}

- - - - - - - - - - {this.props.responses.map(response => { - return - - - + let allQuestions = _.flatMap( + this.props.formSpec.sections, + (s) => s.questions + ); + const title = + this.props.event.event_type === "JOURNAL" + ? this.props.t("Your Submissions") + : this.props.t("Your Nominations"); + let firstColumn = + this.props.event.event_type === "JOURNAL" + ? this.props.t("Submission") + : this.props.t("Nominee"); + return ( +
+

{title}

+
{firstColumn}{this.props.t("Status")}
{this.getCandidate(allQuestions, response)}{this.getStatus(response)}{this.getAction(response)}
+ + + + + + - })} - -
{firstColumn}{this.props.t("Status")}{this.props.t("Results")}
-
+ + + {this.props.responses.map((response) => { + return ( + + {this.getCandidate(allQuestions, response)} + {this.getStatus(response)} + {this.getOutcome(response)} + {this.getAction(response)} + + ); + })} + + +
+ ); } } - const ApplicationList = withRouter(withTranslation()(ApplicationListComponent)); - class ApplicationForm extends Component { constructor(props) { super(props); + this.state = { isLoading: true, isError: false, @@ -1089,8 +1334,11 @@ class ApplicationForm extends Component { formSpec: null, responses: [], selectedResponse: null, - journalSubmissionFlag: false - } + listselectedResponse: null, + journalSubmissionFlag: false, + continue: false, + view: false, + }; } componentDidMount() { @@ -1098,38 +1346,54 @@ class ApplicationForm extends Component { Promise.all([ applicationFormService.getForEvent(eventId), applicationFormService.getResponse(eventId), - eventService.getEvent(eventId) - ]).then(responses => { + eventService.getEvent(eventId), + ]).then((responses) => { const [formResponse, responseResponse, eventResponse] = responses; - const selectFirstResponse = formResponse && formResponse.formSpec && !formResponse.formSpec.nominations && responseResponse.response.length > 0; + const selectFirstResponse = + formResponse && + formResponse.formSpec && + !formResponse.formSpec.nominations && + responseResponse.response.length > 0; + this.setState({ formSpec: formResponse.formSpec, responses: responseResponse.response, isError: formResponse.formSpec === null || responseResponse.error, - errorMessage: (formResponse.error + " " + responseResponse.error).trim(), + errorMessage: ( + formResponse.error + + " " + + responseResponse.error + ).trim(), isLoading: false, - selectedResponse: selectFirstResponse ? responseResponse.response[0] : null, + selectedResponse: selectFirstResponse + ? responseResponse.response[0] + : null, responseSelected: selectFirstResponse, - event: eventResponse.event + listselectedResponse: selectFirstResponse + ? responseResponse.response + : null, + event: eventResponse.event, }); }); - } - responseSelected = response => { + responseSelected = (response) => { this.setState({ selectedResponse: response, - responseSelected: true + responseSelected: true, }); - } + }; newNomination = () => { this.setState({ - responseSelected: true + responseSelected: true, }); - } + }; + + newSubmission = () => { + window.location.href = `/${this.props.event.key}/apply/new`; + }; - render() { const { isLoading, @@ -1138,34 +1402,105 @@ class ApplicationForm extends Component { formSpec, responses, selectedResponse, - responseSelected} = this.state; + responseSelected, + listselectedResponse, + } = this.state; if (isLoading) { - return (); + return ; } if (isError) { - return
{errorMessage}
; - } - - if (this.props.event.event_type === 'JOURNAL' && this.props.journalSubmissionFlag) { - return - } - else if (formSpec.nominations && responses.length > 0 && !responseSelected) { - let newForm = this.state.event.event_type ==='JOURNAL' ? this.props.t("New Submission") + " " : this.props.t("New Nomination") + " "; - return
-
- -
+ return ( +
+ {errorMessage} +
+ ); } - else { - return + + if (this.props.event.event_type === "JOURNAL") { + if (this.props.journalSubmissionFlag) { + return ( + + ); + } + if (this.props.view) { + let newForm = this.props.t("New Submission"); + + return ( +
+ +
+ +
+ ); + } + if (this.props.continue) { + return ( + item.id === parseInt(this.props.match.params.id) + )} + event={this.props.event} + /> + ); + } + return ( + + ); + } else if ( + formSpec.nominations && + responses.length > 0 && + !responseSelected + ) { + let newForm = + this.state.event.event_type === "JOURNAL" + ? this.props.t("New Submission") + " " + : this.props.t("New Nomination") + " "; + return ( +
+ +
+ +
+ ); + } else { + return ( + + ); } } } diff --git a/webapp/src/pages/eventHome/EventHome.js b/webapp/src/pages/eventHome/EventHome.js index a63fec54..8804d448 100755 --- a/webapp/src/pages/eventHome/EventHome.js +++ b/webapp/src/pages/eventHome/EventHome.js @@ -28,6 +28,8 @@ import ResponseList from "../ResponseList/ResponseList"; import ResponsePage from "../ResponsePage/ResponsePage"; import ReviewDashboard from "../reviewDashboard"; import { Attendance, Indemnity } from '../attendance'; +import ResponseDetails from "../ReponseDetails/ReponseDetails"; +import ReponseDetails from "../ReponseDetails/ReponseDetails"; class EventInfo extends Component { constructor(props) { @@ -154,11 +156,26 @@ class EventHome extends Component { path={`${match.path}/apply`} render={(props) => } /> + } /> + + {/*new route */} + } + /> + + } + /> + } /> + } + /> + + { isOpen: props.event.is_review_open, eventId: event.event.id, stage: currentStage, - deadline: event.event.review_close, + deadline: event.event.review_close,//"2024-12-05T23:59:59" active: currentStage !== 1 ? true : false }) setCreateMode(true); diff --git a/webapp/src/services/applicationForm/applicationForm.service.js b/webapp/src/services/applicationForm/applicationForm.service.js index 4170a3bf..b4deb08b 100644 --- a/webapp/src/services/applicationForm/applicationForm.service.js +++ b/webapp/src/services/applicationForm/applicationForm.service.js @@ -85,6 +85,7 @@ function submit(applicationFormId, isSubmitted, answers) { return axios.post(baseUrl + `/api/v1/response`, response, {headers: authHeader()}) .then(resp=> { + console.log() return { response_id: resp.data.id, is_submitted: resp.data.is_submitted, From 8df2e2c2fc4cb541e9d5c9e3bf40e27b463ce8a8 Mon Sep 17 00:00:00 2001 From: Yvan CARRE Date: Mon, 9 Dec 2024 23:49:50 +0200 Subject: [PATCH 02/17] fixing bug front/back -end - improvement --- api/app/outcome/api.py | 38 ++++- api/app/outcome/models.py | 4 + api/app/outcome/repository.py | 14 ++ api/app/responses/api.py | 7 +- api/migrations/alembic.ini | 1 + api/migrations/versions/062abca36566_.py | 22 +++ api/migrations/versions/41517b1f4730_.py | 22 +++ .../4923563b3a63_add_reponseID_outcome.py | 42 ++++++ api/migrations/versions/67d04fb5a5ed_.py | 22 +++ .../a4662031beca_add_outcome_model.py | 2 +- api/migrations/versions/ba73c854003e_.py | 22 +++ .../bf4cb562e36c_add_new_status_outcome.py | 38 +++++ webapp/src/components/form/FormCreator.js | 3 +- .../pages/ReponseDetails/ReponseDetails.js | 131 ++++++++++++------ ...ReponseDetails.css => ResponseDetails.css} | 0 webapp/src/pages/ResponsePage/ResponsePage.js | 10 +- .../components/ApplicationForm.js | 13 +- .../pages/reviewForm/components/ReviewForm.js | 4 + .../src/services/outcome/outcome.service.js | 11 +- 19 files changed, 345 insertions(+), 61 deletions(-) create mode 100644 api/migrations/versions/062abca36566_.py create mode 100644 api/migrations/versions/41517b1f4730_.py create mode 100755 api/migrations/versions/4923563b3a63_add_reponseID_outcome.py create mode 100644 api/migrations/versions/67d04fb5a5ed_.py create mode 100644 api/migrations/versions/ba73c854003e_.py create mode 100755 api/migrations/versions/bf4cb562e36c_add_new_status_outcome.py rename webapp/src/pages/ReponseDetails/{ReponseDetails.css => ResponseDetails.css} (100%) diff --git a/api/app/outcome/api.py b/api/app/outcome/api.py index a782a49f..d90a38b3 100644 --- a/api/app/outcome/api.py +++ b/api/app/outcome/api.py @@ -8,6 +8,7 @@ from app.outcome.repository import OutcomeRepository as outcome_repository from app.events.repository import EventRepository as event_repository from app.users.repository import UserRepository as user_repository +from app.responses.repository import ResponseRepository as response_repository from app.utils.emailer import email_user from app.utils.auth import auth_required, event_admin_required @@ -28,6 +29,27 @@ def _extract_status(outcome): 'timestamp': fields.DateTime(dt_format='iso8601'), } +answer_fields = { + 'id': fields.Integer, + 'question_id': fields.Integer, + 'question': fields.String(attribute='question.headline'), + 'value': fields.String(attribute='value_display'), + 'question_type': fields.String(attribute='question.type') +} + +response_fields = { + 'id': fields.Integer, + 'application_form_id': fields.Integer, + 'user_id': fields.Integer, + 'is_submitted': fields.Boolean, + 'submitted_timestamp': fields.DateTime(dt_format='iso8601'), + 'is_withdrawn': fields.Boolean, + 'withdrawn_timestamp': fields.DateTime(dt_format='iso8601'), + 'started_timestamp': fields.DateTime(dt_format='iso8601'), + 'answers': fields.List(fields.Nested(answer_fields)) +} + + user_fields = { 'id': fields.Integer, 'email': fields.String, @@ -41,6 +63,7 @@ def _extract_status(outcome): 'status': fields.String(attribute=_extract_status), 'timestamp': fields.DateTime(dt_format='iso8601'), 'user': fields.Nested(user_fields), + 'response': fields.Nested(response_fields), 'updated_by_user': fields.Nested(user_fields) } @@ -51,12 +74,14 @@ class OutcomeAPI(restful.Resource): def get(self, event_id): req_parser = reqparse.RequestParser() req_parser.add_argument('user_id', type=int, required=True) + req_parser.add_argument('response_id', type=int, required=True) args = req_parser.parse_args() - + + response_id=args['response_id'] user_id = args['user_id'] try: - outcome = outcome_repository.get_latest_by_user_for_event(user_id, event_id) + outcome = outcome_repository.get_latest_by_user_for_event_response(user_id,response_id, event_id) if not outcome: return errors.OUTCOME_NOT_FOUND @@ -74,6 +99,7 @@ def get(self, event_id): def post(self, event_id): req_parser = reqparse.RequestParser() req_parser.add_argument('user_id', type=int, required=True) + req_parser.add_argument('response_id', type=int, required=True) req_parser.add_argument('outcome', type=str, required=True) args = req_parser.parse_args() @@ -84,6 +110,11 @@ def post(self, event_id): user = user_repository.get_by_id(args['user_id']) if not user: return errors.USER_NOT_FOUND + + response = response_repository.get_by_id_and_user_id(args['response_id'],args['user_id']) + if not response: + return errors.RESPONSE_NOT_FOUND + try: status = Status[args['outcome']] @@ -92,7 +123,7 @@ def post(self, event_id): try: # Set existing outcomes to no longer be the latest outcome - existing_outcomes = outcome_repository.get_all_by_user_for_event(args['user_id'], event_id) + existing_outcomes = outcome_repository.get_all_by_user_for_event_response(args['user_id'],args['response_id'], event_id) for existing_outcome in existing_outcomes: existing_outcome.reset_latest() @@ -100,6 +131,7 @@ def post(self, event_id): outcome = Outcome( event_id, args['user_id'], + args['response_id'], status, g.current_user['id']) diff --git a/api/app/outcome/models.py b/api/app/outcome/models.py index 95dfe418..82e9c779 100644 --- a/api/app/outcome/models.py +++ b/api/app/outcome/models.py @@ -14,6 +14,7 @@ class Outcome(db.Model): id = db.Column(db.Integer(), primary_key = True, nullable = False) event_id = db.Column(db.Integer(), db.ForeignKey('event.id'), nullable = False) user_id = db.Column(db.Integer(), db.ForeignKey('app_user.id'), nullable = False) + response_id = db.Column(db.Integer(), db.ForeignKey('response.id'), nullable=False) ## add response_id status = db.Column(db.Enum(Status, name='outcome_status'), nullable = False) timestamp = db.Column(db.DateTime(), nullable = False) latest = db.Column(db.Boolean(), nullable = False) @@ -22,15 +23,18 @@ class Outcome(db.Model): event = db.relationship('Event', foreign_keys=[event_id]) user = db.relationship('AppUser', foreign_keys=[user_id]) updated_by_user = db.relationship('AppUser', foreign_keys=[updated_by_user_id]) + response = db.relationship('Response', foreign_keys=[response_id]) ## add relationship def __init__(self, event_id, user_id, + response_id, status, updated_by_user_id ): self.event_id = event_id self.user_id = user_id + self.response_id = response_id self.status = status self.timestamp = datetime.now() self.latest = True diff --git a/api/app/outcome/repository.py b/api/app/outcome/repository.py index e673cc6d..47696878 100644 --- a/api/app/outcome/repository.py +++ b/api/app/outcome/repository.py @@ -28,5 +28,19 @@ def get_latest_for_event(event_id): @staticmethod def add(outcome): db.session.add(outcome) + + @staticmethod + def get_latest_by_user_for_event_response(user_id,response_id,event_id): + outcome = (db.session.query(Outcome) + .filter_by(user_id=user_id,response_id=response_id, event_id=event_id, latest=True) + .first()) + return outcome + + @staticmethod + def get_all_by_user_for_event_response(user_id,response_id, event_id): + outcomes = (db.session.query(Outcome) + .filter_by(user_id=user_id,response_id=response_id, event_id=event_id) + .all()) + return outcomes \ No newline at end of file diff --git a/api/app/responses/api.py b/api/app/responses/api.py index f222595c..369fc8e2 100644 --- a/api/app/responses/api.py +++ b/api/app/responses/api.py @@ -123,8 +123,9 @@ def get(self): responses = response_repository.get_all_for_user_application(current_user_id, form.id) # TODO: Link outcomes to responses rather than events to cater for multiple submissions. - outcome = outcome_repository.get_latest_by_user_for_event(current_user_id, event_id) for response in responses: + outcome = outcome_repository.get_latest_by_user_for_event_response(current_user_id,response.id, event_id) + # get_latest_by_user_for_event(current_user_id, event_id) response.outcome = outcome return marshal(responses, ResponseAPI.response_fields), 200 @@ -146,8 +147,8 @@ def post(self): user = user_repository.get_by_id(user_id) responses = response_repository.get_all_for_user_application(user_id, application_form_id) - if not application_form.nominations and len(responses) < 0: - return errors.RESPONSE_ALREADY_SUBMITTED + # if not application_form.nominations and len(responses) > 0: + # return errors.RESPONSE_ALREADY_SUBMITTED response = Response(application_form_id, user_id, language) response_repository.save(response) diff --git a/api/migrations/alembic.ini b/api/migrations/alembic.ini index f8ed4801..872a02d4 100644 --- a/api/migrations/alembic.ini +++ b/api/migrations/alembic.ini @@ -1,6 +1,7 @@ # A generic, single database configuration. [alembic] + # template used to generate migration files # file_template = %%(rev)s_%%(slug)s diff --git a/api/migrations/versions/062abca36566_.py b/api/migrations/versions/062abca36566_.py new file mode 100644 index 00000000..f59e6cd7 --- /dev/null +++ b/api/migrations/versions/062abca36566_.py @@ -0,0 +1,22 @@ +"""empty message + +Revision ID: 062abca36566 +Revises: 41517b1f4730 +Create Date: 2024-12-07 18:01:36.623356 + +""" + +# revision identifiers, used by Alembic. +revision = '062abca36566' +down_revision = '41517b1f4730' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/api/migrations/versions/41517b1f4730_.py b/api/migrations/versions/41517b1f4730_.py new file mode 100644 index 00000000..316c3546 --- /dev/null +++ b/api/migrations/versions/41517b1f4730_.py @@ -0,0 +1,22 @@ +"""empty message + +Revision ID: 41517b1f4730 +Revises: e8eafcfe7800 +Create Date: 2024-12-07 17:40:47.044636 + +""" + +# revision identifiers, used by Alembic. +revision = '41517b1f4730' +down_revision = 'e8eafcfe7800' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/api/migrations/versions/4923563b3a63_add_reponseID_outcome.py b/api/migrations/versions/4923563b3a63_add_reponseID_outcome.py new file mode 100755 index 00000000..d42da0e6 --- /dev/null +++ b/api/migrations/versions/4923563b3a63_add_reponseID_outcome.py @@ -0,0 +1,42 @@ +""" Add foreign key response_id to outcome + +Revision ID: 4923563b3a63 +Revises: bf4cb562e36c +Create Date: 2024-12-07 19:07:18.021334 + +""" + +# revision identifiers, used by Alembic. +revision = '4923563b3a63' +down_revision = 'bf4cb562e36c' + + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.add_column( + 'outcome', + sa.Column('response_id', sa.Integer(), nullable=False) + ) + + op.create_foreign_key( + 'fk_outcome_response_id', + 'outcome', + 'response', + ['response_id'], + ['id'] + ) + + +def downgrade(): + op.drop_constraint( + 'fk_outcome_response_id', + 'outcome', + type_='foreignkey' + ) + + op.drop_column('outcome', 'response_id') + + diff --git a/api/migrations/versions/67d04fb5a5ed_.py b/api/migrations/versions/67d04fb5a5ed_.py new file mode 100644 index 00000000..5e55b1c7 --- /dev/null +++ b/api/migrations/versions/67d04fb5a5ed_.py @@ -0,0 +1,22 @@ +"""empty message + +Revision ID: 67d04fb5a5ed +Revises: 4923563b3a63 +Create Date: 2024-12-07 19:08:54.277900 + +""" + +# revision identifiers, used by Alembic. +revision = '67d04fb5a5ed' +down_revision = '4923563b3a63' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/api/migrations/versions/a4662031beca_add_outcome_model.py b/api/migrations/versions/a4662031beca_add_outcome_model.py index 3731ba9f..2f0241c2 100644 --- a/api/migrations/versions/a4662031beca_add_outcome_model.py +++ b/api/migrations/versions/a4662031beca_add_outcome_model.py @@ -39,4 +39,4 @@ def downgrade(): # ### commands auto generated by Alembic - please adjust! ### op.drop_table('outcome') outcome_status.drop(op.get_bind()) - # ### end Alembic commands ### + # ### end Alembic commands ### \ No newline at end of file diff --git a/api/migrations/versions/ba73c854003e_.py b/api/migrations/versions/ba73c854003e_.py new file mode 100644 index 00000000..7ff7960b --- /dev/null +++ b/api/migrations/versions/ba73c854003e_.py @@ -0,0 +1,22 @@ +"""empty message + +Revision ID: ba73c854003e +Revises: 67d04fb5a5ed +Create Date: 2024-12-07 19:09:55.640261 + +""" + +# revision identifiers, used by Alembic. +revision = 'ba73c854003e' +down_revision = '67d04fb5a5ed' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/api/migrations/versions/bf4cb562e36c_add_new_status_outcome.py b/api/migrations/versions/bf4cb562e36c_add_new_status_outcome.py new file mode 100755 index 00000000..fe21111f --- /dev/null +++ b/api/migrations/versions/bf4cb562e36c_add_new_status_outcome.py @@ -0,0 +1,38 @@ +""" Add new status outcome + +Revision ID: bf4cb562e36c +Revises: 062abca36566 +Create Date: 2024-12-07 19:05:30.962104 + +""" +# revision identifiers, used by Alembic. +revision = 'bf4cb562e36c' +down_revision = '062abca36566' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.execute("ALTER TYPE outcome_status ADD VALUE 'REVIEW';") + op.execute("ALTER TYPE outcome_status ADD VALUE 'ACCEPT_W_REVISION';") + op.execute("ALTER TYPE outcome_status ADD VALUE 'REJECT_W_ENCOURAGEMENT';") + + +def downgrade(): + op.execute(""" + DO $$ + BEGIN + ALTER TYPE outcome_status RENAME TO outcome_status_old; + + CREATE TYPE outcome_status AS ENUM('ACCEPTED', 'REJECTED', 'WAITLIST'); + + ALTER TABLE outcome + ALTER COLUMN status + TYPE outcome_status + USING status::text::outcome_status; + + DROP TYPE outcome_status_old; + END $$; + """) + diff --git a/webapp/src/components/form/FormCreator.js b/webapp/src/components/form/FormCreator.js index 7cd63527..f5f746ec 100644 --- a/webapp/src/components/form/FormCreator.js +++ b/webapp/src/components/form/FormCreator.js @@ -147,7 +147,8 @@ const FormCreator = ({ /> {homeRedirect && } {errorResponse ? ( -
+ // tooltiptext-error response-error +
{{errorResponse}}
) : ( diff --git a/webapp/src/pages/ReponseDetails/ReponseDetails.js b/webapp/src/pages/ReponseDetails/ReponseDetails.js index ef022728..a192dc88 100644 --- a/webapp/src/pages/ReponseDetails/ReponseDetails.js +++ b/webapp/src/pages/ReponseDetails/ReponseDetails.js @@ -2,12 +2,13 @@ import React, { Component } from "react"; import { withRouter } from "react-router"; import { Link } from "react-router-dom"; import { applicationFormService } from "../../services/applicationForm"; - +import "./ResponseDetails.css"; import Loading from "../../components/Loading"; import _ from "lodash"; import { withTranslation } from "react-i18next"; import AnswerValue from "../../components/answerValue"; import { eventService } from "../../services/events"; +import moment from "moment"; class ReponseDetails extends Component { constructor(props) { @@ -16,6 +17,8 @@ class ReponseDetails extends Component { applicationData: null, applicationForm: null, isLoading: true, + outcome: null, + error: false, }; } @@ -26,13 +29,16 @@ class ReponseDetails extends Component { applicationFormService.getResponse(eventId), eventService.getEvent(eventId), ]).then((responses) => { - console.log(responses); this.setState({ applicationForm: responses[0].formSpec, applicationData: responses[1].response.find( (item) => item.id === parseInt(this.props.match.params.id) ), + outcome: responses[1].response.find( + (item) => item.id === parseInt(this.props.match.params.id) + ).outcome, isLoading: false, + error: responses[0].error || responses[1].error || responses[2].error, }); }); } @@ -91,55 +97,99 @@ class ReponseDetails extends Component { this.props.history.goBack(); } + formatDate = (dateString) => { + return moment(dateString).format("D MMM YYYY, H:mm:ss [(UTC)]"); + }; + + applicationStatus() { + const data = this.state.applicationData; + if (data) { + const unsubmitted = !data.is_submitted && !data.is_withdrawn; + const submitted = data.is_submitted; + const withdrawn = data.is_withdrawn; + + if (unsubmitted) { + return ( + + Unsubmitted{" "} + {this.formatDate(data.started_timestamp)} + + ); + } + if (submitted) { + return ( + + Submitted{" "} + {this.formatDate(data.submitted_timestamp)} + + ); + } + if (withdrawn) { + return ( + + Withdrawn{" "} + {this.formatDate(data.started_timestamp)} + + ); + } + } + } + + outcomeStatus() { + const data = this.state.applicationData; + console.log("data"); + console.log(data); + + if (data.outcome) { + const badgeClass = + data.outcome === "ACCEPTED" + ? "badge-success" + : data.outcome === "REJECTED" + ? "badge-danger" + : "badge-warning"; + + return ( + + + {data.outcome} + {" "} + {/* {this.formatDate(this.state.outcome.timestamp)} */} + + ); + } else { + + return ( + + {"Pending ..."} + + ); + } + } render() { - const { applicationData, isLoading } = this.state; + const { applicationData, isLoading, outcome, error } = this.state; if (isLoading) { return ; } return (
- {/* API Error - {error && -
-

{JSON.stringify(error)}

-
- } - - {this.renderReviewerModal()} - {this.renderDeleteTagModal()} - {this.renderDeleteReviewerModal()} */} - - {/* Headings */} - {/* {applicationData && -
-
-

{applicationData.user_title} {applicationData.firstname} {applicationData.lastname}

-

{t("Language")}: {applicationData.language}

-
- {this.renderTags()} - this.setTagSelectorVisible()}>{t("Add tag")} - -
- -
- - {/* User details Right Tab */} - {/*
-
-

{this.applicationStatus()}

-

{this.outcomeStatus()}

- -
- -
-
- } */} + {error && ( +
+

{JSON.stringify(error)}

+
+ )} +
+
+ {" "} +

{this.applicationStatus()}

+ {" "} +

{this.outcomeStatus()}

+
+
{/*Response Data*/} {applicationData && (
- {this.renderSections()}
@@ -150,7 +200,6 @@ class ReponseDetails extends Component { {this.props.t("Go Back")}
-
)}
diff --git a/webapp/src/pages/ReponseDetails/ReponseDetails.css b/webapp/src/pages/ReponseDetails/ResponseDetails.css similarity index 100% rename from webapp/src/pages/ReponseDetails/ReponseDetails.css rename to webapp/src/pages/ReponseDetails/ResponseDetails.css diff --git a/webapp/src/pages/ResponsePage/ResponsePage.js b/webapp/src/pages/ResponsePage/ResponsePage.js index d4d63ea0..2fe35406 100644 --- a/webapp/src/pages/ResponsePage/ResponsePage.js +++ b/webapp/src/pages/ResponsePage/ResponsePage.js @@ -53,6 +53,7 @@ class ResponsePage extends Component { tagsService.getTagList(this.props.event.id), reviewService.getReviewAssignments(this.props.event.id), ]).then((responses) => { + this.setState( { eventLanguages: responses[0].event @@ -131,7 +132,7 @@ class ResponsePage extends Component { return ( Submitted{" "} - {this.formatDate(data.started_timestamp)} + {this.formatDate(data.submitted_timestamp)} ); } @@ -200,8 +201,9 @@ class ResponsePage extends Component { getOutcome() { outcomeService - .getOutcome(this.props.event.id, this.state.applicationData.user_id) + .getOutcome(this.props.event.id, this.state.applicationData.user_id,this.props.match.params.id) .then((response) => { + console.log(this.props); if (response.status === 200) { const newOutcome = { timestamp: response.outcome.timestamp, @@ -224,6 +226,7 @@ class ResponsePage extends Component { outcomeService .assignOutcome( this.state.applicationData.user_id, + this.props.match.params.id, this.props.event.id, selectedOutcome ) @@ -239,7 +242,7 @@ class ResponsePage extends Component { confirmModalVisible: false, }); } else { - this.setState({ erorr: response.error }); + this.setState({ error: response.error }); } }); } @@ -265,6 +268,7 @@ class ResponsePage extends Component { confirmModalVisible: false, }); this.submitOutcome(this.state.pendingOutcome); + }; handleConfirmationCancel = (event) => { diff --git a/webapp/src/pages/applicationForm/components/ApplicationForm.js b/webapp/src/pages/applicationForm/components/ApplicationForm.js index f0c54c2d..b9530925 100644 --- a/webapp/src/pages/applicationForm/components/ApplicationForm.js +++ b/webapp/src/pages/applicationForm/components/ApplicationForm.js @@ -720,6 +720,8 @@ class SubmittedComponent extends React.Component { ? t("Thank you for submitting an article to the") : t("Thank you for applying for"); + + return ( )} +
+ {this.getSubmissionList( + result.filter( + (element) => element.id !== parseInt(this.props.match.params.id) + ) + )} +
); } diff --git a/webapp/src/pages/ReponseDetails/ResponseDetails.css b/webapp/src/pages/ReponseDetails/ResponseDetails.css index a6617a2d..0d4b8224 100644 --- a/webapp/src/pages/ReponseDetails/ResponseDetails.css +++ b/webapp/src/pages/ReponseDetails/ResponseDetails.css @@ -1,408 +1,48 @@ -/* Universal */ -.table-wrapper.response-page { - width: 87vw; - margin-top: 20px; -} - -.table-wrapper.response-page h2 { - color: hsl(0, 0%, 25%); - font-weight: 300; - margin-top: 2px; - text-align: left; - margin-bottom: 5px; -} - -.response-details h4 { - color: hsl(213deg 16% 49%);; - font-weight: 300; - padding-left: 7px; - margin-top: 5px; - text-align: left; -} - -.table-wrapper.response-page button { - margin-bottom: 10px; -} - -.table-wrapper.response-page label { - font-size: 12px; - color: grey; - margin: 0 1px; -} - - -/* Flex Box Classes */ -.flex { - display: flex; -} - -.baseline { - align-items: baseline; -} - -.center { - justify-content: center; -} - - -/* General and User Details Heading */ -.headings-lower { - display: flex; - width: 100%; - justify-content: space-between; - margin-top: 8px; - flex-wrap: wrap; - padding: 2%; - box-shadow: 1px 2px 6px hsl(0, 0%, 53%); - border-radius: 4px; -} - -.user-details { - padding: 0 2px -} - -.outcome { - display: flex; - flex-direction: column; - align-content: center; - grid-auto-rows: auto; -} - -.user-details.right { - padding: 0 10px -} - -html .headings-lower label { - color: hsl(0deg 0% 55%); -} - -.user-details { - display: flex; - text-align: left; - flex-direction: column; - justify-content: center; -} - - -.user-details.right { - display: flex; - flex-direction: column; - align-items: flex-start; -} - -.user-details h4 { - color: hsl(0, 0%, 44%); - margin: 0; -} - -.tags .btn-light { - font-size: 13px; - background: hsl(0deg 0% 100%); - border: 1px solid hsl(0deg, 1%, 74%); -} - -.badge { - margin: 2px; -} - -.badge-info { - background: hsl(188deg, 100%, 23%); - font-size: 14px; -} - -.fa-trash-alt { - padding: 0 5px; -} - -/* Tag List */ -.tag-response { - opacity: 0; - pointer-events: none; - overflow: scroll; - transition: 300ms; - height: 1px; - margin-top: 2px; -} - -.tag-response.show { - opacity: 1; - pointer-events: all; - transition: 300ms; - height: 180px; -} - -.badge.add-tags { - background: hsl(208, 74%, 57%); - color: white; -} - -.badge.add-tags.active { - background: hsl(120, 0%, 57%); -} - -.tag-item { - display: flex; - margin: 5px 0; - opacity: 1; - pointer-events: none; -} - -.btn.tag { - background: none; - border: 1px solid hsl(0, 0%, 75%); - pointer-events: all; - opacity: 1; -} - -.badge-light { - border: 1px solid grey; -} - -.tags { - display: flex; - flex-wrap: wrap; - min-width: 1px; -} - -.tags .btn.badge-primary:hover { - background-color: hsl(0, 64%, 61%); - transition: 300ms; - cursor: pointer; -} - -.tags .btn.badge-add:hover { - background-color: hsl(110, 76%, 31%); - transition: 300ms; - cursor: pointer; -} - -.tags .btn.badge-add { - background: hsl(0deg 0% 32%); - color: hsl(0, 0%, 100%); -} - -/* New Tag Modal */ -.new-tag-inputs { - margin: 5px 0; - display: flex; - flex-direction: column; - width: 100%; -} - -.new-tag-inputs label { - text-align: left; -} - -.new-tag-inputs input { - width: 100%; -} - -/* Reviewers Section */ -.reviewers-section .list { - display: flex; - flex-wrap: wrap; - align-items: center; - margin: 12px 0; -} - -.reviewers-section .list .reviewer { - box-shadow: 1px 1px 5px hsl(0deg, 0%, 75%); - margin-right: 20px; - padding: 3px 15px; +.application-list { + padding: 20px; + background-color: #f9f9f9; border-radius: 8px; - margin: 10px 5px; - height: 3.5em; -} - -.reviewers-section .list .add-reviewer .btn { - margin: 0 10px; - font-size: 13px; - border: 1px solid hsl(0, 0%, 91%); -} - -.reviewers-section p { - margin: 0 8px 0 0; - font-size: 13px; -} - -.reviewers-section h4 { - font-size: 17px; - font-weight: bold; -} - -.reviewers-section h6 { - font-size: 15px; - color: grey; + } + + .application-list h3 { + margin-bottom: 15px; + font-size: 1.5em; + color: #333; + } + + .application-list_items { + list-style-type: none; + padding: 0; margin: 0; - padding-left: 7px; - padding-bottom: 4px; - padding-right: 4px; -} - -.reviewers-section h5 { - margin-top: 11px; - color: grey; -} - -.reviewer div { - display: flex; -} - -.divider { - height: 1px; - background: lightgrey; - width: 100%; - margin: 35px auto; -} - -/* Review Modal */ - -.review-select { - background: none; - border: none; - outline: none; - text-align: left; - box-shadow: 1px 2px 6px hsl(0, 0%, 53%); - cursor: pointer; - border-radius: 10px; - padding: 5px 30px 5px 15px; -} - -.review-select label { - color: hsl(208deg, 84%, 59%) -} - - -.review-select:hover { - background: hsl(0, 0%, 96%); - color: hsl(0, 0%, 19%); -} - -.review-select div { - display: flex; -} - -html button:focus { - border: none; - outline: none; -} - - -.review-select.active { - background:hsl(208deg, 84%, 59%); - border: none; - outline: none; -} - -.trash-review { - background: none; - border: none; - outline: none -} - -.review-select p { -color: hsl(0, 0%, 38%); -font-size: 12px; -margin-right: 8px; -margin-bottom: 4px; -} - -.review-select.active label { -color: white; -font-size: 15px; -} - -.review-select.active p { - color: hsl(0, 0%, 100%); -} - -.review-completed { - color: hsl(120, 41%, 47%); -} - -.review-not-started { - color: hsl(0, 64%, 61%); -} - -.review-started { - color: #e8a64a; -} - - -/* Response Details Section and Sub-Sections */ -.response-details { - margin-top: 20px; - width: 100%; - padding: 2%; - background: white; - box-shadow: 1px 2px 6px hsl(0, 0%, 53%); - min-height: 40px; - padding-bottom: 40px; - text-align: left; - border-radius: 4px; -} - -.section { - padding: 10px 0; -} - -.review-select .reviewer-email { - color: grey; - font-size: 12px; - margin-bottom: 0.75em; -} - -/* Q&A Sections */ -.Q-A { - display: flex; - flex-wrap: wrap; -} - -.question-answer-block { - width: 100%; + } + + .application-list_item { + margin-bottom: 10px; padding: 10px; - border: 1px solid #e8e8e8; - margin: 5px; - background: hsl(0deg 0% 98%); - border-radius: 2px; -} - -.question-answer-block p { - color: grey; - font-size: 17px; -} - -.question-answer-block p.answer { - color: #212529; - font-size: 17px; - white-space: pre-wrap; -} - - html .answerer { + background-color: #ffffff; + border-radius: 6px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + transition: all 0.3s ease; + } + + .application-list_item:hover { + transform: translateY(-3px); + } + + .application-list_link { + text-decoration: none; + color: #333; font-weight: bold; - font-size: 13px; -} - -.answer.file { - color: skyblue; - text-decoration: underline; -} - - -/* Response Details Type Face */ -.response-details h3 { - color: hsl(213deg 16% 49%); - margin: 0; - padding-left: 7px; - padding-bottom: 4px; - padding-right: 4px; -} - -.question-answer-block .question-description { - font-size: 0.9em; -} - -.question-answer-block .question-headline { - color: #666; -} \ No newline at end of file + display: block; + transition: color 0.3s ease; + } + + .application-list_link:hover { + color: #007BFF; + } + + .shift_button{ + margin-left: 30px; + } + \ No newline at end of file diff --git a/webapp/src/pages/ReponseDetails/components/ReviewModal.js b/webapp/src/pages/ReponseDetails/components/ReviewModal.js deleted file mode 100644 index bbd33c2f..00000000 --- a/webapp/src/pages/ReponseDetails/components/ReviewModal.js +++ /dev/null @@ -1,121 +0,0 @@ -import React, { Component } from "react"; - -class ReviewModal extends Component { - constructor(props) { - super(props); - this.state = { - selectedReviewer: null, - }; - } - - handlePost(reviewers) { - this.props.handlePost(reviewers); - - this.setState({ - selectedReviewer: null, - }); - } - - handleSelect(reviewer) { - this.setState({ - selectedReviewer: reviewer, - }); - } - - renderModal() { - const { selectedReviewer } = this.state; - - const { reviewers, t } = this.props; - - if (this.props.event) { - return ( - - ); - } - } - - render() { - const modal = this.renderModal(); - return
{modal}
; - } -} - -export default ReviewModal; diff --git a/webapp/src/pages/ResponseList/components/ResponseListComponent.js b/webapp/src/pages/ResponseList/components/ResponseListComponent.js index e6876a66..17ccd89d 100644 --- a/webapp/src/pages/ResponseList/components/ResponseListComponent.js +++ b/webapp/src/pages/ResponseList/components/ResponseListComponent.js @@ -266,7 +266,7 @@ class ResponseListComponent extends Component { minWidth: 150 }, { - id: "email", + id: "language", Header:
{t("Language")}
, accessor: u => u.language, minWidth: 80 diff --git a/webapp/src/pages/ResponsePage/ResponsePage.js b/webapp/src/pages/ResponsePage/ResponsePage.js index 2fe35406..ad73d756 100644 --- a/webapp/src/pages/ResponsePage/ResponsePage.js +++ b/webapp/src/pages/ResponsePage/ResponsePage.js @@ -203,7 +203,6 @@ class ResponsePage extends Component { outcomeService .getOutcome(this.props.event.id, this.state.applicationData.user_id,this.props.match.params.id) .then((response) => { - console.log(this.props); if (response.status === 200) { const newOutcome = { timestamp: response.outcome.timestamp, @@ -247,14 +246,6 @@ class ResponsePage extends Component { }); } - openConfirmationModal = (outcome, message) => { - this.setState({ - confirmModalVisible: true, - pendingOutcome: outcome, - confirmationMessage: message, - }); - }; - handleConfirmation = (outcome, message) => { this.setState({ confirmModalVisible: true, @@ -287,7 +278,7 @@ class ResponsePage extends Component { this.handleConfirmation(outcome, message) } > - {label} + {this.props.t(label)} ); } @@ -341,7 +332,7 @@ class ResponsePage extends Component { "REJECT_W_ENCOURAGEMENT", "Reject with Encouragement to Resubmit", "btn-warning", - "Are you sure you want to REJECT WITH ENCOURAGEMENT TO RESUBMIT??" + "Are you sure you want to REJECT WITH ENCOURAGEMENT TO RESUBMIT?" ) ); } diff --git a/webapp/src/pages/applicationForm/Application.css b/webapp/src/pages/applicationForm/Application.css index 69e268e9..18974d7c 100644 --- a/webapp/src/pages/applicationForm/Application.css +++ b/webapp/src/pages/applicationForm/Application.css @@ -222,3 +222,7 @@ ol.progtrckr li.progtrckr-done:hover:before { display: inline; } } + +.shift_button { + margin-left: 50px; +} \ No newline at end of file diff --git a/webapp/src/pages/applicationForm/components/ApplicationForm.js b/webapp/src/pages/applicationForm/components/ApplicationForm.js index 40c3b552..7914685d 100644 --- a/webapp/src/pages/applicationForm/components/ApplicationForm.js +++ b/webapp/src/pages/applicationForm/components/ApplicationForm.js @@ -768,8 +768,7 @@ class SubmittedComponent extends React.Component { + />
+ /> handleLanguageChange(e)} + onChange={e => handleLanguageChange(e)} value={language} defaultValue={language} - className="select-language" + className='select-language' styles={{ control: (base, state) => ({ ...base, boxShadow: "none", border: state.isFocused && "none", - transition: - state.isFocused && "color,background-color 1.5s ease-out", - background: state.isFocused && "lightgray", - color: "#fff", + transition: state.isFocused && 'color,background-color 1.5s ease-out', + background: state.isFocused && 'lightgray', + color: '#fff' }), option: (base, state) => ({ - ...base, - backgroundColor: state.isFocused && "#1f2d3e", - color: state.isFocused && "#fff", - }), + ...base, + backgroundColor: state.isFocused && "#1f2d3e", + color: state.isFocused && "#fff" + }) }} menuPlacement="auto" /> @@ -252,41 +223,44 @@ const FormCreator = ({ applyTransition={applyTransition} setApplytransition={setApplytransition} > - {sections.map((section, i) => ( -
- ))} + { + sections + .map((section, i) => ( +
+ )) + }
)} - ); -}; + ) +} -export default FormCreator; +export default FormCreator; \ No newline at end of file From b664b979f3dfd27fae0906ac214f1d27e2cb02a6 Mon Sep 17 00:00:00 2001 From: Yvan CARRE Date: Sun, 15 Dec 2024 11:06:18 +0200 Subject: [PATCH 07/17] new changes --- webapp/public/locales/en/translation.json | 3 +-- webapp/public/locales/fr/translation.json | 10 +++++----- webapp/src/pages/ResponsePage/ResponsePage.js | 2 ++ 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/webapp/public/locales/en/translation.json b/webapp/public/locales/en/translation.json index 05dc1c70..4e25741b 100644 --- a/webapp/public/locales/en/translation.json +++ b/webapp/public/locales/en/translation.json @@ -592,5 +592,4 @@ "Reject with Encouragement to Resubmit": "Reject with Encouragement to Resubmit", "Yes - Confirm":"Yes - Confirm", "No - Don't confirm":"No - Don't confirm" - -} +} \ No newline at end of file diff --git a/webapp/public/locales/fr/translation.json b/webapp/public/locales/fr/translation.json index 707b04c4..9a425321 100644 --- a/webapp/public/locales/fr/translation.json +++ b/webapp/public/locales/fr/translation.json @@ -358,7 +358,7 @@ "Application opens ": "L'application s'ouvre ", "Application closes": "L'application se ferme", "sectionPlace": "Section {{index}} de {{numSections}}", - "Add Section": "Ajouter Section", + "Add Section": "Ajouter Section", "Delete Section": "Suprimer Section", "Duplicate Section": "Dupliquer section", "Add Question": "Ajouter Question", @@ -489,8 +489,8 @@ "No event exists with that KEY": "Aucun événement n'existe avec cette clé", "Event with that KEY already exists": "Un événement avec cette clé existe déjà", "Translation for event not found": "Traduction pour l'événement introuvable", - "Event must contain at least one translation for 'name' and 'description'": "L'événement doit contenir au moins une traduction pour 'nom' et 'description'", - "Event must contain same translations for 'name' and 'description'": "L'événement doit contenir les mêmes traductions pour 'nom' et 'description'", + "Event must contain at least one translation for 'name' and 'description'" : "L'événement doit contenir au moins une traduction pour 'nom' et 'description'", + "Event must contain same translations for 'name' and 'description'" : "L'événement doit contenir les mêmes traductions pour 'nom' et 'description'", "Stripe setup has not yet been completed.": "La configuration de Stripe n'a pas encore été terminée.", "review-assignment-filter-note": "Si vous avez sélectionné des filtres de balises, seules les réponses correspondant à ces balises seront attribuées. Notez que le nombre total de révisions non attribuées tient compte des filtres de balises, mais pas le nombre attribué et le nombre terminé dans le tableau.", "Configure Tags": "Configurer les balises", @@ -516,14 +516,14 @@ "Reviewers": "Évaluateurs", "ID": "ID", "None": "Aucun", - "Loading responses...": "Chargement des réponses...", + "Loading responses...": "Chargement des réponses...", "Add tag": "Ajouter une balise", "Save Tag": "Enregistrer la balise", "We are pleased to offer you the following grants": "Nous sommes heureux de vous offrir les bourses suivantes", "I accept this grant": "J'accepte cette bourse", "Unfortunately we are unable to award you any grants for this event": "Malheureusement, nous ne pouvons pas vous attribuer de bourses pour cet événement", "We are pleased to offer you a place at": "Nous sommes heureux de vous offrir une place à", - "Please see the details of this offer below": "Veuillez consulter les détails de cette offre ci-dessous", + "Please see the details of this offer below" : "Veuillez consulter les détails de cette offre ci-dessous", "Offer Details": "Détails de l'offre", "registrationFee": "Le paiement de {{paymentAmount}} {{paymentCurrency}} est requis pour confirmer votre place.", "Please accept or reject this offer by": "Veuillez accepter ou rejeter cette offre avant le", diff --git a/webapp/src/pages/ResponsePage/ResponsePage.js b/webapp/src/pages/ResponsePage/ResponsePage.js index ad73d756..ba07f4ec 100644 --- a/webapp/src/pages/ResponsePage/ResponsePage.js +++ b/webapp/src/pages/ResponsePage/ResponsePage.js @@ -910,3 +910,5 @@ class ResponsePage extends Component { } export default withTranslation()(ResponsePage); + + From e969592b1a080d3cca2ad69f0f32f73de7f7e5ba Mon Sep 17 00:00:00 2001 From: Yvan CARRE Date: Sun, 15 Dec 2024 11:06:30 +0200 Subject: [PATCH 08/17] new changes --- webapp/src/pages/ResponsePage/ResponsePage.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/webapp/src/pages/ResponsePage/ResponsePage.js b/webapp/src/pages/ResponsePage/ResponsePage.js index ba07f4ec..b40037c9 100644 --- a/webapp/src/pages/ResponsePage/ResponsePage.js +++ b/webapp/src/pages/ResponsePage/ResponsePage.js @@ -909,6 +909,4 @@ class ResponsePage extends Component { } } -export default withTranslation()(ResponsePage); - - +export default withTranslation()(ResponsePage); \ No newline at end of file From 10a920da749cd6bbdac0d60fc8fee55def357ea8 Mon Sep 17 00:00:00 2001 From: Yvan CARRE Date: Mon, 16 Dec 2024 20:32:09 +0200 Subject: [PATCH 09/17] new changes --- api/app/responses/api.py | 1 + api/app/responses/models.py | 2 +- webapp/public/locales/en/translation.json | 3 +- webapp/public/locales/fr/translation.json | 3 +- .../pages/ReponseDetails/ReponseDetails.js | 48 ++-- .../src/pages/applicationForm/Application.css | 67 +++++- .../components/ApplicationForm.js | 209 ++++++++++++------ .../components/ApplicationForm.js | 2 + 8 files changed, 237 insertions(+), 98 deletions(-) diff --git a/api/app/responses/api.py b/api/app/responses/api.py index f0d98955..fbbcccc4 100644 --- a/api/app/responses/api.py +++ b/api/app/responses/api.py @@ -378,6 +378,7 @@ def get(self, event_id): language = args['language'] responses = response_repository.get_all_for_event(event_id, not include_unsubmitted) + review_config = review_configuration_repository.get_configuration_for_event(event_id) required_reviewers = 1 if review_config is None else review_config.num_reviews_required + review_config.num_optional_reviews diff --git a/api/app/responses/models.py b/api/app/responses/models.py index a8dc7e0c..fba4a1b7 100644 --- a/api/app/responses/models.py +++ b/api/app/responses/models.py @@ -46,7 +46,7 @@ def __init__(self, application_form_id, user_id, language, parent_id=None): self.submitted_timestamp = None self.is_withdrawn = False self.withdrawn_timestamp = None - self.started_timestamp = date.today() + self.started_timestamp = datetime.now() self.language = language self.parent_id = parent_id diff --git a/webapp/public/locales/en/translation.json b/webapp/public/locales/en/translation.json index 4e25741b..497c2bba 100644 --- a/webapp/public/locales/en/translation.json +++ b/webapp/public/locales/en/translation.json @@ -591,5 +591,6 @@ "Accept with Minor Revision": "Accept with Minor Revision", "Reject with Encouragement to Resubmit": "Reject with Encouragement to Resubmit", "Yes - Confirm":"Yes - Confirm", - "No - Don't confirm":"No - Don't confirm" + "No - Don't confirm":"No - Don't confirm", + "PENDING": "PENDING" } \ No newline at end of file diff --git a/webapp/public/locales/fr/translation.json b/webapp/public/locales/fr/translation.json index 9a425321..4f48f817 100644 --- a/webapp/public/locales/fr/translation.json +++ b/webapp/public/locales/fr/translation.json @@ -590,5 +590,6 @@ "Accept with Minor Revision": "Accepter avec des révisions mineures", "Reject with Encouragement to Resubmit": "Rejeter avec encouragement à resoumettre", "Yes - Confirm": "Oui - Confirmer", - "No - Don't confirm": "Non - Ne pas confirmer" + "No - Don't confirm": "Non - Ne pas confirmer", + "PENDING": "EN ATTENTE" } diff --git a/webapp/src/pages/ReponseDetails/ReponseDetails.js b/webapp/src/pages/ReponseDetails/ReponseDetails.js index 8334b721..adbd94bc 100644 --- a/webapp/src/pages/ReponseDetails/ReponseDetails.js +++ b/webapp/src/pages/ReponseDetails/ReponseDetails.js @@ -1,6 +1,5 @@ import React, { Component } from "react"; import { withRouter } from "react-router"; -import { Link } from "react-router-dom"; import { applicationFormService } from "../../services/applicationForm"; import Loading from "../../components/Loading"; import { withTranslation } from "react-i18next"; @@ -37,8 +36,10 @@ class ReponseDetails extends Component { ), isLoading: false, error: responses[0].error || responses[1].error || responses[2].error, + }); }); + } renderSections() { @@ -46,16 +47,13 @@ class ReponseDetails extends Component { const applicationData = this.state.applicationData; let html = []; - // main function if (applicationForm && applicationData) { applicationForm.sections.forEach((section) => { html.push(
- {/*Heading*/}

{section.name}

- {/*Q & A*/}
{this.renderResponses(section)}
); @@ -155,7 +153,7 @@ class ReponseDetails extends Component { return ( - {"Pending ..."} + {this.props.t("PENDING")} ); @@ -190,41 +188,25 @@ class ReponseDetails extends Component { } const result = getChain(elementMap[id]); - result.sort((a, b) => a.id - b.id); - + result.sort((a, b) => b.id - a.id); return result; }; - hasAcceptedStatus = (chain) => { - return chain.some((element) => element.outcome === "ACCEPTED"); - }; - - getLastId = (data) => { - if (!data || data.length === 0) return null; - let lastId = null; - data.forEach((element) => { - if (lastId === null || element.id > lastId) { - lastId = element.id; - } - }); - return lastId; - }; getSubmissionList = (applications) => { if (!applications || applications.length === 0) { return

No submissions available.

; } - return (

Related Submissions

    - {applications.map((application) => ( + {applications.map((application,index) => (
  • - {this.props.t(`Submission ${application.id}`)} + {this.props.t(`Submission`) + " " + this.formatDate(application.submitted_timestamp)}
  • ))} @@ -233,12 +215,22 @@ class ReponseDetails extends Component { ); }; + getLastResponse(response) { + const lastResponse = response[0]; + if (lastResponse.outcome!=null && lastResponse.outcome !== 'ACCEPTED') { + return true; + } + return false; +} + + render() { const { applicationData, isLoading, error, all_responses } = this.state; if (isLoading) { return ; } - const result = this.getChainById(all_responses, this.props.match.params.id); + const chain_responses = this.getChainById(all_responses, this.props.match.params.id); + return (
    {error && ( @@ -273,9 +265,9 @@ class ReponseDetails extends Component { onClick={() => (window.location.href = `/${ this.props.event.key - }/apply/new/${this.getLastId(result)}`) + }/apply/new/${chain_responses[0].id}`) } - disabled={this.hasAcceptedStatus(result)} + disabled={!this.getLastResponse(chain_responses)} > {this.props.t("New submission")} @@ -285,7 +277,7 @@ class ReponseDetails extends Component { )}
    {this.getSubmissionList( - result.filter( + chain_responses.filter( (element) => element.id !== parseInt(this.props.match.params.id) ) )} diff --git a/webapp/src/pages/applicationForm/Application.css b/webapp/src/pages/applicationForm/Application.css index 18974d7c..4bc53f64 100644 --- a/webapp/src/pages/applicationForm/Application.css +++ b/webapp/src/pages/applicationForm/Application.css @@ -225,4 +225,69 @@ ol.progtrckr li.progtrckr-done:hover:before { .shift_button { margin-left: 50px; -} \ No newline at end of file +} + +.response-chains-container { + background-color: #f8f9fa; + padding: 20px; + border-radius: 10px; +} + +.response-chains-title { + text-align: center; + margin-bottom: 20px; + font-size: 28px; + color: #333; + font-weight: bold; +} + +.response-chains-table { + width: 100%; + border-collapse: collapse; + background-color: #fff; + border-radius: 10px; + overflow: hidden; +} + +.response-chains-header { + padding: 15px; + font-size: 20px; + background-color: #007bff; + color: #fff; + text-align: left; +} + +.chain-header { + cursor: pointer; + transition: background-color 0.3s ease; +} + +.chain-header-title { + padding: 15px; + font-weight: bold; + font-size: 18px; + border-bottom: 2px solid #007bff; +} + +.chain-toggle-icon { + float: right; + font-size: 18px; +} + +.response-row { + background-color: #ffffff; + border-bottom: 1px solid #dcdcdc; + transition: background-color 0.3s ease; +} + +.resubmit-button-container { + text-align: center; + padding: 15px; + background-color: #f9f9f9; +} + +.btn.btn-primary { + padding: 10px 20px; + font-size: 16px; + border-radius: 5px; +} diff --git a/webapp/src/pages/applicationForm/components/ApplicationForm.js b/webapp/src/pages/applicationForm/components/ApplicationForm.js index 7914685d..f84d35bd 100644 --- a/webapp/src/pages/applicationForm/components/ApplicationForm.js +++ b/webapp/src/pages/applicationForm/components/ApplicationForm.js @@ -1,4 +1,4 @@ -import React, { Component } from "react"; +import React, { Component,Fragment } from "react"; import { withRouter } from "react-router"; import { Link } from "react-router-dom"; import { applicationFormService } from "../../../services/applicationForm"; @@ -23,6 +23,7 @@ import FormSelectOther from "../../../components/form/FormSelectOther"; import FormMultiCheckboxOther from "../../../components/form/FormMultiCheckboxOther"; import FormCheckbox from "../../../components/form/FormCheckbox"; import { eventService } from "../../../services/events"; +import moment from "moment"; const baseUrl = process.env.REACT_APP_API_URL; @@ -1154,51 +1155,25 @@ class ApplicationListComponent extends Component { this.state = {}; } - getCandidate = (allQuestions, response) => { - const nominating_capacity = answerByQuestionKey( - "nominating_capacity", - allQuestions, - response.answers - ); - if (nominating_capacity === "other") { - let firstname = answerByQuestionKey( - "nomination_firstname", - allQuestions, - response.answers - ); - let lastname = answerByQuestionKey( - "nomination_lastname", - allQuestions, - response.answers - ); - return firstname + " " + lastname; - } - return this.props.event.event_type === "JOURNAL" - ? this.props.t("Submission") + " " + response.id - : this.props.t("Self Nomination"); + formatDate = (dateString) => { + return moment(dateString).format("D MMM YYYY, H:mm:ss [(UTC)]"); }; - getParent = (allQuestions, response) => { - const nominating_capacity = answerByQuestionKey( - "nominating_capacity", - allQuestions, - response.answers - ); + + getCandidate = (allQuestions, response) => { + const nominating_capacity = answerByQuestionKey("nominating_capacity", allQuestions, response.answers); if (nominating_capacity === "other") { - let firstname = answerByQuestionKey( - "nomination_firstname", - allQuestions, - response.answers - ); - let lastname = answerByQuestionKey( - "nomination_lastname", - allQuestions, - response.answers - ); + let firstname = answerByQuestionKey("nomination_firstname", allQuestions, response.answers); + let lastname = answerByQuestionKey("nomination_lastname", allQuestions, response.answers); return firstname + " " + lastname; } - return this.props.event.event_type === "JOURNAL" - ? (response.parent_id ? this.props.t(`Submission ${response.parent_id}`): this.props.t(" ")) : this.props.t("Self Nomination")}; + return this.props.event.event_type ==='JOURNAL' ? this.props.t("Submission") + " " + response.id : this.props.t("Self Nomination"); + } + + + getSubmission = (response) => { + return this.props.t("Submission") + " - " + (response.is_submitted==true?this.formatDate(response.submitted_timestamp):this.formatDate(response.started_timestamp)) + }; getStatus = (response) => { if (response.is_submitted) { @@ -1213,24 +1188,21 @@ class ApplicationListComponent extends Component { const applicationData = this.state.applicationData; let html = []; - // main function if (applicationForm && applicationData) { applicationForm.sections.forEach((section) => { html.push(
    - {/*Heading*/}

    {section.name}

    - {/*Q & A*/}
    {this.renderResponses(section)}
    ); }); } - return html; } + renderResponses(section) { const applicationData = this.state.applicationData; const questions = section.questions.map((q) => { @@ -1303,6 +1275,97 @@ class ApplicationListComponent extends Component { ); }; + newSubmission = (id) => { + window.location.href = `/${this.props.event.key}/apply/new/${id}`; + }; + + getLastResponse = (response) => { + if (response.children.length === 0) return response; + return this.getLastResponse(response.children[response.children.length - 1]); + }; + + buildResponseChains = (responses) => { + const responseMap = responses.reduce((map, response) => { + map[response.id] = { ...response, children: [] }; + return map; + }, {}); + + const chains = []; + + responses.forEach((response) => { + if (response.parent_id !== null && responseMap[response.parent_id]) { + responseMap[response.parent_id].children.push(responseMap[response.id]); + } else { + chains.push(responseMap[response.id]); + } + }); + + return chains.sort((a, b) => this.getLastResponse(b).id - this.getLastResponse(a).id); + }; + + toggleChain = (chainId) => { + this.setState((prevState) => { + const expandedChains = prevState.expandedChains ? { ...prevState.expandedChains } : {}; + expandedChains[chainId] = !expandedChains[chainId]; + return { expandedChains }; + }); + }; + + renderResubmitButton = (chain) => { + const lastResponse = this.getLastResponse(chain); + if (lastResponse.outcome !== 'ACCEPTED' && lastResponse.outcome !== null) { + return ( + + + + + + ); + } + return null; + }; + + renderEntireChain = (response) => { + const children = response.children.slice().reverse(); + return ( + + {children.map((childResponse) => this.renderEntireChain(childResponse))} + + {this.getSubmission(response)} + {this.getStatus(response)} + {this.getOutcome(response)} + {this.getAction(response)} + + + ); + }; + + renderResponseChain = (chain) => { + return ( + + this.toggleChain(chain.id)} className="chain-header"> + + {this.props.t("Submission")} {this.formatDate(chain.started_timestamp)} + {this.state.expandedChains && this.state.expandedChains[chain.id] ? "▲" : "▼"} + + + {/* {this.state.expandedChains && this.state.expandedChains[chain.id] && ( + <> + {renderEntireChain(chain)} + {renderResubmitButton(chain)} + + )} */} + {this.state.expandedChains && this.state.expandedChains[chain.id] && + this.renderEntireChain(chain) + } + {this.renderResubmitButton(chain)} + + ); + + }; + + + render() { let allQuestions = _.flatMap( this.props.formSpec.sections, @@ -1316,35 +1379,47 @@ class ApplicationListComponent extends Component { this.props.event.event_type === "JOURNAL" ? this.props.t("Submission") : this.props.t("Nominee"); - return ( -
    -

    {title}

    - + + + if (this.props.event.event_type =='JOURNAL') { + const responseChains = this.buildResponseChains(this.props.responses); + return ( +
    +

    {title}

    +
    - - - - - + - {this.props.responses.map((response) => { - return ( - - - - - - - - ); - })} + {responseChains.map((chain, index) => this.renderResponseChain(chain, index))}
    {firstColumn}{this.props.t("Following")}{this.props.t("Status")}{this.props.t("Results")}{firstColumn}
    {this.getCandidate(allQuestions, response)}{this.getParent(allQuestions, response)}{this.getStatus(response)}{this.getOutcome(response)}{this.getAction(response)}
    - ); + ); + } + return
    +

    {title}

    + + + + + + + + + + {this.props.responses.map(response => { + return + + + + + })} + +
    {firstColumn}{this.props.t("Status")}
    {this.getCandidate(allQuestions, response)}{this.getStatus(response)}{this.getAction(response)}
    +
    ; } } @@ -1466,6 +1541,7 @@ class ApplicationForm extends Component { click={this.responseSelected} />
    +
    diff --git a/webapp/src/pages/ResponsePage/ResponsePage.js b/webapp/src/pages/ResponsePage/ResponsePage.js index b40037c9..26057ba3 100644 --- a/webapp/src/pages/ResponsePage/ResponsePage.js +++ b/webapp/src/pages/ResponsePage/ResponsePage.js @@ -282,23 +282,27 @@ class ResponsePage extends Component { ); } + outcomeStatus() { const data = this.state.applicationData; - if (data) { if (this.state.outcome.status && this.state.outcome.status !== "REVIEW") { - const badgeClass = - this.state.outcome.status === "ACCEPTED" + const badgeClass = this.state.outcome.status === "ACCEPTED" ? "badge-success" : this.state.outcome.status === "REJECTED" ? "badge-danger" : "badge-warning"; + + const outcome= this.state.outcome.status ==='ACCEPTED'?this.props.t("ACCEPTED"):this.state.outcome.status ==='REJECTED'? + this.props.t("REJECTED"):this.state.outcome.status ==='ACCEPT_W_REVISION'? + this.props.t("ACCEPTED WITH REVISION"):this.state.outcome.status ==='REJECT_W_ENCOURAGEMENT'? + this.props.t("REJECTED WITH ENCOURAGEMENT"):this.props.t("REVIEWING"); return ( - {this.state.outcome.status} + {outcome} {" "} {this.formatDate(this.state.outcome.timestamp)} @@ -846,7 +850,7 @@ class ResponsePage extends Component { {" "}

    {this.applicationStatus()}

    {" "} -

    {this.outcomeStatus()}

    +

    {this.outcomeStatus(applicationData)}

); } + if (this.props.chain) { + const isIdAndNotParent=(listResponse, id) =>{ + const isIdPresent = listResponse.some(entry => entry.id === id); + const isNotAParent = !listResponse.some(entry => entry.parent_id === id); + return isIdPresent && isNotAParent; + } + if (!isIdAndNotParent(responses, parseInt(this.props.match.params.id))) { + return + } + return ( + + ); + } if (this.props.continue) { const response = listselectedResponse.find( (item) => item.id === parseInt(this.props.match.params.id)); if (response == null || response.outcome != null) { - return + return } return ( } + render={(props) => } /> Date: Tue, 17 Dec 2024 08:50:32 +0200 Subject: [PATCH 14/17] new changes --- .../components/ApplicationForm.js | 479 +++++++----------- 1 file changed, 196 insertions(+), 283 deletions(-) diff --git a/webapp/src/pages/applicationForm/components/ApplicationForm.js b/webapp/src/pages/applicationForm/components/ApplicationForm.js index 07da7454..e2a4b8ae 100644 --- a/webapp/src/pages/applicationForm/components/ApplicationForm.js +++ b/webapp/src/pages/applicationForm/components/ApplicationForm.js @@ -79,10 +79,11 @@ class FieldEditor extends React.Component { uploadPercentComplete: 0, uploadError: "", uploaded: false, - }; + } + } - handleChange = (event) => { + handleChange = event => { // Some components (datepicker, custom controls) return pass the value directly rather than via event.target.value const value = event && event.target ? event.target.value : event; @@ -91,12 +92,12 @@ class FieldEditor extends React.Component { } }; - handleCheckChange = (event) => { + handleCheckChange = event => { const value = event.target.checked; if (this.props.onChange) { this.props.onChange(this.props.question, value); } - }; + } handleChangeDropdown = (name, dropdown) => { if (this.props.onChange) { @@ -107,36 +108,33 @@ class FieldEditor extends React.Component { handleUploadFile = (file) => { this.setState({ uploading: true, - }); + }) // TODO: Handle errors - return fileService - .uploadFile(file, (progressEvent) => { - const percentCompleted = Math.round( - (progressEvent.loaded * 100) / progressEvent.total + return fileService.uploadFile(file, progressEvent => { + const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total); + this.setState({ + uploadPercentComplete: percentCompleted + }); + }).then(response => { + if (response.fileId && this.props.onChange) { + this.props.onChange( + this.props.question, + JSON.stringify({ filename: response.fileId, rename: file.name }) ); - this.setState({ - uploadPercentComplete: percentCompleted, - }); - }) - .then((response) => { - if (response.fileId && this.props.onChange) { - this.props.onChange( - this.props.question, - JSON.stringify({ filename: response.fileId, rename: file.name }) - ); - } - this.setState({ - uploaded: response.fileId !== "", - uploadError: response.error, - uploading: false, - }); - - return response.fileId; + } + this.setState({ + uploaded: response.fileId !== "", + uploadError: response.error, + uploading: false }); - }; + + return response.fileId; + }); + } formControl = (key, question, answer, validationError, responseId) => { + switch (question.type) { case SHORT_TEXT: return ( @@ -210,9 +208,8 @@ class FieldEditor extends React.Component { defaultValue={answer || null} key={"i_" + key} showError={validationError} - errorText={validationError} - /> - ); + errorText={validationError}/> + ) case MULTI_CHECKBOX: return ( - ); - case MULTI_CHECKBOX_OTHER: - return ( - - ); + errorText={validationError} /> + ) + case MULTI_CHECKBOX_OTHER: + return ( + + ) case FILE: return ( - ); + responseId={responseId} /> + ) case INFORMATION[0]: case INFORMATION[1]: - return ( - question.description && ( -
- {question.description} -
- ) - ); + return question.description &&
{question.description}
default: return (

@@ -327,14 +315,8 @@ class FieldEditor extends React.Component { render() { return (

-

- {this.props.question.is_required && ( - * - )} +

+ {this.props.question.is_required && *} {this.props.question.headline}

{this.formControl( @@ -359,32 +341,36 @@ class Section extends React.Component { .sort((a, b) => a.question.order - b.question.order), hasValidated: false, validationStale: false, + }; } + onChange = (question, value) => { const newAnswer = { question_id: question.id, - value: value, + value: value }; - const newQuestionModels = this.state.questionModels.map((q) => { - if (q.question.id !== question.id) { - return q; - } - return { - ...q, - validationError: this.state.hasValidated - ? this.validate(q, newAnswer) - : "", - answer: newAnswer, - }; - }); + + const newQuestionModels = this.state.questionModels + .map(q => { + if (q.question.id !== question.id) { + return q; + } + return { + ...q, + validationError: this.state.hasValidated + ? this.validate(q, newAnswer) + : "", + answer: newAnswer + }; + }) this.setState( { questionModels: newQuestionModels, - validationStale: true, + validationStale: true }, () => { if (this.props.changed) { @@ -394,10 +380,11 @@ class Section extends React.Component { ); }; + // validate validate = (questionModel, updatedAnswer) => { let errors = []; - + const question = questionModel.question; const answer = updatedAnswer || questionModel.answer; @@ -416,32 +403,31 @@ class Section extends React.Component { return errors.join("; "); }; + // isValidated isValidated = () => { - const allAnswersInSection = this.state.questionModels.map((q) => q.answer); + const allAnswersInSection = this.state.questionModels.map(q => q.answer); const validatedModels = this.state.questionModels - .filter((q) => - this.dependentQuestionFilter(q.question, allAnswersInSection) - ) - .map((q) => { + .filter(q => this.dependentQuestionFilter(q.question, allAnswersInSection)) + .map(q => { return { ...q, - validationError: this.validate(q), + validationError: this.validate(q) }; }); - const isValid = !validatedModels.some((v) => v.validationError); + const isValid = !validatedModels.some(v => v.validationError); this.setState( { questionModels: validatedModels, hasValidated: true, - validationStale: false, + validationStale: false }, () => { if (this.props.answerChanged) { this.props.answerChanged( - this.state.questionModels.map((q) => q.answer).filter((a) => a), + this.state.questionModels.map(q => q.answer).filter(a => a), isValid ); } @@ -454,7 +440,7 @@ class Section extends React.Component { handleSave = () => { if (this.props.save) { this.props.save( - this.state.questionModels.map((q) => q.answer).filter((a) => a) + this.state.questionModels.map(q => q.answer).filter(a => a) ); } }; @@ -465,38 +451,35 @@ class Section extends React.Component { */ dependentQuestionFilter = (question, sectionCurrentAnswers) => { if (isEntityDependentOnAnswer(question)) { - const answer = findDependentQuestionAnswer( - question, - sectionCurrentAnswers - ); - return answer - ? doesAnswerMatch(question, answer) - : this.props.showQuestionBasedOnSavedFormAnswers(question); + const answer = findDependentQuestionAnswer(question, sectionCurrentAnswers); + return answer ? doesAnswerMatch(question, answer) : this.props.showQuestionBasedOnSavedFormAnswers(question); } else { return true; } - }; + } render() { - const { section, questionModels, hasValidated, validationStale } = - this.state; + const { + section, + questionModels, + hasValidated, + validationStale + } = this.state; - const allAnswersInSection = questionModels.map((q) => q.answer); + const allAnswersInSection = questionModels.map(q => q.answer); return (

{section.name}

- +
{questionModels && questionModels - .filter((q) => - this.dependentQuestionFilter(q.question, allAnswersInSection) - ) - .map((model) => ( + .filter(q => this.dependentQuestionFilter(q.question, allAnswersInSection)) + .map(model => ( - ))} + ) + ) + } {this.props.unsavedChanges && !this.props.isSaving && ( - )} - {this.props.isSaving && ( - {this.props.t("Saving")}... - )} + {this.props.isSaving && {this.props.t("Saving")}...} {hasValidated && !validationStale && (
{this.props.t("Please fix the errors before continuing.")} @@ -525,6 +508,7 @@ class Section extends React.Component { } class ConfirmationComponent extends React.Component { + dependentQuestionFilter = (question, allAnswers) => { if (isEntityDependentOnAnswer(question)) { const answer = findDependentQuestionAnswer(question, allAnswers); @@ -532,123 +516,83 @@ class ConfirmationComponent extends React.Component { } else { return true; } - }; + } render() { const t = this.props.t; - const allAnswers = - this.props.sectionModels && - this.props.sectionModels.flatMap((s) => - s.questionModels.map((q) => q.answer) - ); - const allErrors = - this.props.sectionModels && - this.props.sectionModels - .flatMap((s) => - s.questionModels - .filter((qm) => - this.dependentQuestionFilter(qm.question, allAnswers) - ) - .map((q) => q.validationError) - ) - .filter((e) => e); + const allAnswers = this.props.sectionModels && this.props.sectionModels.flatMap(s=>s.questionModels.map(q=>q.answer)); + const allErrors = this.props.sectionModels + && this.props.sectionModels.flatMap(s=>s.questionModels.filter(qm => this.dependentQuestionFilter(qm.question, allAnswers)).map(q=>q.validationError)).filter(e=>e); return (

{t("Review your Answers")}

-

{t("applicationConfirmationText")}

+

+ {t("applicationConfirmationText")} +

- {" "} - {t( - "You MUST click SUBMIT before the deadline for your application to be considered!" - )} + {t("You MUST click SUBMIT before the deadline for your application to be considered!")}
- {allErrors.length > 0 && ( -
- {t( - "Could not submit your application due to validation errors. Please go back and fix these any try again." - )} -
- )} - {allErrors.length === 0 && ( - - )} + {allErrors.length > 0 &&
+ {t("Could not submit your application due to validation errors. Please go back and fix these any try again.")} +
} + {allErrors.length === 0 && }
+
- {this.props.sectionModels && - this.props.sectionModels - .filter((sm) => sm.questionModels.length > 0) - .map((sm) => { - return ( -
-

{sm.section.name}

- {sm.questionModels && - sm.questionModels - .filter((qm) => - this.dependentQuestionFilter(qm.question, allAnswers) - ) - .map((qm) => { - return ( - qm.question && ( -
-
-
-
{qm.question.headline}
-
-
-
-
-

- - {qm.validationError && ( -

- {qm.validationError} -
- )} -

-
-
-
- ) - ); - })} -
- ); - })} - - {allErrors.length > 0 && ( -
- {t( - "Could not submit your application due to validation errors. Please go back and fix these any try again." - )} -
- )} - {allErrors.length === 0 && ( - - )} + {this.props.sectionModels && + this.props.sectionModels.filter(sm => sm.questionModels.length > 0).map(sm => { + return
+

{sm.section.name}

+ {sm.questionModels && + sm.questionModels.filter(qm => this.dependentQuestionFilter(qm.question, allAnswers)).map(qm => { + return ( + qm.question && ( +
+
+
+
{qm.question.headline}
+
+
+
+
+

+ + {qm.validationError &&

{qm.validationError}
} +

+
+
+
+ ) + ); + })} +
+ })} + + {allErrors.length > 0 &&
+ {t("Could not submit your application due to validation errors. Please go back and fix these any try again.")} +
} + {allErrors.length === 0 && }
); } @@ -667,13 +611,13 @@ class SubmittedComponent extends React.Component { }; } - handleWithdrawOK = (event) => { - applicationFormService.withdraw(this.props.responseId).then((resp) => { + handleWithdrawOK = event => { + applicationFormService.withdraw(this.props.responseId).then(resp => { this.setState( { isError: resp.isError, errorMessage: resp.message, - withdrawModalVisible: false, + withdrawModalVisible: false }, () => { if (this.props.onWithdrawn) { @@ -684,46 +628,43 @@ class SubmittedComponent extends React.Component { }); }; - handleEditOK = (event) => { + handleEditOK = event => { if (this.props.onEdit) { this.props.onEdit(); } }; - handleWithdrawCancel = (event) => { + handleWithdrawCancel = event => { this.setState({ - withdrawModalVisible: false, + withdrawModalVisible: false }); }; - handleWithdraw = (event) => { + handleWithdraw = event => { this.setState({ - withdrawModalVisible: true, + withdrawModalVisible: true }); }; - handleEdit = (event) => { + handleEdit = event => { this.setState({ - editAppModalVisible: true, + editAppModalVisible: true }); }; - cancelEditModal = (event) => { + cancelEditModal = event => { this.setState({ - editAppModalVisible: false, + editAppModalVisible: false }); }; render() { const t = this.props.t; - const initialText = - this.props.event && this.props.event.event_type === "CALL" - ? t("Thank you for responding to the") - : this.props.event.event_type === "JOURNAL" - ? t("Thank you for submitting an article to the") - : t("Thank you for applying for"); - - + const initialText = this.props.event && this.props.event.event_type === "CALL" + ? t("Thank you for responding to the") + : this.props.event.event_type === "JOURNAL" + ? t("Thank you for submitting an article to the") + : t("Thank you for applying for"); return ( @@ -894,6 +806,7 @@ class ApplicationFormInstanceComponent extends Component { }; handleSave = (answers) => { + this.setState( (prevState) => { return { From 85ae5b2ad57612ef5de4cf20b8600ee891abca20 Mon Sep 17 00:00:00 2001 From: Yvan CARRE Date: Tue, 17 Dec 2024 22:10:13 +0200 Subject: [PATCH 15/17] new changes --- webapp/public/locales/en/translation.json | 5 +- webapp/public/locales/fr/translation.json | 5 +- .../pages/ReponseDetails/ReponseDetails.js | 10 +-- webapp/src/pages/ResponsePage/ResponsePage.js | 89 +++++++++---------- 4 files changed, 53 insertions(+), 56 deletions(-) diff --git a/webapp/public/locales/en/translation.json b/webapp/public/locales/en/translation.json index 8a07e9a8..10e189b6 100644 --- a/webapp/public/locales/en/translation.json +++ b/webapp/public/locales/en/translation.json @@ -596,6 +596,9 @@ "REVISION": "REVISION", "ACCEPTED WITH REVISION": "ACCEPTED WITH REVISION", "REJECTED WITH ENCOURAGEMENT TO RESUMIT": "REJECTED WITH ENCOURAGEMENT TO RESUMIT", - "REVIEWING": "REVIEWING" + "REVIEWING": "REVIEWING", + "Unsubmitted": "Unsubmitted", + "Submitted": "Submitted", + "Withdrawn": "Withdrawn" } \ No newline at end of file diff --git a/webapp/public/locales/fr/translation.json b/webapp/public/locales/fr/translation.json index 3946b8d6..f03f5d4d 100644 --- a/webapp/public/locales/fr/translation.json +++ b/webapp/public/locales/fr/translation.json @@ -595,5 +595,8 @@ "REVISION": "REVISION", "ACCEPTED WITH REVISION": "ACCEPTE AVEC REVISION", "REJECTED WITH ENCOURAGEMENT TO RESUMIT": "REJECTÉ AVEC ENCOURAGEMENT DE RESOUMETTRE", - "REVIEWING": "EN REVUE" + "REVIEWING": "EN REVUE", + "Unsubmitted": "Non soumis", + "Submitted": "Soumis", + "Withdrawn": "Retiré" } diff --git a/webapp/src/pages/ReponseDetails/ReponseDetails.js b/webapp/src/pages/ReponseDetails/ReponseDetails.js index 5985eb97..dd80b9f9 100644 --- a/webapp/src/pages/ReponseDetails/ReponseDetails.js +++ b/webapp/src/pages/ReponseDetails/ReponseDetails.js @@ -107,30 +107,28 @@ class ReponseDetails extends Component { if (unsubmitted) { return ( - Unsubmitted{" "} - {this.formatDate(data.started_timestamp)} + {this.props.t('Unsubmitted')}{this.formatDate(data.started_timestamp)} ); } if (submitted) { return ( - Submitted{" "} - {this.formatDate(data.submitted_timestamp)} + {this.props.t('Submitted')}{this.formatDate(data.submitted_timestamp)} ); } if (withdrawn) { return ( - Withdrawn{" "} - {this.formatDate(data.started_timestamp)} + {this.props.t('Withdrawn')}{this.formatDate(data.started_timestamp)} ); } } } + outcomeStatus= (response) => { if (response.outcome) { const badgeClass = diff --git a/webapp/src/pages/ResponsePage/ResponsePage.js b/webapp/src/pages/ResponsePage/ResponsePage.js index 9878acd0..e498a31d 100644 --- a/webapp/src/pages/ResponsePage/ResponsePage.js +++ b/webapp/src/pages/ResponsePage/ResponsePage.js @@ -123,24 +123,21 @@ class ResponsePage extends Component { if (unsubmitted) { return ( - Unsubmitted{" "} - {this.formatDate(data.started_timestamp)} + {this.props.t('Unsubmitted')}{this.formatDate(data.started_timestamp)} ); } if (submitted) { return ( - Submitted{" "} - {this.formatDate(data.submitted_timestamp)} + {this.props.t('Submitted')}{this.formatDate(data.submitted_timestamp)} ); } if (withdrawn) { return ( - Withdrawn{" "} - {this.formatDate(data.started_timestamp)} + {this.props.t('Withdrawn')}{this.formatDate(data.started_timestamp)} ); } @@ -148,56 +145,52 @@ class ResponsePage extends Component { } renderReviewResponse(review_response, section) { - const questions = section.review_questions.map((q) => { - const a = review_response.scores.find( - (a) => a.review_question_id === q.id - ); + const questions = section.review_questions.map(q => { + const a = review_response.scores.find(a => a.review_question_id === q.id); - if (a) { - return ( -
-
-

- {q.headline} - {q.description && a && ( - -
- {q.description} -
- )} -

-
- -
-
-
- ); - } + if (a) { + return ( +
+

+ {q.headline} + {q.description &&
{q.description}
} +

+
+
+ ); + } + return null; }); + return questions; - } +} + // Render Reviews renderCompleteReviews() { if (this.state.reviewResponses.length) { - const reviews = this.state.reviewResponses.map((val) => { - return ( -
-

- {this.props.t( - val.reviewer_user_firstname + " " + val.reviewer_user_lastname - )} -

- {this.renderReviewResponse( - val, - this.state.reviewForm.review_sections[0] - )} -
- ); - }); - return reviews; + const reviews = this.state.reviewResponses.map(val => { + + return ( +
+

+ {this.props.t(val.reviewer_user_firstname + " " + val.reviewer_user_lastname)} +

+ {this.state.reviewForm.review_sections.map(section => ( +
+
{section.headline}
+ {this.renderReviewResponse(val, section)} +
+ ))} +
+ ); + }); + + return reviews; } - } + + return

No reviews available

; +} getOutcome() { outcomeService From e88966764b40db75bd7b16216972731806d81cdc Mon Sep 17 00:00:00 2001 From: Yvan CARRE Date: Tue, 17 Dec 2024 22:11:13 +0200 Subject: [PATCH 16/17] new changes --- webapp/public/locales/en/translation.json | 4 +--- webapp/public/locales/fr/translation.json | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/webapp/public/locales/en/translation.json b/webapp/public/locales/en/translation.json index 10e189b6..d9d8d2f8 100644 --- a/webapp/public/locales/en/translation.json +++ b/webapp/public/locales/en/translation.json @@ -597,8 +597,6 @@ "ACCEPTED WITH REVISION": "ACCEPTED WITH REVISION", "REJECTED WITH ENCOURAGEMENT TO RESUMIT": "REJECTED WITH ENCOURAGEMENT TO RESUMIT", "REVIEWING": "REVIEWING", - "Unsubmitted": "Unsubmitted", - "Submitted": "Submitted", - "Withdrawn": "Withdrawn" + "Unsubmitted": "Unsubmitted" } \ No newline at end of file diff --git a/webapp/public/locales/fr/translation.json b/webapp/public/locales/fr/translation.json index f03f5d4d..43942f20 100644 --- a/webapp/public/locales/fr/translation.json +++ b/webapp/public/locales/fr/translation.json @@ -596,7 +596,5 @@ "ACCEPTED WITH REVISION": "ACCEPTE AVEC REVISION", "REJECTED WITH ENCOURAGEMENT TO RESUMIT": "REJECTÉ AVEC ENCOURAGEMENT DE RESOUMETTRE", "REVIEWING": "EN REVUE", - "Unsubmitted": "Non soumis", - "Submitted": "Soumis", - "Withdrawn": "Retiré" + "Unsubmitted": "Non soumis" } From 4b8067215bbac09642a50fb3740994ada6cda9dd Mon Sep 17 00:00:00 2001 From: Yvan CARRE Date: Tue, 17 Dec 2024 22:48:00 +0200 Subject: [PATCH 17/17] new changes --- api/migrations/versions/41517b1f4730_.py | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 api/migrations/versions/41517b1f4730_.py diff --git a/api/migrations/versions/41517b1f4730_.py b/api/migrations/versions/41517b1f4730_.py deleted file mode 100644 index 316c3546..00000000 --- a/api/migrations/versions/41517b1f4730_.py +++ /dev/null @@ -1,22 +0,0 @@ -"""empty message - -Revision ID: 41517b1f4730 -Revises: e8eafcfe7800 -Create Date: 2024-12-07 17:40:47.044636 - -""" - -# revision identifiers, used by Alembic. -revision = '41517b1f4730' -down_revision = 'e8eafcfe7800' - -from alembic import op -import sqlalchemy as sa - - -def upgrade(): - pass - - -def downgrade(): - pass