diff --git a/.eslintrc b/.eslintrc index 43cd6b850..699df6366 100644 --- a/.eslintrc +++ b/.eslintrc @@ -30,6 +30,7 @@ "no-unused-vars": "warn", "no-extra-semi": "warn", "quotes": ["warn", "single", {"avoidEscape": true, "allowTemplateLiterals": true}], // Checks for single quote usage where possible + "jsx-quotes": ["warn", "prefer-single"], "react-hooks/rules-of-hooks": "error", // Checks rules of Hooks "react-hooks/exhaustive-deps": "warn", // Checks effect dependencies "react/jsx-filename-extension": [ @@ -78,8 +79,8 @@ ] }, "extends": [ - "eslint:recommended", - "plugin:react/recommended", + "eslint:recommended", + "plugin:react/recommended", "plugin:@typescript-eslint/eslint-recommended" ] } diff --git a/cypress/component/DAC/admin.json b/cypress/component/DAC/admin.json new file mode 100644 index 000000000..eb012674b --- /dev/null +++ b/cypress/component/DAC/admin.json @@ -0,0 +1,15 @@ +{ + "userId": 2, + "displayName": "Admin", + "institution": { + "id": 150, + "name": "The Broad Institute of MIT and Harvard" + }, + "roles": [ + { + "userId": 2, + "roleId": 4, + "name": "Admin" + } + ] +} diff --git a/cypress/component/DAC/chair.json b/cypress/component/DAC/chair.json new file mode 100644 index 000000000..52b268236 --- /dev/null +++ b/cypress/component/DAC/chair.json @@ -0,0 +1,16 @@ +{ + "userId": 1, + "displayName": "Chairperson", + "institution": { + "id": 150, + "name": "The Broad Institute of MIT and Harvard" + }, + "roles": [ + { + "userId": 1, + "roleId": 2, + "name": "Chairperson", + "dacId": 1 + } + ] +} diff --git a/cypress/component/DAC/daas.json b/cypress/component/DAC/daas.json new file mode 100644 index 000000000..66d14b316 --- /dev/null +++ b/cypress/component/DAC/daas.json @@ -0,0 +1,21 @@ +[ + { + "daaId": 1, + "createUserId": 3479, + "createDate": 1722023675199, + "updateUserId": 3479, + "updateDate": 1722023675199, + "initialDacId": 1, + "file": { + "fileStorageObjectId": 1, + "entityId": "1", + "fileName": "DUOS_Uniform_Data_Access_Agreement.pdf", + "category": "dataAccessAgreement", + "mediaType": "application/octet-stream", + "createUserId": 3479, + "createDate": 1722023675199, + "deleted": false + }, + "broadDaa": true + } +] diff --git a/cypress/component/DAC/dac.json b/cypress/component/DAC/dac.json new file mode 100644 index 000000000..e501436df --- /dev/null +++ b/cypress/component/DAC/dac.json @@ -0,0 +1,67 @@ +{ + "dacId": 1, + "name": "Test DAC", + "description": "Test DAC", + "createDate": "Oct 6, 2020", + "updateDate": "Jun 27, 2024", + "chairpersons": [ + { + "userId": 1, + "email": "test@broadinstitute.org", + "displayName": "Chairperson", + "createDate": 1704827256598, + "roles": [ + { + "userId": 1, + "roleId": 2, + "name": "Chairperson", + "dacId": 1 + } + ], + "emailPreference": true, + "institutionId": 150, + "eraCommonsId": "test" + } + ], + "members": [ + { + "userId": 2, + "email": "test2@broadinstitute.org", + "displayName": "Member", + "createDate": 1704827256598, + "roles": [ + { + "userId": 2, + "roleId": 1, + "name": "Member", + "dacId": 1 + } + ], + "emailPreference": true, + "institutionId": 150, + "eraCommonsId": "test" + } + ], + "electionIds": [], + "datasetIds": [], + "email": "grushton@broadinstitute.org", + "associatedDaa": { + "daaId": 1, + "createUserId": 5146, + "createDate": 1713386755554, + "updateUserId": 5146, + "updateDate": 1713386755554, + "initialDacId": 8, + "file": { + "fileStorageObjectId": 216, + "entityId": 1, + "fileName": "test_daa.txt", + "category": "dataAccessAgreement", + "mediaType": "application/octet-stream", + "createUserId": 5146, + "createDate": 1713386755554, + "deleted": false + }, + "broadDaa": true + } +} diff --git a/cypress/component/DAC/dacUsers.spec.tsx b/cypress/component/DAC/dacUsers.spec.tsx new file mode 100644 index 000000000..1daa3afd3 --- /dev/null +++ b/cypress/component/DAC/dacUsers.spec.tsx @@ -0,0 +1,36 @@ +/* eslint-disable no-undef,no-console */ + +import React from 'react'; +import {mount} from 'cypress/react'; +import {DacUsers} from '../../../src/pages/manage_dac/DacUsers.jsx'; +import dac from './dac.json'; + +describe('Dac User Tests', () => { + it('Shows a DAC Members', () => { + cy.viewport(600, 800); + const props = { + dac: dac, + removeButton: true, + removeHandler: () => { console.log('Remove Button Clicked'); } + }; + mount(); + dac.chairpersons.forEach(u => { + cy.contains(u.displayName); + cy.get('[data-cy="remove_button_' + u.userId + '"]').click().then(() => { + cy.contains('Pending Removal'); + }); + cy.get('[data-cy="remove_button_' + u.userId + '"]').click().then(() => { + cy.get('Pending Removal').should('not.exist'); + }); + }); + dac.members.forEach(u => { + cy.contains(u.displayName); + cy.get('[data-cy="remove_button_' + u.userId + '"]').click().then(() => { + cy.contains('Pending Removal'); + }); + cy.get('[data-cy="remove_button_' + u.userId + '"]').click().then(() => { + cy.get('Pending Removal').should('not.exist'); + }); + }); + }); +}); diff --git a/cypress/component/DAC/editDac.spec.tsx b/cypress/component/DAC/editDac.spec.tsx new file mode 100644 index 000000000..6ea83af6d --- /dev/null +++ b/cypress/component/DAC/editDac.spec.tsx @@ -0,0 +1,119 @@ +/* eslint-disable no-undef */ + +import React from 'react'; +import {mount} from 'cypress/react'; +import {DAA} from '../../../src/libs/ajax/DAA'; +import {DAC} from '../../../src/libs/ajax/DAC'; +import {Storage} from '../../../src/libs/storage'; +import EditDac from '../../../src/pages/manage_dac/EditDac'; +import {BrowserRouter} from 'react-router-dom'; +import admin from './admin.json'; +import chair from './chair.json'; +import daas from './daas.json'; +import dac from './dac.json'; +import {setUserRoleStatuses} from '../../../src/libs/utils'; + +// It's necessary to wrap components that contain `Link` components +const WrappedEditDac = (props) => { + return ; +}; + +describe('EditDAC Tests', () => { + + Cypress._.each([admin, chair], (user) => { + it('Edit DAC page should load for ' + user.displayName, () => { + cy.viewport(600, 800); + cy.stub(Storage, 'getCurrentUser').returns(user); + cy.stub(DAC, 'get').returns(dac); + cy.stub(DAA, 'getDaas').returns([]); + const props = {match: {params: {dacId: dac.dacId}}}; + mount(WrappedEditDac(props)); + cy.contains(dac.name).should('exist'); + cy.get('[data-cy="dac_name"]').should('not.be.disabled'); + cy.get('[data-cy="dac_description"]').should('not.be.disabled'); + cy.get('[data-cy="dac_email"]').should('not.be.disabled'); + cy.get('[data-cy="btn_save"]').should('not.be.disabled'); + cy.get('[data-cy="btn_cancel"]').should('not.be.disabled'); + cy.get('[data-cy="daa_radio"]').should('not.be.disabled'); + cy.get('[data-cy="daa_upload_button"]').should('not.be.disabled'); + }); + }); + + it('Admins can create a DAC', () => { + cy.viewport(600, 600); + Storage.clearStorage(); + setUserRoleStatuses(admin, Storage); + cy.stub(DAA, 'getDaas').returns(daas); + cy.stub(DAC, 'removeDacMember').returns(Promise.resolve(200)); + cy.stub(DAC, 'addDacChair').returns(Promise.resolve(200)); + cy.stub(DAC, 'removeDacChair').returns(Promise.resolve(200)); + cy.stub(DAC, 'addDacMember').returns(Promise.resolve(200)); + cy.stub(DAA, 'addDaaToDac').returns(Promise.resolve(200)); + const props = { + match: { + params: { + dacId: undefined + } + }, + history: { + push() { + } + } + }; + mount(WrappedEditDac(props)); + cy.get('[data-cy="dac_name"]').should('not.be.disabled'); + cy.get('[data-cy="dac_name"]').should('be.empty'); + cy.get('[data-cy="dac_description"]').should('not.be.disabled'); + cy.get('[data-cy="dac_description"]').should('be.empty'); + cy.get('[data-cy="dac_email"]').should('not.be.disabled'); + cy.get('[data-cy="dac_email"]').should('be.empty'); + cy.get('[data-cy="btn_save"]').should('not.be.disabled'); + cy.get('[data-cy="btn_cancel"]').should('not.be.disabled'); + + // Create a DAC + const dacCreate = cy.stub(DAC, 'create').returns(dac); + + cy.get('[data-cy="dac_name"]').type('New DAC Name'); + cy.get('[data-cy="dac_description"]').type('New DAC Description'); + cy.get('[data-cy="dac_email"]').type('New DAC Email'); + cy.get('[data-cy="daa_radio"]').first().check(); + cy.get('[data-cy="btn_save"]').click().then(() => { + expect(dacCreate).to.be.called; + }); + }); + + it('Chairs cannot create a DAC', () => { + cy.viewport(600, 600); + Storage.clearStorage(); + setUserRoleStatuses(chair, Storage); + cy.stub(DAA, 'getDaas').returns(daas); + cy.stub(DAC, 'removeDacMember').returns(Promise.resolve(200)); + cy.stub(DAC, 'addDacChair').returns(Promise.resolve(200)); + cy.stub(DAC, 'removeDacChair').returns(Promise.resolve(200)); + cy.stub(DAC, 'addDacMember').returns(Promise.resolve(200)); + cy.stub(DAA, 'addDaaToDac').returns(Promise.resolve(200)); + const props = { + match: { + params: { + dacId: undefined + } + }, + history: { + push() { + } + } + }; + mount(WrappedEditDac(props)); + + // Try to create a DAC + const dacCreate = cy.stub(DAC, 'create'); + cy.get('[data-cy="dac_name"]').type('New DAC Name'); + cy.get('[data-cy="dac_description"]').type('New DAC Description'); + cy.get('[data-cy="dac_email"]').type('New DAC Email'); + cy.get('[data-cy="daa_radio"]').first().check(); + cy.get('[data-cy="btn_save"]').click().then(() => { + expect(dacCreate).to.not.be.called; + }); + }); + +}); diff --git a/cypress/component/DAC/manageEditDac.spec.tsx b/cypress/component/DAC/manageEditDac.spec.tsx new file mode 100644 index 000000000..fcc6a7ef8 --- /dev/null +++ b/cypress/component/DAC/manageEditDac.spec.tsx @@ -0,0 +1,101 @@ +/* eslint-disable no-undef */ + +import React from 'react'; +import {mount} from 'cypress/react'; +import {DAC} from '../../../src/libs/ajax/DAC'; +import {Storage} from '../../../src/libs/storage'; +import ManageEditDac from '../../../src/pages/manage_dac/ManageEditDac'; +import {BrowserRouter} from 'react-router-dom'; +import admin from './admin.json'; +import chair from './chair.json'; +import dac from './dac.json'; +import {setUserRoleStatuses} from '../../../src/libs/utils'; + +// It's necessary to wrap components that contain `Link` components +const WrappedManageEditDac = (props) => { + return ; +}; + +/** + * This manage page is the pre-Data Access Agreement way to edit a DAC and will be removed when DAA work is complete. + */ +describe('ManageEditDAC Tests', () => { + + Cypress._.each([admin, chair], (user) => { + it('Manage Edit DAC page should load for ' + user.displayName, () => { + cy.viewport(600, 600); + setUserRoleStatuses(user, Storage); + cy.stub(DAC, 'get').returns(dac); + const props = {match: {params: {dacId: dac.dacId}}}; + mount(WrappedManageEditDac(props)); + cy.contains(dac.name).should('exist'); + cy.get('[data-cy="dac_name"]').should('not.be.disabled'); + cy.get('[data-cy="dac_description"]').should('not.be.disabled'); + cy.get('[data-cy="dac_email"]').should('not.be.disabled'); + cy.get('[data-cy="btn_save"]').should('not.be.disabled'); + cy.get('[data-cy="btn_cancel"]').should('not.be.disabled'); + }); + }); + + it('Admins can create a DAC', () => { + cy.viewport(600, 600); + Storage.clearStorage(); + setUserRoleStatuses(admin, Storage); + const props = { + match: { + params: { + dacId: undefined + } + }, + history: { + push() { + } + } + }; + mount(WrappedManageEditDac(props)); + cy.get('[data-cy="dac_name"]').should('not.be.disabled'); + cy.get('[data-cy="dac_name"]').should('be.empty'); + cy.get('[data-cy="dac_description"]').should('not.be.disabled'); + cy.get('[data-cy="dac_description"]').should('be.empty'); + cy.get('[data-cy="dac_email"]').should('not.be.disabled'); + cy.get('[data-cy="dac_email"]').should('be.empty'); + cy.get('[data-cy="btn_save"]').should('not.be.disabled'); + cy.get('[data-cy="btn_cancel"]').should('not.be.disabled'); + + // Create a DAC + const dacCreate = cy.stub(DAC, 'create'); + + cy.get('[data-cy="dac_name"]').type('New DAC Name'); + cy.get('[data-cy="dac_description"]').type('New DAC Description'); + cy.get('[data-cy="dac_email"]').type('New DAC Email'); + cy.get('[data-cy="btn_save"]').click().then(() => { + expect(dacCreate).to.be.called; + }); + }); + + it('Chairs cannot create a DAC', () => { + cy.viewport(600, 600); + Storage.clearStorage(); + setUserRoleStatuses(chair, Storage); + const dacCreate = cy.stub(DAC, 'create'); + const props = { + match: { + params: { + dacId: undefined + } + }, + history: { + push() { + } + } + }; + mount(WrappedManageEditDac(props)); + cy.get('[data-cy="dac_name"]').type('New DAC Name'); + cy.get('[data-cy="dac_description"]').type('New DAC Description'); + cy.get('[data-cy="dac_email"]').type('New DAC Email'); + cy.get('[data-cy="btn_save"]').click().then(() => { + expect(dacCreate).to.not.be.called; + }); + }); + +}); diff --git a/cypress/support/component-index.html b/cypress/support/component-index.html index aec5e614c..23b2efe9d 100644 --- a/cypress/support/component-index.html +++ b/cypress/support/component-index.html @@ -10,7 +10,8 @@ -
+ +
\ No newline at end of file diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json index 4d630e944..186c778b4 100644 --- a/cypress/tsconfig.json +++ b/cypress/tsconfig.json @@ -10,7 +10,8 @@ "node" ], "jsx": "react", - "esModuleInterop": true + "esModuleInterop": true, + "resolveJsonModule": true }, "include": [ "**/*.ts", diff --git a/src/pages/manage_dac/DacUsers.jsx b/src/pages/manage_dac/DacUsers.jsx index fd0799af8..b026b35fe 100644 --- a/src/pages/manage_dac/DacUsers.jsx +++ b/src/pages/manage_dac/DacUsers.jsx @@ -1,15 +1,16 @@ import * as ld from 'lodash'; -import React, { useState } from 'react'; -import { CHAIR, MEMBER } from './AddDacModal'; +import React, {useState} from 'react'; +import {CHAIR, MEMBER} from './AddDacModal'; -const buttonPadding = { paddingTop: 6 }; -const headerStyle = { fontWeight: 500, color: '#00609f' }; +const buttonPadding = {paddingTop: 6}; +const headerStyle = {fontWeight: 500, color: '#00609f'}; export const DacUsers = (props) => { const [state, setState] = useState({ dac: props.dac, removeButton: props.removeButton, - removeHandler: props.removeButton ? props.removeHandler : () => { }, + removeHandler: props.removeButton ? props.removeHandler : () => { + }, removedIds: [] }); @@ -36,22 +37,29 @@ export const DacUsers = (props) => { const roleTitle = (role === CHAIR) ? 'Chairperson' : 'Member'; const isRemoved = state.removedIds.includes(u.userId); const rowStyle = isRemoved ? - { borderBottom: '1px solid white', padding: '.75rem 0 .75rem 0', backgroundColor: 'lightgray', opacity: .5, borderRadius: 5 } : - { borderBottom: '1px solid lightgray', padding: '.75rem 0 .75rem 0' }; + { + borderBottom: '1px solid white', + padding: '.75rem 0 .75rem 0', + backgroundColor: 'lightgray', + opacity: .5, + borderRadius: 5 + } : + {borderBottom: '1px solid lightgray', padding: '.75rem 0 .75rem 0'}; const buttonMessage = isRemoved ? 'Pending Removal' : 'Remove'; return ( -
+
{u.displayName + ' ' + u.email}
{roleTitle}
{state.removeButton &&
@@ -62,12 +70,12 @@ export const DacUsers = (props) => { }; return ( -
-
+
+
User
Role
{state.removeButton && -
+
}
{ld.flatMap(state.dac.chairpersons, (u) => makeRow(u, CHAIR))} diff --git a/src/pages/manage_dac/EditDac.jsx b/src/pages/manage_dac/EditDac.jsx index 3c5767ba6..dfa94cf8c 100644 --- a/src/pages/manage_dac/EditDac.jsx +++ b/src/pages/manage_dac/EditDac.jsx @@ -1,21 +1,21 @@ import * as ld from 'lodash'; -import React, { useEffect, useState } from 'react'; +import React, {useEffect, useState} from 'react'; import AsyncSelect from 'react-select/async'; -import { DAC } from '../../libs/ajax/DAC'; -import { DAA } from '../../libs/ajax/DAA'; -import { Models } from '../../libs/models'; -import { PromiseSerial } from '../../libs/utils'; -import { Alert } from '../../components/Alert'; -import { Link } from 'react-router-dom'; -import { DacUsers } from './DacUsers'; -import { Notifications } from '../../libs/utils'; +import {DAC} from '../../libs/ajax/DAC'; +import {DAA} from '../../libs/ajax/DAA'; +import {Models} from '../../libs/models'; +import {PromiseSerial} from '../../libs/utils'; +import {Alert} from '../../components/Alert'; +import {Link} from 'react-router-dom'; +import {DacUsers} from './DacUsers'; +import {Notifications} from '../../libs/utils'; import editDACIcon from '../../images/dac_icon.svg'; import backArrowIcon from '../../images/back_arrow.svg'; -import { Spinner } from '../../components/Spinner'; -import { Styles } from '../../libs/theme'; +import {Spinner} from '../../components/Spinner'; +import {Styles} from '../../libs/theme'; import DUOSUniformDataAccessAgreement from '../../assets/DUOS_Uniform_Data_Access_Agreement.pdf'; import PublishIcon from '@mui/icons-material/Publish'; -import { UploadDaaModal } from '../../components/modals/UploadDaaModal'; +import {UploadDaaModal} from '../../components/modals/UploadDaaModal'; import {Storage} from '../../libs/storage'; export const CHAIR = 'chair'; @@ -56,13 +56,12 @@ export default function EditDac(props) { const daas = await DAA.getDaas(); const broadDaa = daas.find(daa => daa.broadDaa === true); setBroadDaa(broadDaa); - setState(prev => ({ ...prev, dac: fetchedDac })); + setState(prev => ({...prev, dac: fetchedDac})); const matchingDaas = daas.filter(daa => daa.initialDacId === fetchedDac.dacId); setMatchingDaas(matchingDaas); const daa = fetchedDac?.associatedDaa ? fetchedDac.associatedDaa : null; setSelectedDaa(daa?.daaId ? daa : null); - } - catch(e) { + } catch (e) { Notifications.showError({text: 'Error: Unable to retrieve current DAC from server'}); } } else { @@ -70,8 +69,7 @@ export default function EditDac(props) { const daas = await DAA.getDaas(); const broadDaa = daas.find(daa => daa.broadDaa === true); setBroadDaa(broadDaa); - } - catch(e) { + } catch (e) { Notifications.showError({text: 'Error: Unable to retrieve current DAC from server'}); } } @@ -171,7 +169,9 @@ export default function EditDac(props) { const userSearch = (invalidUserIds, query, callback) => { DAC.autocompleteUsers(query).then( items => { - const filteredUsers = ld.filter(items, item => { return !invalidUserIds.includes(item.userId); }); + const filteredUsers = ld.filter(items, item => { + return !invalidUserIds.includes(item.userId); + }); const options = filteredUsers.map(function (item) { return { key: item.userId, @@ -272,7 +272,7 @@ export default function EditDac(props) { } }; - const handleAttachment = async(attachment) => { + const handleAttachment = async (attachment) => { if (dacId !== undefined) { setUploadedDaaFile(attachment); setDaaFileData(attachment[0]); @@ -301,7 +301,7 @@ export default function EditDac(props) { dirtyFlag: true })); } else { - setSelectedDaa({ ...selectedDaa, daaId: daaId }); + setSelectedDaa({...selectedDaa, daaId: daaId}); setNewDaaId(daaId); setState(prev => ({ ...prev, @@ -310,21 +310,24 @@ export default function EditDac(props) { } }; - const DaaItem = ({ specificDaa }) => ( -
- handleDaaChange(specificDaa.daaId)} style={{accentColor:'#00609f'}}/> -
-
-
+ const DaaItem = ({specificDaa}) => ( +
+ handleDaaChange(specificDaa.daaId)} style={{accentColor: '#00609f'}}/> +
+
+
{specificDaa.file.fileName}
-
+
Uploaded on {specificDaa?.updateDate ? new Date(specificDaa.updateDate).toLocaleDateString() : ''}
-
-
- {DAA.getDaaFileById(specificDaa.daaId, specificDaa.file.fileName);}} className='button button-white' style={{ padding: '10px 12px' }}> +
-
{dacText}
-
{dacId === undefined ? 'Create DAC' : fetchedDac?.name}
+
{dacText}
+
{dacId === undefined ? 'Create DAC' : fetchedDac?.name}

-
-
-
-
+
+
+
+
- -
+ +
- -
+ +