diff --git a/grails-app/controllers/org/broadinstitute/orsp/SubmissionController.groovy b/grails-app/controllers/org/broadinstitute/orsp/SubmissionController.groovy index cad911bf9..b68d1ba55 100644 --- a/grails-app/controllers/org/broadinstitute/orsp/SubmissionController.groovy +++ b/grails-app/controllers/org/broadinstitute/orsp/SubmissionController.groovy @@ -1,25 +1,19 @@ package org.broadinstitute.orsp +import com.google.gson.Gson +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonParser import grails.converters.JSON import groovy.util.logging.Slf4j -import org.apache.tomcat.util.http.fileupload.disk.DiskFileItem +import org.apache.commons.fileupload.disk.DiskFileItem import org.broadinstitute.orsp.utils.IssueUtils @Slf4j class SubmissionController extends AuthenticatedController { - def show() { - Issue issue = queryService.findByKey(params.id) - if (issueIsForbidden(issue)) { - redirect(controller: 'Index', action: 'index') - } - Collection submissions = queryService.getSubmissionsByProject(issue.projectKey) - Map> groupedSubmissions = groupSubmissions(issue, submissions) - render(view: '/common/_submissionsPanel', - model: [issue : issue, - groupedSubmissions : groupedSubmissions, - attachmentsApproved: issue.attachmentsApproved() - ]) + def renderMainComponent() { + render(view: "/mainContainer/index", model: [params.projectKey, params.type, params.submissionId]) } def getSubmissions() { @@ -55,48 +49,73 @@ class SubmissionController extends AuthenticatedController { } } SubmissionType defaultType = SubmissionType.getForLabel(params.get("type").toString()) ?: SubmissionType.Amendment.label - render(view: '/submission/submission', - model: [issue: issue, - submission: submission, - docTypes: PROJECT_DOC_TYPES, - submissionTypes: submissionTypes, - submissionNumberMaximums: submissionNumberMaximums, - defaultType: defaultType - ]) + render( [issue: issue, + typeLabel: issue.typeLabel, + submission: submission, + documents: submission?.documents, + docTypes: PROJECT_DOC_TYPES, + submissionTypes: submissionTypes, + submissionNumberMaximums: submissionNumberMaximums, + defaultType: defaultType + ] as JSON) } def save() { - Issue issue = queryService.findByKey(params.projectKey) - Submission submission - if (params?.submissionId) { - submission = Submission.findById(params?.submissionId) - submission.number = Integer.parseInt(params?.submissionNumber) - submission.type = params?.submissionType - submission.comments = params?.comments - if (!submission.save(flush: true)) { - flash.message = submission.errors.allErrors.collect { it }.join("
") + JsonParser parser = new JsonParser() + JsonElement jsonElement = parser.parse(request.parameterMap["submission"].toString()) + JsonElement jsonFileTypes = parser.parse(request?.parameterMap["fileTypes"].toString()) + JsonArray dataSubmission + JsonArray fileData + + if (jsonElement.jsonArray) { + dataSubmission = jsonElement.asJsonArray + } + + if (jsonFileTypes.jsonArray) { + fileData = jsonFileTypes.asJsonArray + } + + List filesItems = params?.files?.part?.fileItem.collect().flatten() + User user = getUser() + + try { + Submission submission + if (params?.submissionId) { + submission = Submission.findById(params?.submissionId) + submission.comments = dataSubmission[0].comments.value + submission.type = dataSubmission[0].type.value + } else { + submission = getJson(Submission.class, dataSubmission[0]) + submission.createDate = new Date() + submission.author = getUser()?.displayName + submission.documents = new ArrayList() } - } else { - submission = new Submission( - projectKey: params.projectKey, - number: Integer.parseInt(params?.submissionNumber), - author: getUser()?.displayName, - type: params?.submissionType, - comments: params.comments, - createDate: new Date(), - documents: new ArrayList()) + if (!filesItems.isEmpty()) { + filesItems.forEach{ + StorageDocument submissionDoc = storageProviderService.saveFileItem( + (String) user.displayName, + (String) user.userName, + (String) submission.projectKey, + (String) fileData.find { data -> data.name.value == it.fileName }.fileType.value, + it) + submission.documents.add(submissionDoc) + fileData.remove(fileData.findIndexOf{ data -> data.name.value == it.fileName }) + } + } + if (!submission.save(flush: true)) { - flash.message = submission.errors.allErrors.collect { it }.join("
") + response.status = 500 + render([error: submission.errors.allErrors] as JSON) } + response.status = 201 + render([message: submission] as JSON) + + } catch (Exception e) { + log.error("There was an error trying to save the submission: " + e.message) + response.status = 500 + render([error: e.message] as JSON) } - def number = params?.submissionNumber ?: 0 // Helpful in the error case - render(view: '/submission/submission', - model: [issue : issue, - submission: submission, - minNumber : number, - docTypes : PROJECT_DOC_TYPES, - submissionTypes: getSubmissionTypesForIssueType(issue.getType())]) } def delete() { @@ -111,66 +130,36 @@ class SubmissionController extends AuthenticatedController { StorageDocument doc = StorageDocument.findById(it) storageProviderService.removeStorageDocument(doc, getUser()?.displayName) } - flash.message = message } - redirect(controller: 'project', action: 'main', params: [projectKey: params.projectKey, tab: "submissions"]) - } - - def addFile() { - Issue issue = queryService.findByKey(params.projectKey) - Submission submission = Submission.findById(params.submissionId) - if (submission) { - DiskFileItem file = params?.files?.part?.fileItem - if (file) { - StorageDocument document = storageProviderService.saveFileItem( - (String) getUser()?.displayName, - (String) getUser()?.userName, - (String) params?.projectKey, - (String) params?.type, - file - ) - if (document) { - submission?.documents?.add(document) - persistenceService.saveEvent(params.projectKey, getUser()?.displayName, "Added Document to Submission: " + document?.fileName, EventType.CHANGE) - } - submission.save(flush: true) - flash.message = "Successfully uploaded file" - } else { - flash.error = "Please select a file to upload" - } + if (submission.hasErrors()) { + response.status = 500 + render([error: submission.getErrors() ] as JSON) } else { - flash.error = "Unable to save file for unknown/unsaved submission" + response.status = 200 + render([message: 'Submission was deleted'] as JSON) } - render(view: '/submission/submission', - model: [issue : issue, - submission: submission, - minNumber : submission.number, - docTypes : PROJECT_DOC_TYPES, - submissionTypes: getSubmissionTypesForIssueType(issue.getType())]) } def removeFile() { - def issue = Issue.findByProjectKey(params?.projectKey) def submission = Submission.findById(params?.submissionId) def document = StorageDocument.findByUuid(params?.uuid) - def message = "Removed Document from Submission: " + document?.fileName submission?.getDocuments()?.remove(document) submission?.save(flush: true) if (document) { storageProviderService.removeStorageDocument(document, getUser()?.getDisplayName()) } if (StorageDocument.findByUuid(params?.uuid)?.id) { - flash.error = "Unable to delete file" - } else { - persistenceService.saveEvent(params.projectKey, getUser()?.displayName, message, EventType.CHANGE) - flash.message = message + response.status = 500 + render([error: "Unable to delete file"] as JSON) } - render(view: '/submission/submission', - model: [issue : issue, - submission: submission, - minNumber : submission.number, - docTypes : PROJECT_DOC_TYPES, - submissionTypes: getSubmissionTypesForIssueType(issue.getType())]) + + response.status = 200 + render (['message': 'document deleted'] as JSON) } + private static getJson(Class type, Object json) { + Gson gson = new Gson() + gson.fromJson(gson.toJson(json), type) + + } } diff --git a/grails-app/controllers/org/broadinstitute/orsp/UrlMappings.groovy b/grails-app/controllers/org/broadinstitute/orsp/UrlMappings.groovy index 4c1cc86d0..363e53557 100644 --- a/grails-app/controllers/org/broadinstitute/orsp/UrlMappings.groovy +++ b/grails-app/controllers/org/broadinstitute/orsp/UrlMappings.groovy @@ -96,10 +96,17 @@ class UrlMappings { '/api/comments/save'(controller: 'comments', action: 'saveNewComment', method: 'POST') '/api/comments/list'(controller: 'comments', action: 'getComments', method: 'GET') '/api/history'(controller: 'history', action: 'list', method: 'GET') + '/api/submissions'(controller: 'submission', action: 'show', method: 'GET') + '/api/submissions'(controller: 'submission', action: 'delete', method: 'DELETE') '/api/submissions/display'(controller: 'submission', action: 'getSubmissions', method: 'GET') - '/api/submissions/add-new'(controller: 'submission', action: 'index') + '/api/submissions/info'(controller: 'submission', action: 'index', method: 'GET') + '/submissions/add-new'(controller: 'submission', action: 'renderMainComponent', method: 'GET') + '/api/submissions/add-new-old'(controller: 'submission', action: 'index', method: 'GET') + '/api/submissions/save-new'(controller: 'submission', action: 'save', method: 'POST') '/api/submissions/add-new'(controller: 'submission', action: 'save', method: 'POST') + '/api/submission/remove-file'(controller: 'submission', action: 'removeFile', method: 'DELETE') + '/api/data-use/new-restriction'(controller: 'dataUse', action: 'create') '/api/data-use/restriction'(controller: 'dataUse', action: 'findRestriction') '/api/consent/export'(controller: 'dataUse', action: 'exportConsent', method: 'POST') diff --git a/grails-app/views/common/_submissionsPanel.gsp b/grails-app/views/common/_submissionsPanel.gsp deleted file mode 100644 index cb269cca9..000000000 --- a/grails-app/views/common/_submissionsPanel.gsp +++ /dev/null @@ -1,97 +0,0 @@ -%{-- -This template requires the following arguments: - -[ - issue: Issue - groupedSubmissions: Submissions, grouped by type -] - ---}% - -
- %{-- Using status for anchor links since types could be problematic --}% - - - - -
- %{-- Submission creation/editing is restricted to ORSP --}% - - - - - - - - - - - - - - - - - - - - - - - - -
NumberDescriptionDocumentsCreated
${submission.number} - - - Edit - - - View - - - ${raw(submission.comments)} - - - -  ${document.fileName} -
-
-
-
-
- -
- - - $(document).ready(function() { - $.fn.dataTable.moment( 'M/D/YYYY' ); - $(".submissionTable").DataTable({ - dom: '<"H"Tfr><"pull-right"B>
t
<"F"lp>', - buttons: [], - language: { search: 'Filter:' }, - pagingType: "full_numbers", - pageLength: 50, - columnDefs: [ { targets: [1, 2], orderable: false} ], - order: [0, "desc"] - }); - $("#submission-tabs").tabs(); - }); -
diff --git a/grails-app/views/submission/submission.gsp b/grails-app/views/submission/submission.gsp deleted file mode 100644 index 658dade9a..000000000 --- a/grails-app/views/submission/submission.gsp +++ /dev/null @@ -1,252 +0,0 @@ -%{-- -Page expects the following: - [issue: issue, - submission: submission, - docTypes: SUBMISSION_DOC_TYPES, - submissionTypes: getSubmissionTypes(issue), - submissionNumberMaximums: submissionNumberMaximums] - ---}% - - - - Submission - - - - -
- -

Submission for ${issue.typeLabel}: ${issue.projectKey} -

- -

Protocol Number: ${issue.protocol}

-
- -
-

Submission Details

- -
- - - - - - -
- - - - - - - - - - -
- -
- - -
- ${submission?.number} -
-
- -
- - -
- - -
-
-
-
- -
- - - - - -
- ${submission?.comments} -
-
-
- - -
- -
- -
-
-
- - -
-
-
- -
-
-
- - - -
-

Files

- -
- - - - - - -
- - -
- -
-
-
- Select fileChange - Remove -
- -
-
-
-
- - -
-
-

Files

- - - - - - - - - - - - - - - - - - - - - - - -
TypeFile NameAuthorCreated
${document.fileType} - ${document.fileName} - ${document.creator}${document.creationDate} - - - - - - - - - - Delete - -
-
-
-
- -
- -
- -
- -
- - - - - - - $(document).ready(function() { - let submissionNumberWidget = $('#submission-number'); - let submissionTypeWidget = $('#submission-type'); - let currentMax = 1; - let typeMaxMap = new Map(); - typeMaxMap.set("${entry.key}", ${entry.value}) - - - // Spinner update when submission type is changed - submissionTypeWidget.on('change', function () { - if (typeMaxMap.has($(this).val())) { - currentMax = typeMaxMap.get($(this).val()) + 1; - } else { - currentMax = 1; - } - submissionNumberWidget.val(currentMax); - }); - - // Spinner and spinner validation - $('.spinner .btn:first-of-type').on('click', function () { - currentMax ++; - submissionNumberWidget.val(currentMax); - return false; - }); - - // Decrement handler - $('.spinner .btn:last-of-type').on('click', function () { - // Don't go negative - if (currentMax > 0) { - currentMax --; - submissionNumberWidget.val(currentMax); - } - return false; - }); - - - // To ensure that the right submission number is displayed for new submissions, trigger the type selection widget: - submissionTypeWidget.trigger('change'); - - - }); - - diff --git a/src/main/webapp/adminOnly/AdminOnly.js b/src/main/webapp/adminOnly/AdminOnly.js index 7ec7f0b5a..711d387fa 100644 --- a/src/main/webapp/adminOnly/AdminOnly.js +++ b/src/main/webapp/adminOnly/AdminOnly.js @@ -56,12 +56,12 @@ export const AdminOnly = hh(class AdminOnly extends Component { } init = () => { - Project.getProject(component.projectKey).then( + Project.getProject(this.props.projectKey).then( issue => { let formData = {}; let initial = {}; this.props.initStatusBoxInfo(issue.data); - formData.projectKey = component.projectKey; + formData.projectKey = this.props.projectKey; formData.investigatorFirstName = issue.data.extraProperties.investigatorFirstName; formData.investigatorLastName = issue.data.extraProperties.investigatorLastName; formData.degrees = issue.data.extraProperties.degrees; @@ -144,7 +144,7 @@ export const AdminOnly = hh(class AdminOnly extends Component { submit = () => { spinnerService.show(ADMIN_ONLY_SPINNER); const parsedForm = this.getParsedForm(); - Project.updateAdminOnlyProps(parsedForm , component.projectKey).then( + Project.updateAdminOnlyProps(parsedForm , this.props.projectKey).then( response => { spinnerService.hide(ADMIN_ONLY_SPINNER); this.setState(prev => { diff --git a/src/main/webapp/components/Comments.js b/src/main/webapp/components/Comments.js index 3e9d38c4f..325e780d3 100644 --- a/src/main/webapp/components/Comments.js +++ b/src/main/webapp/components/Comments.js @@ -46,7 +46,7 @@ export const Comments = hh(class Comments extends Component { printComments = () => { let cols = columns.filter(el => el.dataField !== 'id'); let commentsArray = formatDataPrintableFormat(this.props.comments, cols); - const titleText = (component.issueType === "project" ? ("Project ID: "+ component.projectKey) + const titleText = (component.issueType === "project" ? ("Project ID: "+ this.props.projectKey) : ("Sample Data Cohort ID:"+ component.consentKey)); const columnsWidths = [100, '*', 200]; printData(commentsArray, titleText, 'ORSP Comments', columnsWidths); diff --git a/src/main/webapp/components/Documents.js b/src/main/webapp/components/Documents.js index 424235377..58b4d88b8 100644 --- a/src/main/webapp/components/Documents.js +++ b/src/main/webapp/components/Documents.js @@ -21,7 +21,7 @@ const headers = { name: 'Author', value: 'creator' }, { name: 'Version', value: 'docVersion' }, { name: 'Status', value: 'status' }, - { name: 'Created', value: 'createDate' }, + { name: 'Created', value: 'creationDate' }, { name: '', value: 'remove' } ]; diff --git a/src/main/webapp/components/History.js b/src/main/webapp/components/History.js index 77e69a37e..c6d3d454a 100644 --- a/src/main/webapp/components/History.js +++ b/src/main/webapp/components/History.js @@ -48,7 +48,7 @@ export const History = hh(class History extends Component { printHistory = () => { let cols = columns.filter(el => el.dataField !== 'id'); let historyArray = formatDataPrintableFormat(this.props.history, cols); - const titleText = (component.issueType === "project" ? ("Project ID: " + component.projectKey) + const titleText = (component.issueType === "project" ? ("Project ID: " + this.props.projectKey) : ("Sample Data Cohort ID:"+ component.consentKey)); const columnsWidths = [100, '*', 200]; printData(historyArray, titleText, 'ORSP History', columnsWidths); diff --git a/src/main/webapp/components/InputFieldNumber.js b/src/main/webapp/components/InputFieldNumber.js new file mode 100644 index 000000000..39d46a54b --- /dev/null +++ b/src/main/webapp/components/InputFieldNumber.js @@ -0,0 +1,28 @@ +import React from 'react'; +import { input, div, label, p, span } from 'react-hyperscript-helpers'; +import {isEmpty} from "../util/Utils"; + +const InputFieldNumber = (props) => { + const onChange = event => { + props.handleChange(event.target.value) + }; + + return ( +
+

+ {props.label} +

+ +
+ ) +}; + +export default InputFieldNumber; diff --git a/src/main/webapp/components/InputFieldText.js b/src/main/webapp/components/InputFieldText.js index 4c49a44ab..2315aea58 100644 --- a/src/main/webapp/components/InputFieldText.js +++ b/src/main/webapp/components/InputFieldText.js @@ -2,7 +2,6 @@ import { Component } from 'react'; import { input, hh, div } from 'react-hyperscript-helpers'; import { InputField } from './InputField'; import { areSomeTheseThingsTruthy } from '../util/Utils'; -import { isEmpty } from '../util/Utils'; import './InputField.css'; export const InputFieldText = hh(class InputFieldText extends Component { diff --git a/src/main/webapp/components/Table.js b/src/main/webapp/components/Table.js index b50befef1..3b5434e24 100644 --- a/src/main/webapp/components/Table.js +++ b/src/main/webapp/components/Table.js @@ -19,13 +19,20 @@ const styles = { infoLinkWidth: '96', creationDateWidth: '140', removeWidth: '45', + removeWidthFile: '80', unlinkSampleCollectionWidth: '80', collectionNameWidth: '270', numberWidth: '30', createDateWidth: '15', submissionDocumentsWidth: '100', submissionComments: '75', - createdWidth: '140' + createdWidth: '30', + linkOverflowEllipsis: { + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + color: '#337ab7' + } }; export const Table = hh(class Table extends Component { @@ -50,13 +57,6 @@ export const Table = hh(class Table extends Component { } }; - parseDate = (date) => { - if (date !== null) { - const simpleDate = new Date(date); - return format(simpleDate, 'M/D/YY h:m A') - } - }; - renderDropdownButton = (uuid) => { return ( @@ -108,7 +108,7 @@ export const Table = hh(class Table extends Component { Btn({ action: { labelClass: "glyphicon glyphicon-remove", - handler: this.props.remove(row) + handler: () => this.props.remove(row) }, }); @@ -147,16 +147,15 @@ export const Table = hh(class Table extends Component { cell.forEach(data => { if (data.document !== undefined) { documents.push([ - div({className: "linkOverflowEllipsis", key: data.document.id}, [ + div({style: styles.linkOverflowEllipsis, key: data.document.id}, [ a({ - href: `${component.downloadDocumentUrl}?uuid=${data.document.uuid}`, + href: `${UrlConstants.downloadDocumentUrl}?uuid=${data.document.uuid}`, target: '_blank', title: data.document.fileType, }, [ span({ - className: 'glyphicon glyphicon-download submission-download', - styles: "margin-right: 10px;" - }, []), + className: 'glyphicon glyphicon-download submission-download' + }, []), " ", data.document.fileName ]) ]) @@ -289,7 +288,7 @@ export const Table = hh(class Table extends Component { return {header.name} } else if (header.value === 'remove') { @@ -298,6 +297,12 @@ export const Table = hh(class Table extends Component { key={header.value} dataFormat={this.formatRemoveBtn} width={styles.removeWidth}>{header.name} + } else if (header.value === 'removeFile') { + return {header.name} } else if (header.value === 'unlinkSampleCollection') { return (
}/>
}/> }/> - }/> + }/> }/> }/> }/> }/> }/> }/> + } /> }/> ); diff --git a/src/main/webapp/projectContainer/ConsentGroups.js b/src/main/webapp/projectContainer/ConsentGroups.js index 78d1f56b8..f65c70800 100644 --- a/src/main/webapp/projectContainer/ConsentGroups.js +++ b/src/main/webapp/projectContainer/ConsentGroups.js @@ -92,7 +92,7 @@ export const ConsentGroups = hh(class ConsentGroups extends Component { } getProjectConsentGroups = () => { - ConsentGroup.getProjectConsentGroups(component.projectKey).then( result => { + ConsentGroup.getProjectConsentGroups(this.props.projectKey).then( result => { this.handleSuccessNotification(); this.setState(prev => { prev.consentGroups = result.data.consentGroups; @@ -242,7 +242,7 @@ export const ConsentGroups = hh(class ConsentGroups extends Component { }; redirect = (action) => { - const path = action === 'new' ? UrlConstants.newConsentGroupUrl + '?projectKey='+ component.projectKey : UrlConstants.useExistingConsentGroupUrl; + const path = action === 'new' ? UrlConstants.newConsentGroupUrl + '?projectKey='+ this.props.projectKey : UrlConstants.useExistingConsentGroupUrl; this.props.history.push(path) }; @@ -293,7 +293,7 @@ export const ConsentGroups = hh(class ConsentGroups extends Component { RequestClarificationDialog({ closeModal: this.closeRequestClarification, show: this.state.showRequestClarification, - issueKey: component.projectKey, + issueKey: this.props.projectKey, consentKey: this.state.actionConsentKey, user: component.user, emailUrl: component.emailUrl, diff --git a/src/main/webapp/projectContainer/ProjectContainer.js b/src/main/webapp/projectContainer/ProjectContainer.js index f64b00788..2c3639a4d 100644 --- a/src/main/webapp/projectContainer/ProjectContainer.js +++ b/src/main/webapp/projectContainer/ProjectContainer.js @@ -10,6 +10,7 @@ import { ProjectDocument } from "../projectDocument/ProjectDocument"; import { AdminOnly } from "../adminOnly/AdminOnly"; import { MultiTab } from "../components/MultiTab"; import { ProjectMigration, Review } from '../util/ajax'; +import {isEmpty} from "../util/Utils"; export const ProjectContainer = hh(class ProjectContainer extends Component { @@ -54,7 +55,7 @@ export const ProjectContainer = hh(class ProjectContainer extends Component { // history getHistory() { - ProjectMigration.getHistory(component.projectKey).then(resp => { + ProjectMigration.getHistory(this.props.projectKey).then(resp => { this.setState(prev => { prev.history = resp.data; return prev; @@ -64,7 +65,7 @@ export const ProjectContainer = hh(class ProjectContainer extends Component { //comments getComments() { - Review.getComments(component.projectKey).then(result => { + Review.getComments(this.props.projectKey).then(result => { this.setState(prev => { prev.comments = result.data; return prev; @@ -72,11 +73,22 @@ export const ProjectContainer = hh(class ProjectContainer extends Component { }); } + activeTab = () => { + let tab = this.state.defaultActive; + + if (!isEmpty(this.props.tab) && !isEmpty(component.tab)){ + tab = component.tab; + } else if (!isEmpty(this.props.tab)) { + tab = this.props.tab; + } + return tab; + }; + render() { return ( div({ className: "headerBoxContainer" }, [ div({ className: "containerBox" }, [ - MultiTab({ defaultActive: component.tab === "" ? this.state.defaultActive : component.tab }, + MultiTab({ defaultActive: this.activeTab() }, [ div({ key: "review", @@ -86,7 +98,8 @@ export const ProjectContainer = hh(class ProjectContainer extends Component { updateDetailsStatus: this.updateDetailsStatus, changeInfoStatus: this.props.changeInfoStatus, initStatusBoxInfo: this.props.initStatusBoxInfo, - updateContent: this.updateContent + updateContent: this.updateContent, + projectKey: this.props.projectKey, }) ]), div({ @@ -96,7 +109,8 @@ export const ProjectContainer = hh(class ProjectContainer extends Component { h(ProjectDocument, { statusBoxHandler: this.props.statusBoxHandler, updateDocumentsStatus: this.updateDocumentsStatus, - initStatusBoxInfo: this.props.initStatusBoxInfo + initStatusBoxInfo: this.props.initStatusBoxInfo, + projectKey: this.props.projectKey, }) ]), div({ @@ -105,14 +119,18 @@ export const ProjectContainer = hh(class ProjectContainer extends Component { }, [ h(Fragment, {}, [ConsentGroups({ history: this.props.history, - updateContent: this.updateContent + updateContent: this.updateContent, + projectKey: this.props.projectKey, })]), ]), div({ key: "submissions", title: "Submissions", }, [ - h(Fragment, {}, [Submissions({})]), + h(Fragment, {}, [Submissions({ + history: this.props.history, + projectKey: this.props.projectKey, + })]), ]), div({ key: "comments", @@ -120,8 +138,9 @@ export const ProjectContainer = hh(class ProjectContainer extends Component { }, [ h(Fragment, {}, [Comments({ comments: this.state.comments, - id: component.projectKey, - updateContent: this.updateContent + id: this.props.projectKey, + updateContent: this.updateContent, + projectKey: this.props.projectKey, })]), ]), div({ @@ -129,7 +148,8 @@ export const ProjectContainer = hh(class ProjectContainer extends Component { title: "History", }, [ h(Fragment, {}, [History({ - history: this.state.history + history: this.state.history, + projectKey: this.props.projectKey, } )]), ]), @@ -140,7 +160,8 @@ export const ProjectContainer = hh(class ProjectContainer extends Component { h(AdminOnly, { statusBoxHandler: this.props.statusBoxHandler, updateAdminOnlyStatus: this.updateAdminOnlyStatus, - initStatusBoxInfo: this.props.initStatusBoxInfo + initStatusBoxInfo: this.props.initStatusBoxInfo, + projectKey: this.props.projectKey, }) ]) ]) diff --git a/src/main/webapp/projectContainer/SubmissionForm.js b/src/main/webapp/projectContainer/SubmissionForm.js new file mode 100644 index 000000000..4b9d4b321 --- /dev/null +++ b/src/main/webapp/projectContainer/SubmissionForm.js @@ -0,0 +1,393 @@ +import { Component } from 'react'; +import { div, h1, button, h, a } from 'react-hyperscript-helpers'; +import { Panel } from "../components/Panel"; +import {Files, ProjectMigration} from "../util/ajax"; +import { InputFieldSelect } from "../components/InputFieldSelect"; +import InputFieldNumber from "../components/InputFieldNumber"; +import { InputFieldTextArea } from "../components/InputFieldTextArea"; +import { Table } from "../components/Table"; +import { AddDocumentDialog } from "../components/AddDocumentDialog"; +import { isEmpty, scrollToTop } from "../util/Utils"; +import { spinnerService } from "../util/spinner-service"; +import { ConfirmationDialog } from "../components/ConfirmationDialog"; +import { Spinner } from "../components/Spinner"; +import { AlertMessage } from "../components/AlertMessage"; + +const styles = { + addDocumentContainer: { + display: 'block', height: '40px', margin: '15px 0 10px 0' + }, + addDocumentBtn: { + position: 'relative', float: 'right' + }, + deleteSubmission: { + position: 'absolute', + right: '30px', + top: '46px' + }, +}; +const headers = + [ + { name: 'Document Type', value: 'fileType' }, + { name: 'File Name', value: 'fileName' }, + { name: 'Remove', value: 'removeFile' } + ]; + +class SubmissionForm extends Component { + constructor(props) { + super(props); + this.state = { + submissionInfo: { + typeLabel: '', + docTypes: [], + submissionTypes: [], + submissionNumberMaximums: {}, + selectedType: '', + number: 1, + comments: '', + }, + showAddDocuments: false, + documents: [], + params: { + projectKey: '', + type: '' + }, + showDialog: false, + fileToRemove: {}, + action: '', + errors: { + comment: false, + serverError: false + }, + }; + } + + componentDidMount() { + scrollToTop(); + const params = new URLSearchParams(this.props.location.search); + this.getSubmissionFormInfo(params.get('projectKey'), params.get('type'), params.get('submissionId')); + this.setState(prev => { + prev.params.projectKey = params.get('projectKey'); + prev.params.type = params.get('type'); + prev.params.submissionId = params.get('submissionId'); + return prev; + }); + } + + componentWillUnmount() { + spinnerService.hideAll(); + spinnerService._unregisterAll(); + } + + getTypeSelected() { + const params = new URLSearchParams(this.props.location.search); + return { label: params.get('type'), value: params.get('type') }; + } + + formatSubmissionType(submissionTypes) { + return submissionTypes.map(type => { + return { + label: type, + value: type + } + }); + }; + + getSubmissionFormInfo = (projectKey, type, submissionId = '') => { + ProjectMigration.getSubmissionFormInfo(projectKey, type, submissionId).then(resp => { + const submissionInfo = resp.data; + this.setState(prev => { + prev.submissionInfo.typeLabel = submissionInfo.typeLabel; + prev.submissionInfo.projectKey = submissionInfo.issue.projectKey; + prev.submissionInfo.selectedType = this.getTypeSelected(); + prev.submissionInfo.submissionTypes = this.formatSubmissionType(submissionInfo.submissionTypes); + prev.submissionInfo.comments = submissionInfo.submission !== null ? submissionInfo.submission.comments : ''; + prev.submissionInfo.submissionNumber = this.maximumNumber(submissionInfo.submissionNumberMaximums, prev.params.type, prev.params.submissionId); + prev.submissionInfo.submissionNumberMaximums = submissionInfo.submissionNumberMaximums; + prev.submissionInfo.number = this.maximumNumber(submissionInfo.submissionNumberMaximums, prev.params.type, prev.params.submissionId); + prev.docTypes = this.loadOptions(submissionInfo.docTypes); + prev.documents = isEmpty(submissionInfo.documents) ? [] : submissionInfo.documents; + return prev; + }); + }); + }; + + maximumNumber(submissionMax, type, submissionId) { + if (isEmpty(submissionMax[type])) { + return 1; + } else if (isEmpty(submissionId) && !isEmpty(submissionMax[type])) { + return submissionMax[type] + 1; + } else { + return submissionMax[type]; + } + } + + loadOptions(docTypes) { + return docTypes.map(type => { return { value: type, label: type } }); + }; + + handleInputChange = (e) => { + const field = e.target.name; + const value = e.target.value; + this.setState(prev => { + prev.submissionInfo[field] = value; + prev.errors.comment = false; + return prev; + }); + }; + + handleUpdate = (value) => { + this.setState(prev => { + prev.submissionInfo.number = value; + return prev; + }); + }; + + handleSelectChange = (field) => () => (value) => { + this.setState(prev => { + if (field === "selectedType") { + const maxNumber = this.maximumNumber(prev.submissionInfo.submissionNumberMaximums, value.value, this.state.params.submissionId); + prev.submissionInfo.number = maxNumber; + prev.submissionInfo.submissionNumber = maxNumber; + } + prev.submissionInfo[field] = value; + return prev; + }); + }; + + submitSubmission = () => { + if(this.validateSubmission()) { + spinnerService.showAll(); + const submissionData = { + type: this.state.submissionInfo.selectedType.value, + number: this.state.submissionInfo.number, + comments: this.state.submissionInfo.comments, + projectKey: this.state.submissionInfo.projectKey + }; + + ProjectMigration.saveSubmission(submissionData, this.state.documents, this.state.params.submissionId).then(resp => { + this.backToProject(); + }).catch(error => { + spinnerService.hideAll(); + console.error(error); + this.setState(prev => { + prev.errors.serverError = true; + return prev; + }); + }); + } + }; + + handleAction = () => { + spinnerService.showAll(); + this.closeModal('showDialog'); + if (this.state.action === 'document') { + this.removeFile(); + } else { + this.deleteSubmission(); + } + }; + + validateSubmission = () => { + if (isEmpty(this.state.submissionInfo.comments)) { + this.setState(prev => { + prev.errors.comment = true; + return prev; + }); + return false; + } + return true; + }; + + removeFileDialog = (data) => { + this.setState(prev => { + prev.showDialog = true; + prev.fileToRemove = data; + prev.action = 'document'; + return prev; + }); + }; + + removeSubmissionDialog = () => { + this.setState(prev => { + prev.showDialog = true; + prev.action = 'submission'; + return prev; + }); + }; + + removeFile = () => { + ProjectMigration.removeSubmissionFile(this.state.params.submissionId, this.state.fileToRemove.uuid).then(prev => { + const documentsToUpdate = this.state.documents.filter(doc => doc.id !== this.state.fileToRemove.id); + this.setState(prev => { + prev.documents = documentsToUpdate; + return prev; + }); + spinnerService.hideAll(); + }); + }; + + setFilesToUpload = (doc) => { + this.setState(prev => { + let document = { fileType: doc.fileKey, file: doc.file, fileName: doc.file.name, id: Math.random() }; + let documents = prev.documents; + documents.push(document); + prev.documents = documents; + return prev; + }, () => { + this.closeModal("showAddDocuments"); + }); + }; + + addDocuments = () => { + this.setState({ + showAddDocuments: !this.state.showAddDocuments + }); + }; + + closeModal = (type) => { + this.setState(prev => { + prev[type] = !this.state[type]; + return prev; + }); + }; + + deleteSubmission = () => { + spinnerService.showAll(); + ProjectMigration.deleteSubmission(this.state.params.submissionId).then(prev => { + this.props.history.goBack(); + spinnerService.hideAll(); + }).catch(error => { + spinnerService.hideAll(); + console.error(error); + this.setState(prev => { + prev.errors.serverError = true; + return prev; + }); + }); + }; + + backToProject = () => { + this.props.history.push('/project/main?projectKey=' + this.state.params.projectKey + '&tab=submissions', {tab: 'submissions'}); + }; + + render() { + if (this.state.hasError) { + // You can render any custom fallback UI + return h1({}, ["Something went wrong."]); + } + + const minimunNumber = this.state.submissionInfo.submissionNumber; + const edit = !isEmpty(this.state.params.submissionId); + + return ( + div({}, [ + ConfirmationDialog({ + closeModal: () => this.closeModal("showDialog"), + show: this.state.showDialog, + handleOkAction: this.handleAction, + title: 'Delete Confirmation', + bodyText: `Are you sure you want to delete this ${this.state.action}?`, + actionLabel: 'Yes' + }), + AddDocumentDialog({ + closeModal: () => this.closeModal("showAddDocuments"), + show: this.state.showAddDocuments, + options: this.state.docTypes, + projectKey: this.props.projectKey, + user: this.props.user, + handleLoadDocuments: this.props.handleLoadDocuments, + emailUrl: this.props.emailUrl, + userName: this.props.userName, + documentHandler: this.setFilesToUpload + }), + h1({ + style: {'marginBottom':'20px'} + }, [ + "Submission for ", h(a, { + onClick: () => this.backToProject(), + style: {'cursor': 'pointer'} + }, [`${this.state.submissionInfo.typeLabel}: ${this.state.submissionInfo.projectKey}`]), + ]), + Panel({ + title: "Add new submission", + }, [ + InputFieldSelect({ + label: "Submission Type", + id: "submissionType", + name: "selectedType", + options: this.state.submissionInfo.submissionTypes, + value: this.state.submissionInfo.selectedType, + onChange: this.handleSelectChange("selectedType"), + placeholder: this.state.submissionInfo.selectedType, + readOnly: !component.isAdmin, + edit: false + }), + InputFieldNumber({ + name: "submissionNumber", + handleChange: this.handleUpdate, + value: this.state.submissionInfo.number, + label: "Submission Number", + min: minimunNumber, + showLabel: true, + readOnly: !component.isAdmin, + edit: false + }), + InputFieldTextArea({ + id: "submission-comment", + name: "comments", + label: "Description", + value: this.state.submissionInfo.comments, + required: false, + onChange: this.handleInputChange, + edit: component.isAdmin, + error: this.state.errors.comment, + errorMessage: "Required field", + readOnly: !component.isAdmin, + }), + ]), + Panel({ + title: "Files" + },[ + div({ style: styles.addDocumentContainer }, [ + button({ + isRendered: component.isAdmin && !component.isViewer, + className: "btn buttonSecondary", + style: styles.addDocumentBtn, + onClick: this.addDocuments + }, ["Add Document"]) + ]), + Table({ + headers: headers, + data: this.state.documents, + sizePerPage: 10, + paginationSize: 10, + remove: this.removeFileDialog, + reviewFlow: false, + pagination: false, + + }), + button({ + isRendered: component.isAdmin, + className: "btn buttonPrimary pull-right", style: {'marginTop':'30px', 'marginLeft':'12px'}, + onClick: this.submitSubmission, + }, [edit ? "Save" : "Submit"]), + button({ + isRendered: component.isAdmin && edit, + className: "btn buttonPrimary floatRight", style: {'marginTop':'30px'}, + onClick: this.removeSubmissionDialog + }, ["Delete"]) + ]), + AlertMessage({ + msg: 'Something went wrong in the server. Please try again later.', + show: this.state.errors.serverError + }), + h(Spinner, { + name: "mainSpinner", group: "orsp", loadingImage: component.loadingImage + }) + ]) + ); + } +} + +export default SubmissionForm; diff --git a/src/main/webapp/projectContainer/Submissions.js b/src/main/webapp/projectContainer/Submissions.js index d15561e55..657353f7a 100644 --- a/src/main/webapp/projectContainer/Submissions.js +++ b/src/main/webapp/projectContainer/Submissions.js @@ -6,6 +6,7 @@ import { MultiTab } from "../components/MultiTab"; import { Table } from "../components/Table"; import { Files } from "../util/ajax"; import _ from 'lodash'; +import { UrlConstants } from "../util/UrlConstants"; const headers = [ @@ -56,14 +57,10 @@ export const Submissions = hh(class Submissions extends Component { this.getDisplaySubmissions(); } - getDocumentLink = (data) => { - return [component.serverURL, 'api/files-helper/get-document?id=' + data].join("/"); - }; - submissionEdit = (data) => { const indexButton = a({ className: 'btn btn-default btn-xs pull-left link-btn', - href: `${component.contextPath}/submission/index?projectKey=${component.projectKey}&submissionId=${data.id}` + onClick: () => this.redirectEditSubmission(data) }, [component.isAdmin === true ? 'Edit': 'View']); const submissionComment = span({style: styles.submissionComment}, [data.comments]); return h(Fragment, {}, [indexButton, submissionComment]); @@ -71,7 +68,7 @@ export const Submissions = hh(class Submissions extends Component { getDisplaySubmissions = () => { let submissions = {}; - ProjectMigration.getDisplaySubmissions(component.projectKey).then(resp => { + ProjectMigration.getDisplaySubmissions(this.props.projectKey).then(resp => { submissions = resp.data.groupedSubmissions; _.map(submissions, (data, title) => { @@ -93,14 +90,19 @@ export const Submissions = hh(class Submissions extends Component { }); }; - redirectNewSubmission(e) { - window.location.href = `${component.serverURL}/api/submissions/add-new?projectKey=${component.projectKey}&type=${e.target.id}`; - } + redirectEditSubmission = (data) => { + this.props.history.push(`${UrlConstants.submissionsAddNewUrl}?projectKey=${this.props.projectKey}&type=${data.type}&submissionId=${data.id}`); + }; + + redirectNewSubmission = (e) => { + this.props.history.push(`/submissions/add-new?projectKey=${this.props.projectKey}&type=${e.target.id}`); + }; submissionTab = (data, title) => { return div({ key: title, title: this.tabTitle(title, data.length) },[ a({ + isRendered: !component.isViewer, onClick: this.redirectNewSubmission, className: "btn btn-primary", style: styles.addSubmission, @@ -112,7 +114,6 @@ export const Submissions = hh(class Submissions extends Component { sizePerPage: 10, paginationSize: 10, isAdmin: component.isAdmin, - getDocumentLink: this.getDocumentLink, pagination: true, reviewFlow: true, submissionEdit: this.submissionEdit, diff --git a/src/main/webapp/projectDocument/ProjectDocument.js b/src/main/webapp/projectDocument/ProjectDocument.js index a7b003864..bcae45e46 100644 --- a/src/main/webapp/projectDocument/ProjectDocument.js +++ b/src/main/webapp/projectDocument/ProjectDocument.js @@ -31,11 +31,11 @@ export const ProjectDocument = hh(class ProjectDocument extends Component { } getAttachedDocuments = () => { - Project.getProject(component.projectKey).then( + Project.getProject(this.props.projectKey).then( issue => { this.props.initStatusBoxInfo(issue.data); }); - DocumentHandler.attachedDocuments(component.projectKey).then(resp => { + DocumentHandler.attachedDocuments(this.props.projectKey).then(resp => { User.getUserSession().then(user => { this.setState(prev => { prev.documents = JSON.parse(resp.data.documents); @@ -118,7 +118,7 @@ export const ProjectDocument = hh(class ProjectDocument extends Component { handleDialogConfirm: this.handleDialog, user: this.state.user, options: this.state.documentOptions, - projectKey: component.projectKey, + projectKey: this.props.projectKey, handleLoadDocuments: this.getAttachedDocuments, docsClarification: "Please upload any documents related to your overall project, for example: IRB application form, protocol, Continuing Review form, etc. Documents related to a specific cohort, such as consent forms or attestations, should be uploaded in the Sample/Data Cohort tab." }), diff --git a/src/main/webapp/projectReview/ProjectReview.js b/src/main/webapp/projectReview/ProjectReview.js index 41c02812b..db555ad56 100644 --- a/src/main/webapp/projectReview/ProjectReview.js +++ b/src/main/webapp/projectReview/ProjectReview.js @@ -163,7 +163,7 @@ export const ProjectReview = hh(class ProjectReview extends Component { let future = {}; let futureCopy = {}; let formData = {}; - Project.getProject(component.projectKey).then( + Project.getProject(this.props.projectKey).then( issue => { // store current issue info here .... this.props.initStatusBoxInfo(issue.data); @@ -183,7 +183,7 @@ export const ProjectReview = hh(class ProjectReview extends Component { futureCopy = JSON.parse(currentStr); this.projectType = issue.data.issue.type; - Review.getSuggestions(component.projectKey).then( + Review.getSuggestions(this.props.projectKey).then( data => { const urlParams = new URLSearchParams(window.location.search); if (urlParams.has('new') && urlParams.get('tab') === 'review') { @@ -227,7 +227,7 @@ export const ProjectReview = hh(class ProjectReview extends Component { getReviewSuggestions() { this.init(); - Review.getSuggestions(component.projectKey).then( + Review.getSuggestions(this.props.projectKey).then( data => { if (data.data !== '') { this.setState(prev => { @@ -297,7 +297,7 @@ export const ProjectReview = hh(class ProjectReview extends Component { approveInfoDialog: false }); const data = { projectReviewApproved: true }; - Project.addExtraProperties(component.projectKey, data).then( + Project.addExtraProperties(this.props.projectKey, data).then( () => { this.toggleState('approveInfoDialog'); this.setState(prev => { @@ -305,7 +305,7 @@ export const ProjectReview = hh(class ProjectReview extends Component { return prev; }, () => { - Project.getProject(component.projectKey).then( + Project.getProject(this.props.projectKey).then( issue => { this.props.updateDetailsStatus(issue.data); }) @@ -316,7 +316,7 @@ export const ProjectReview = hh(class ProjectReview extends Component { }); if (this.state.reviewSuggestion) { let project = this.getProject(); - Project.updateProject(project, component.projectKey).then( + Project.updateProject(project, this.props.projectKey).then( resp => { this.removeEdits('approve'); }) @@ -328,7 +328,7 @@ export const ProjectReview = hh(class ProjectReview extends Component { rejectProject() { spinnerService.show(PROJECT_REVIEW_SPINNER); - Project.rejectProject(component.projectKey).then(resp => { + Project.rejectProject(this.props.projectKey).then(resp => { this.setState(prev => { prev.rejectProjectDialog = !this.state.rejectProjectDialog; return prev; @@ -351,7 +351,7 @@ export const ProjectReview = hh(class ProjectReview extends Component { spinnerService.show(PROJECT_REVIEW_SPINNER); let project = this.getProject(); project.editsApproved = true; - Project.updateProject(project, component.projectKey).then( + Project.updateProject(project, this.props.projectKey).then( resp => { this.removeEdits('approve'); this.setState((state, props) => { @@ -364,7 +364,7 @@ export const ProjectReview = hh(class ProjectReview extends Component { }; removeEdits(type) { - Review.deleteSuggestions(component.projectKey, type).then( + Review.deleteSuggestions(this.props.projectKey, type).then( resp => { this.props.updateContent(); this.init(); @@ -505,12 +505,12 @@ export const ProjectReview = hh(class ProjectReview extends Component { resp => { suggestions.editCreator = resp.data.userName; const data = { - projectKey: component.projectKey, + projectKey: this.props.projectKey, suggestions: JSON.stringify(suggestions) }; if (this.state.reviewSuggestion) { - Review.updateReview(component.projectKey, data).then(() => + Review.updateReview(this.props.projectKey, data).then(() => this.getReviewSuggestions() ).catch(error => { this.getReviewSuggestions(); @@ -762,7 +762,7 @@ export const ProjectReview = hh(class ProjectReview extends Component { }; redirectToConsentGroupTab = async () => { - window.location.href = [component.serverURL, "project", "main?projectKey=" + component.projectKey + "&tab=consent-groups"].join("/"); + window.location.href = [component.serverURL, "project", "main?projectKey=" + this.props.projectKey + "&tab=consent-groups"].join("/"); }; handleAttestationCheck = (e) => { @@ -820,7 +820,7 @@ export const ProjectReview = hh(class ProjectReview extends Component { RequestClarificationDialog({ closeModal: this.toggleState('requestClarification'), show: this.state.requestClarification, - issueKey: component.projectKey, + issueKey: this.props.projectKey, successClarification: this.successNotification, }), diff --git a/src/main/webapp/util/UrlConstants.js b/src/main/webapp/util/UrlConstants.js index f02665400..b0af46ff9 100644 --- a/src/main/webapp/util/UrlConstants.js +++ b/src/main/webapp/util/UrlConstants.js @@ -67,12 +67,17 @@ export const UrlConstants = { infoLinkUrl: context + '/api/info-link', restrictionUrl: context + '/api/dur', + submissionsUrl: context + '/api/submissions', + submissionsAddNewUrl: '/submissions/add-new', + submissionInfoAddUrl: context + '/api/submissions/info', + submissionSaveUrl: context + '/api/submissions/save-new', + submissionDisplayUrl: context + '/api/submissions/display', + submissionRemoveFileUrl: context + '/api/submission/remove-file', + sampleConsentLinkUrl: context + '/api/sample-consent-link', sampleBreakLinkUrl: context + '/api/break-link', sampleApproveLinkUrl: context + '/api/approve-link', historyUrl: context + '/api/history', - submissionsUrl: context + '/api/submissions', - submissionDisplayUrl: context + '/api/submissions/display', newRestrictionUrl: context + '/api/data-use/new-restriction', reviewCategoriesUrl: context + '/api/report/review-categories', fundingReportsUrl: context + '/api/report/get-funding', diff --git a/src/main/webapp/util/ajax.js b/src/main/webapp/util/ajax.js index 613950aae..a829c3f1c 100644 --- a/src/main/webapp/util/ajax.js +++ b/src/main/webapp/util/ajax.js @@ -386,11 +386,48 @@ export const ProjectMigration = { return axios.get(UrlConstants.historyUrl + '?id=' + id); }, - getSubmissions(id) { - return axios.get(UrlConstants.submissionsUrl + "?id=" + id); - }, - getDisplaySubmissions(id) { return axios.get(UrlConstants.submissionDisplayUrl + '?id=' + id); + }, + + getSubmissionFormInfo(projectKey, type, submissionId) { + if (submissionId === null) { + return axios.get(UrlConstants.submissionInfoAddUrl + "?projectKey=" + projectKey + "&?type=" + type); + } else { + return axios.get(UrlConstants.submissionInfoAddUrl + "?projectKey=" + projectKey + "&type=" + type + "&submissionId=" + submissionId); + } + }, + + saveSubmission(submissionData, files, submissionId) { + let data = new FormData(); + + files.forEach(file => { + if (file.file != null) { + const fileData = { + fileType: file.fileType, + name: file.fileName + }; + data.append('files', file.file, file.fileName); + data.append('fileTypes', JSON.stringify(fileData)); + } + }); + data.append('submission', JSON.stringify(submissionData)); + + const config = { + headers: { 'content-type': 'multipart/form-data' } + }; + if (submissionId === null) { + return axios.post(UrlConstants.submissionSaveUrl, data, config); + } else { + return axios.post(UrlConstants.submissionSaveUrl + '?submissionId=' + submissionId, data, config); + } + }, + + removeSubmissionFile(sumissionId, uuid) { + return axios.delete(UrlConstants.submissionRemoveFileUrl + '?submissionId=' + sumissionId + "&uuid=" + uuid); + }, + + deleteSubmission(submissionId) { + return axios.delete(UrlConstants.submissionsUrl + '?submissionId=' + submissionId); } };