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()}
+
+ );
+
+}