diff --git a/cypress/component/DataAccessRequest/selectable_datasets.spec.js b/cypress/component/DataAccessRequest/selectable_datasets.spec.js new file mode 100644 index 000000000..83d7ae34b --- /dev/null +++ b/cypress/component/DataAccessRequest/selectable_datasets.spec.js @@ -0,0 +1,126 @@ +/* eslint-disable no-undef */ +import {React} from 'react'; +import {mount} from 'cypress/react'; +import DataAccessRequestApplication from '../../../src/pages/dar_application/DataAccessRequestApplication'; +import SelectableDatasets from '../../../src/pages/dar_application/SelectableDatasets.jsx'; + +const datasets = [ + { + dataSetId: 123456, + datasetIdentifier: `DUOS-123456`, + datasetName: 'Some Dataset 1' + }, + { + dataSetId: 234567, + datasetIdentifier: `DUOS-234567`, + datasetName: 'Some Dataset 2' + }, + { + dataSetId: 345678, + datasetIdentifier: `DUOS-345678`, + datasetName: 'Some Dataset 3' + }, + { + dataSetId: 456789, + datasetIdentifier: `DUOS-456789`, + datasetName: 'Some Dataset 4' + }, +]; + +const props = { + datasets: datasets, + setSelectedDatasets: () => {}, + disabled: false +}; + +const propsDisabled = { + datasets: datasets, + setSelectedDatasets: () => {}, + disabled: true +}; + + +describe('Selectable Datasets - Not Read Only', () => { + + describe('With 4 Datasets', () => { + beforeEach(() => { + mount(); + }); + + it('Marks 2 datasets for removal', () => { + cy.get('#DUOS-123456_summary').click(); + cy.get('#DUOS-345678_summary').click(); + cy.get('#restore_dataset_123456').should('exist'); + cy.get('#restore_dataset_345678').should('exist'); + }); + + it('Unmark 1 of the previously marked for removal datasets', () => { + cy.get('#DUOS-123456_summary').click(); + cy.get('#DUOS-345678_summary').click(); + cy.get('#restore_dataset_123456').should('exist'); + cy.get('#restore_dataset_345678').should('exist'); + cy.get('#restore_dataset_345678').click(); + cy.get('#remove_dataset_345678').should('exist'); + }); + + it('Marks 2 more datasets for removal, leaving 1 dataset left not removed', () => { + cy.get('#DUOS-123456_summary').click(); + cy.get('#DUOS-345678_summary').click(); + cy.get('#restore_dataset_123456').should('exist'); + cy.get('#restore_dataset_345678').should('exist'); + cy.get('#restore_dataset_345678').click(); + cy.get('#remove_dataset_345678').should('exist'); + cy.get('#remove_dataset_345678').click(); + cy.get('#DUOS-234567_summary').click(); + cy.get('#restore_dataset_123456').should('exist'); + cy.get('#restore_dataset_345678').should('exist'); + cy.get('#restore_dataset_234567').should('exist'); + cy.get('#remove_dataset_456789').should('exist'); + }); + + it('Cannot delete last dataset', () => { + cy.get('#DUOS-123456_summary').click(); + cy.get('#DUOS-345678_summary').click(); + cy.get('#restore_dataset_123456').should('exist'); + cy.get('#restore_dataset_345678').should('exist'); + cy.get('#restore_dataset_345678').click(); + cy.get('#remove_dataset_345678').should('exist'); + cy.get('#remove_dataset_345678').click(); + cy.get('#DUOS-234567_summary').click(); + cy.get('#restore_dataset_123456').should('exist'); + cy.get('#restore_dataset_345678').should('exist'); + cy.get('#restore_dataset_234567').should('exist'); + cy.get('#remove_dataset_456789').should('exist'); + cy.get('#DUOS-456789_summary [data-testid="DeleteIcon"]').should('have.css', 'opacity', '0.5'); + }); + }); + + describe('Selectable Datasets - Read Only', () => { + beforeEach(() => { + mount(); + }); + + it('Can not click on any dataset', () => { + cy.get('#DUOS-123456_summary').should('have.css', 'cursor', 'auto'); + cy.get('#DUOS-234567_summary').should('have.css', 'cursor', 'auto'); + cy.get('#DUOS-345678_summary').should('have.css', 'cursor', 'auto'); + cy.get('#DUOS-456789_summary').should('have.css', 'cursor', 'auto'); + + cy.get('#DUOS-123456_summary').click(); + cy.get('#DUOS-234567_summary').click(); + cy.get('#DUOS-345678_summary').click(); + cy.get('#DUOS-456789_summary').click(); + + cy.get('#restore_dataset_123456').should('not.exist'); + cy.get('#restore_dataset_234567').should('not.exist'); + cy.get('#restore_dataset_345678').should('not.exist'); + cy.get('#restore_dataset_456789').should('not.exist'); + + cy.get('#DUOS-123456_summary').find('[data-testid="DeleteIcon"]').should('not.exist'); + cy.get('#DUOS-234567_summary').find('[data-testid="DeleteIcon"]').should('not.exist'); + cy.get('#DUOS-345678_summary').find('[data-testid="DeleteIcon"]').should('not.exist'); + cy.get('#DUOS-456789_summary').find('[data-testid="DeleteIcon"]').should('not.exist'); + }); + }); +}); + diff --git a/src/Routes.jsx b/src/Routes.jsx index 219c8411f..ee9cb51cc 100644 --- a/src/Routes.jsx +++ b/src/Routes.jsx @@ -86,7 +86,8 @@ const Routes = (props) => ( {/* Order is important for processing links with embedded dataRequestIds */} - + {DAAUtils.isEnabled() && } diff --git a/src/libs/ajax/DAR.js b/src/libs/ajax/DAR.js index 4adbd103a..61729f8e7 100644 --- a/src/libs/ajax/DAR.js +++ b/src/libs/ajax/DAR.js @@ -5,7 +5,7 @@ import { Config } from '../config'; import axios from 'axios'; import { isFileEmpty } from '../utils'; import { getApiUrl, fetchOk, getOntologyUrl, fetchAny } from '../ajax'; - +import {DAAUtils} from '../../utils/DAAUtils'; export const DAR = { //v2 get for DARs @@ -15,16 +15,20 @@ export const DAR = { return await res.json(); }, - //v2 update for dar partials + //v2, v3 Draft DAR Update updateDarDraft: async (dar, referenceId) => { - const url = `${await getApiUrl()}/api/dar/v2/draft/${referenceId}`; + const url = DAAUtils.isEnabled() ? + `${await getApiUrl()}/api/dar/v3/draft/${referenceId}` : + `${await getApiUrl()}/api/dar/v2/draft/${referenceId}`; const res = await axios.put(url, dar, Config.authOpts()); return res.data; }, - //api endpoint for v2 draft submission + //v2, v3 Draft DAR Creation postDarDraft: async (dar) => { - const url = `${await getApiUrl()}/api/dar/v2/draft/`; + const url = DAAUtils.isEnabled() ? + `${await getApiUrl()}/api/dar/v3/draft` : + `${await getApiUrl()}/api/dar/v2/draft`; const res = await axios.post(url, dar, Config.authOpts()); return res.data; }, @@ -36,10 +40,12 @@ export const DAR = { return await res; }, - //v2 endpoint for DAR POST + //v2, v3 DAR Creation postDar: async (dar) => { const filteredDar = fp.omit(['createDate', 'sortDate', 'data_access_request_id'])(dar); - const url = `${await getApiUrl()}/api/dar/v2`; + const url = DAAUtils.isEnabled() ? + `${await getApiUrl()}/api/dar/v3` : + `${await getApiUrl()}/api/dar/v2`; const res = axios.post(url, filteredDar, Config.authOpts()); return await res.data; }, diff --git a/src/pages/dar_application/DataAccessRequest.jsx b/src/pages/dar_application/DataAccessRequest.jsx index 8274d095c..173098aac 100644 --- a/src/pages/dar_application/DataAccessRequest.jsx +++ b/src/pages/dar_application/DataAccessRequest.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { DataSet } from '../../libs/ajax/DataSet'; import { DAR } from '../../libs/ajax/DAR'; import {FormField, FormFieldTitle, FormFieldTypes, FormValidators} from '../../components/forms/forms'; @@ -10,6 +10,8 @@ import { needsGsoAcknowledgement, newIrbDocumentExpirationDate, } from '../../utils/darFormUtils'; +import SelectableDatasets from './SelectableDatasets'; +import {DAAUtils} from '../../utils/DAAUtils'; const formatOntologyForSelect = (ontology) => { return { @@ -88,20 +90,22 @@ export default function DataAccessRequest(props) { uploadedCollaborationLetter, updateCollaborationLetter, setDatasets, + setSelectedDatasets, validation, readOnlyMode, includeInstructions, formValidationChange, - ariaLevel = 2 + ariaLevel = 2, + draftDar } = props; const irbProtocolExpiration = formData.irbProtocolExpiration || newIrbDocumentExpirationDate(); + // i need to figure out a way to only actually remove them without using onChange const onChange = ({key, value}) => { formFieldChange({key, value}); }; - const onValidationChange = ({key, validation}) => { formValidationChange({key, validation}); }; @@ -137,35 +141,46 @@ export default function DataAccessRequest(props) { // eslint-disable-next-line react/no-unknown-property
- formatSearchDataset(ds))} - selectConfig={{ - // return custom html for displaying dataset options - formatOptionLabel: (opt) => opt.label, - // return string value of dataset for accessibility / html keys - getOptionLabel: (opt) => opt.displayText, - }} - loadOptions={(query, callback) => searchDatasets(query, callback, datasets)} - placeholder={'Dataset Name, Sample Collection ID, or PI'} - onChange={async ({key, value}) => { - const datasets = value.map((val) => val.dataset); - const datasetIds = datasets?.map((ds) => ds.dataSetId); - const fullDatasets = await DataSet.getDatasetsByIds(datasetIds); - onChange({key, value: datasetIds}); - setDatasets(fullDatasets); - }} - /> + {DAAUtils.isEnabled() ? +
+ +

Currently selected datasets:

+ +
: + formatSearchDataset(ds))} + selectConfig={{ + // return custom html for displaying dataset options + formatOptionLabel: (opt) => opt.label, + // return string value of dataset for accessibility / html keys + getOptionLabel: (opt) => opt.displayText, + }} + loadOptions={(query, callback) => searchDatasets(query, callback, datasets)} + placeholder={'Dataset Name, Sample Collection ID, or PI'} + onChange={async ({key, value}) => { + const datasets = value.map((val) => val.dataset); + const datasetIds = datasets?.map((ds) => ds.dataSetId); + const fullDatasets = await DataSet.getDatasetsByIds(datasetIds); + onChange({key, value: datasetIds}); + setDatasets(fullDatasets); + }} + /> + } { }; const [datasets, setDatasets] = useState([]); + const [selectedDatasets, setSelectedDatasets] = useState([]); const [dataUseTranslations, setDataUseTranslations] = useState([]); useEffect(() => { @@ -350,7 +351,7 @@ const DataAccessRequestApplication = (props) => { const attemptSubmit = () => { const validation = validateDARFormData({ formData, - datasets, + datasets: (props.draftDar && DAAUtils.isEnabled()) ? selectedDatasets : datasets, dataUseTranslations, irbDocument: uploadedIrbDocument, collaborationLetter: uploadedCollaborationLetter, @@ -444,13 +445,16 @@ const DataAccessRequestApplication = (props) => { const saveDarDraft = async () => { let formattedFormData = cloneDeep(formData); // DAR datasetIds needs to be a list of ids + if (DAAUtils.isEnabled()) { + formattedFormData.datasetIds = selectedDatasets.map(d => d.dataSetId); + } // Make sure we navigate back to the current DAR after saving. const { dataRequestId } = props.match.params; try { let referenceId = formattedFormData.referenceId; - let darPartialResponse = await updateDraftResponse(formattedFormData, referenceId); + setDatasets(await DataSet.getDatasetsByIds(formData.datasetIds)); referenceId = darPartialResponse.referenceId; if (isNil(dataRequestId)) { props.history.replace('/dar_application/' + referenceId); @@ -592,6 +596,8 @@ const DataAccessRequestApplication = (props) => { uploadedIrbDocument={uploadedIrbDocument} updateUploadedIrbDocument={updateIrbDocument} setDatasets={setDatasets} + setSelectedDatasets={setSelectedDatasets} + draftDar={props.draftDar} />
@@ -610,7 +616,7 @@ const DataAccessRequestApplication = (props) => {
{DAAUtils.isEnabled() ? setIsAttested(false)} isAttested={isAttested} @@ -629,7 +635,13 @@ const DataAccessRequestApplication = (props) => { {isAttested &&
- setShowDialogSave(true)} isLoading={isLoading} formData={formData} datasets={datasets} dataUseTranslations={dataUseTranslations} /> + setShowDialogSave(true)} + isLoading={isLoading} + formData={formData} + datasets={DAAUtils.isEnabled() ? selectedDatasets : datasets} + dataUseTranslations={dataUseTranslations} />
}
diff --git a/src/pages/dar_application/RequiredDAAs.jsx b/src/pages/dar_application/RequiredDAAs.jsx index a16e2bf05..df284c4ae 100644 --- a/src/pages/dar_application/RequiredDAAs.jsx +++ b/src/pages/dar_application/RequiredDAAs.jsx @@ -3,20 +3,20 @@ import React from 'react'; export default function RequiredDAAs(props) { const { datasets, daas, daaDownload } = props; const fileNames = new Set(); - const daaDivs = datasets.map((dataset) => { + const daaDivs = datasets.map((dataset, index) => { const datasetDacId = dataset.dacId; if (!datasetDacId) { - return
; + return
; } const daa = daas.find((daa) => daa.dacs?.some((d) => d.dacId === datasetDacId)); const id = daa.daaId; const fileName = daa.file.fileName.split('.')[0]; if (fileNames.has(fileName)) { - return
; + return
; } fileNames.add(fileName); return ( -
+
{daaDownload(id, fileName)}
); diff --git a/src/pages/dar_application/SelectableDatasets.jsx b/src/pages/dar_application/SelectableDatasets.jsx new file mode 100644 index 000000000..f9095db79 --- /dev/null +++ b/src/pages/dar_application/SelectableDatasets.jsx @@ -0,0 +1,98 @@ +import React, {useEffect, useState} from 'react'; +import DeleteIcon from '@mui/icons-material/Delete'; +import ReactTooltip from 'react-tooltip'; +import RestoreFromTrashIcon from '@mui/icons-material/RestoreFromTrash'; + +export default function SelectableDatasets(props) { + const {datasets, setSelectedDatasets, disabled} = props; + const [removedIds, setRemovedIds] = useState([]); + + useEffect(() => { + // Populate parent state with the current state of datasets to be saved to the DAR + const newSelectedDatasets = datasets.filter(ds => !removedIds.includes(ds.dataSetId)); + setSelectedDatasets(newSelectedDatasets); + }, [removedIds, datasets, setSelectedDatasets]); + + const updateLocalState = (ds) => { + if (removedIds.includes(ds.dataSetId)) { + setRemovedIds(removedIds.toSpliced(removedIds.indexOf(ds.dataSetId), 1)); + } else { + setRemovedIds(removedIds.concat(ds.dataSetId)); + } + }; + + const datasetDescriptionDiv = (ds) => { + return
+
{ds.datasetIdentifier}
+
|
+
{ds.datasetName}
+
; + }; + + const deletableStyled = (ds) => { + const isDeletable = removedIds.length < datasets.length - 1; + const clickable = isDeletable && !disabled; + return
updateLocalState(ds)} : {})}> + {datasetDescriptionDiv(ds)} + + <> + {!disabled && } + {!isDeletable && + + The last dataset can not be deleted + } + + + +
; + }; + + const unDeletableStyled = (ds) => { + const style = disabled ? + {backgroundColor: 'lightgray', opacity: .5} : + {backgroundColor: 'lightgray', opacity: .5, cursor: 'pointer'}; + return
updateLocalState(ds)})}> + {datasetDescriptionDiv(ds)} + + {!disabled && } + + +
; + }; + + const datasetList = () => { + return datasets.map((ds) => { + return removedIds.includes(ds.dataSetId) ? + unDeletableStyled(ds) : + deletableStyled(ds); + }); + + }; + + return ( +
+ {datasetList()} +
+ ); + +}