diff --git a/cypress/component/DataSearch/dataset_search_footer.spec.js b/cypress/component/DataSearch/dataset_search_footer.spec.js new file mode 100644 index 000000000..4bf0c186c --- /dev/null +++ b/cypress/component/DataSearch/dataset_search_footer.spec.js @@ -0,0 +1,65 @@ +/* eslint-disable no-undef */ +import {mount} from 'cypress/react'; +import {React} from 'react'; +import {DatasetSearchFooter} from '../../../src/components/data_search/DatasetSearchFooter'; + +const datasets = [ + { + datasetId: 123456, + study: { + studyId: 1, + } + }, + { + datasetId: 234567, + study: { + studyId: 1, + } + }, + { + datasetId: 345678, + study: { + studyId: 2, + } + }, +]; + +const oneDatasetProps = { + selectedDatasets: [123456], + datasets: datasets, + onClick: () => {}, +}; + +const oneStudyProps = { + selectedDatasets: [123456, 234567], + datasets: datasets, + onClick: () => {}, +}; + +const twoStudiesProps = { + selectedDatasets: [123456, 234567, 345678], + datasets: datasets, + onClick: () => {}, +}; + +describe('Dataset Search Footer renders correct text and button', () => { + + it('Shows button and single dataset and study text', () => { + mount(); + cy.contains('1 dataset selected from 1 study'); + cy.contains('Apply for Access'); + }); + + + it('Shows button and two datasets from one study text', () => { + mount(); + cy.contains('2 datasets selected from 1 study'); + cy.contains('Apply for Access'); + }); + + it('Shows button and three datasets from two studies text', () => { + mount(); + cy.contains('3 datasets selected from 2 studies'); + cy.contains('Apply for Access'); + }); +}); diff --git a/cypress/component/DataSearch/dataset_search_table.spec.js b/cypress/component/DataSearch/dataset_search_table.spec.js new file mode 100644 index 000000000..85c1e92c6 --- /dev/null +++ b/cypress/component/DataSearch/dataset_search_table.spec.js @@ -0,0 +1,43 @@ +/* eslint-disable no-undef */ +import {React} from 'react'; +import {mount} from 'cypress/react'; +import DatasetSearchTable from '../../../src/components/data_search/DatasetSearchTable'; +import {TerraDataRepo} from '../../../src/libs/ajax/TerraDataRepo'; + +const datasets = [ + { + datasetId: 123456, + datasetIdentifier: `DUOS-123456`, + datasetName: 'Some Dataset 1', + study: { + studyId: 1, + dataCustodianEmail: ['Some Data Custodian Email 1'], + } + } +]; + +const props = { + datasets: datasets, + history: {} +}; + +describe('Dataset Search Table tests', () => { + + describe('Data library with three datasets', () => { + beforeEach(() => { + cy.stub(TerraDataRepo, 'listSnapshotsByDatasetIds').returns({}); + mount(); + }); + + it('When no datasets are selected the footer does not appear', () => { + cy.contains('1 dataset selected from 1 study').should('not.exist'); + }); + + + it('When a dataset is selected the footer appears', () => { + cy.get('#header-checkbox').click(); + cy.contains('1 dataset selected from 1 study'); + }); + + }); +}); diff --git a/cypress/component/SignIn/sign_in_button.spec.js b/cypress/component/SignIn/sign_in_button.spec.js index d492b334d..d24c7193d 100644 --- a/cypress/component/SignIn/sign_in_button.spec.js +++ b/cypress/component/SignIn/sign_in_button.spec.js @@ -10,12 +10,18 @@ import {Metrics} from '../../../src/libs/ajax/Metrics'; import {StackdriverReporter} from '../../../src/libs/stackdriverReporter'; import {ToS} from '../../../src/libs/ajax/ToS'; import {mockOidcUser} from '../Auth/mockOidcUser'; - const signInText = 'Sign In'; const duosUser = { displayName: 'display name', email: 'test@user.com', + isAdmin: true, + isAlumni: false, + isChairPerson: false, + isDataSubmitter: false, + isMember: false, + isResearcher: false, + isSigningOfficial: false, roles: [{ name: 'Admin' }] @@ -33,6 +39,11 @@ const notAcceptedUserStatus = Object.assign({}, userStatus, {'tosAccepted': fals describe('Sign In: Component Loads', function () { + // Intercept configuration calls + beforeEach(() => { + cy.initApplicationConfig(); + }); + it('Sign In Button Loads', function () { cy.viewport(600, 300); mount(); @@ -42,14 +53,15 @@ describe('Sign In: Component Loads', function () { it('Sign In: On Success', function () { cy.viewport(600, 300); cy.stub(Auth, 'signIn').returns(Promise.resolve(mockOidcUser)); - cy.stub(User, 'getMe').returns(duosUser); + cy.intercept({method: 'GET', url: '**/api/user/me'}, {statusCode: 200, body: duosUser}).as('getMe'); cy.stub(StackdriverReporter, 'report'); cy.stub(Metrics, 'identify'); cy.stub(Metrics, 'syncProfile'); cy.stub(Metrics, 'captureEvent'); cy.stub(ToS, 'getStatus').returns(userStatus); mount(); - cy.get('button').click().then(() => { + cy.get('button').click(); + cy.wait('@getMe').then(() => { expect(Storage.getCurrentUser()).to.deep.equal(duosUser); expect(Storage.getAnonymousId()).to.not.be.null; expect(StackdriverReporter.report).to.not.be.called; @@ -63,14 +75,15 @@ describe('Sign In: Component Loads', function () { const bareUser = {email: 'test@user.com'}; cy.viewport(600, 300); cy.stub(Auth, 'signIn').returns(Promise.resolve(mockOidcUser)); - cy.stub(User, 'getMe').returns(bareUser); + cy.intercept({method: 'GET', url: '**/api/user/me'}, {statusCode: 200, body: bareUser}).as('getMe'); cy.stub(StackdriverReporter, 'report'); cy.stub(Metrics, 'identify'); cy.stub(Metrics, 'syncProfile'); cy.stub(Metrics, 'captureEvent'); cy.stub(ToS, 'getStatus').returns(userStatus); mount(); - cy.get('button').click().then(() => { + cy.get('button').click(); + cy.wait('@getMe').then(() => { expect(StackdriverReporter.report).to.be.called; }); }); @@ -78,14 +91,15 @@ describe('Sign In: Component Loads', function () { it('Sign In: Redirects to ToS if not accepted', function () { cy.viewport(600, 300); cy.stub(Auth, 'signIn').returns(Promise.resolve(mockOidcUser)); - cy.stub(User, 'getMe').returns(duosUser); + cy.intercept({method: 'GET', url: '**/api/user/me'}, {statusCode: 200, body: duosUser}).as('getMe'); cy.stub(ToS, 'getStatus').returns(notAcceptedUserStatus); cy.stub(Metrics, 'identify'); cy.stub(Metrics, 'syncProfile'); cy.stub(Metrics, 'captureEvent'); let history = []; mount(); - cy.get('button').click().then(() => { + cy.get('button').click(); + cy.wait('@getMe').then(() => { expect(history).to.not.be.empty; expect(history[0].includes('tos_acceptance')).to.be.true; }); @@ -96,15 +110,15 @@ describe('Sign In: Component Loads', function () { cy.stub(Auth, 'signIn').returns(Promise.resolve(mockOidcUser)); // Simulate user not found cy.stub(User, 'getMe').throws(); - cy.stub(User, 'registerUser').returns(duosUser); + cy.intercept({method: 'POST', url: '**/api/user'}, {statusCode: 200, body: duosUser}).as('registerUser'); cy.stub(ToS, 'getStatus').returns(notAcceptedUserStatus); cy.stub(Metrics, 'identify'); cy.stub(Metrics, 'syncProfile'); cy.stub(Metrics, 'captureEvent'); let history = []; mount(); - cy.get('button').click().then(() => { - expect(User.registerUser).to.be.called; + cy.get('button').click(); + cy.wait('@registerUser').then(() => { expect(history).to.not.be.empty; expect(history[0].includes('tos_acceptance')).to.be.true; }); diff --git a/cypress/component/UserProfile/user_profile.spec.js b/cypress/component/UserProfile/user_profile.spec.js new file mode 100644 index 000000000..e50436dad --- /dev/null +++ b/cypress/component/UserProfile/user_profile.spec.js @@ -0,0 +1,50 @@ +/* eslint-disable no-undef */ + +import {mount} from 'cypress/react'; +import React from 'react'; +import {Storage} from '../../../src/libs/storage'; +import {User} from '../../../src/libs/ajax/User'; +import {Institution} from '../../../src/libs/ajax/Institution'; +import UserProfile from '../../../src/pages/user_profile/UserProfile'; + +const duosUser = { + isSigningOfficial: false, +}; + +describe('User Profile', () => { + // Intercept configuration calls + beforeEach(() => { + cy.intercept({ + method: 'GET', + url: '/config.json', + hostname: 'localhost', + }, {'env': 'ci'}); + }); + + it('Renders the user profile page', () => { + cy.stub(Storage, 'getCurrentUser').returns(duosUser); + cy.stub(Institution, 'list').returns([]); + cy.stub(User, 'getMe').returns(duosUser); + cy.stub(User, 'getApprovedDatasets').returns([]); + cy.stub(User, 'getAcknowledgements').returns({}); + mount(); + cy.get('h2').should('contain', 'Your Profile'); + }); + + it('Updates the user email preferences', () => { + cy.stub(Storage, 'getCurrentUser').returns(duosUser); + cy.stub(Institution, 'list').returns([]); + cy.stub(User, 'getMe').returns(duosUser); + cy.stub(User, 'getApprovedDatasets').returns([]); + cy.stub(User, 'getAcknowledgements').returns({}); + cy.intercept( + {method: 'PUT', url: '**/user'}, + {statusCode: 200, body: duosUser} + ).as('updateSelf'); + mount(); + cy.get('input[id="profileEmailEnabled_yes"]').check(); + cy.wait('@updateSelf').then(() => { + cy.get('div').contains('Email preference updated successfully!'); + }); + }); +}); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index d19d7fefc..c256a0e7a 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -34,4 +34,26 @@ Cypress.Commands.add('auth', async (roleName) => { const url = Cypress.env('baseUrl'); await client.request({ url }); return client.credentials; -}); \ No newline at end of file +}); + +Cypress.Commands.add('initApplicationConfig', () => { + cy.intercept({ + method: 'GET', + url: '/config.json', + hostname: 'localhost', + }, { + 'env': 'ci', + 'hash': '', + 'tag': '', + 'bardApiUrl': '', + 'apiUrl': '', + 'ontologyApiUrl': '', + 'terraUrl': '', + 'tdrApiUrl': '', + 'errorApiKey': '', + 'profileUrl': '', + 'nihUrl': '', + 'gaId': '', + 'features': {} + }); +}); diff --git a/src/components/SignInButton.tsx b/src/components/SignInButton.tsx index bede9f788..46c78ce38 100644 --- a/src/components/SignInButton.tsx +++ b/src/components/SignInButton.tsx @@ -109,9 +109,12 @@ export const SignInButton = (props: SignInButtonProps) => { const syncSignInOrRegistrationEvent = async (event: MetricsEventName) => { Storage.setAnonymousId(); - await Metrics.identify(`${Storage.getAnonymousId()}`); - await Metrics.syncProfile(); - await Metrics.captureEvent(event); + // noinspection ES6MissingAwait + Metrics.identify(`${Storage.getAnonymousId()}`); + // noinspection ES6MissingAwait + Metrics.syncProfile(); + // noinspection ES6MissingAwait + Metrics.captureEvent(event); }; const errorStreamToString = async (error: HttpError) => { diff --git a/src/components/dac_dataset_table/DACDatasetConstants.tsx b/src/components/dac_dataset_table/DACDatasetConstants.tsx new file mode 100644 index 000000000..8fdb53e90 --- /dev/null +++ b/src/components/dac_dataset_table/DACDatasetConstants.tsx @@ -0,0 +1,51 @@ +import {Styles} from '../../libs/theme'; + +export const styles = { + baseStyle: { + fontFamily: 'Montserrat', + fontSize: '1.6rem', + fontWeight: 400, + display: 'flex', + padding: '1rem 2%', + justifyContent: 'space-between', + alignItems: 'center', + whiteSpace: 'pre-wrap', + backgroundColor: 'white', + border: '1px solid #DEDEDE', + borderRadius: '4px', + margin: '0.5% 0' + }, + columnStyle: Object.assign({}, Styles.TABLE.HEADER_ROW, { + justifyContent: 'space-between', + color: '#7B7B7B', + fontFamily: 'Montserrat', + fontSize: '1.2rem', + fontWeight: 'bold', + letterSpacing: '0.2px', + textTransform: 'uppercase', + backgroundColor: 'B8CDD3', + border: 'none' + }), + cellWidths: { + duosId: '10%', + phsId: '10%', + datasetName: '15%', + studyName: '15%', + dataSubmitter: '15%', + dataCustodian: '15%', + dataUse: '10%', + status: '10%' + }, + color: { + dataUseGroup: '#000000', + votes: '#000000', + numberOfDatasets: '#000000', + datasets: '#000000', + }, + fontSize: { + dataUseGroup: '1.4rem', + votes: '1.4rem', + numberOfDatasets: '1.4rem', + datasets: '1.4rem', + }, +}; diff --git a/src/components/dac_dataset_table/DACDatasetTableCellData.jsx b/src/components/dac_dataset_table/DACDatasetTableCellData.jsx index cdf6d0421..3becb5847 100644 --- a/src/components/dac_dataset_table/DACDatasetTableCellData.jsx +++ b/src/components/dac_dataset_table/DACDatasetTableCellData.jsx @@ -1,6 +1,6 @@ import React from 'react'; import style from '../../pages/DACDatasets.module.css'; -import {styles} from './DACDatasetsTable'; +import {styles} from './DACDatasetConstants'; import DACDatasetApprovalStatus from './DACDatasetApprovalStatus'; import ReactTooltip from 'react-tooltip'; diff --git a/src/components/dac_dataset_table/DACDatasetsTable.jsx b/src/components/dac_dataset_table/DACDatasetsTable.jsx index 5cd8e556b..2f7aef1c4 100644 --- a/src/components/dac_dataset_table/DACDatasetsTable.jsx +++ b/src/components/dac_dataset_table/DACDatasetsTable.jsx @@ -1,63 +1,13 @@ import React, { useState, useEffect } from 'react'; -import { Styles } from '../../libs/theme'; import { Storage } from '../../libs/storage'; import PaginationBar from '../PaginationBar'; import SimpleTable from '../SimpleTable'; import cellData from './DACDatasetTableCellData'; +import {styles} from './DACDatasetConstants'; import {isNil} from 'lodash/fp'; import {goToPage as updatePage, recalculateVisibleTable} from '../../libs/utils'; import {useCallback} from 'react'; -export const styles = { - baseStyle: { - fontFamily: 'Montserrat', - fontSize: '1.6rem', - fontWeight: 400, - display: 'flex', - padding: '1rem 2%', - justifyContent: 'space-between', - alignItems: 'center', - whiteSpace: 'pre-wrap', - backgroundColor: 'white', - border: '1px solid #DEDEDE', - borderRadius: '4px', - margin: '0.5% 0' - }, - columnStyle: Object.assign({}, Styles.TABLE.HEADER_ROW, { - justifyContent: 'space-between', - color: '#7B7B7B', - fontFamily: 'Montserrat', - fontSize: '1.2rem', - fontWeight: 'bold', - letterSpacing: '0.2px', - textTransform: 'uppercase', - backgroundColor: 'B8CDD3', - border: 'none' - }), - cellWidths: { - duosId: '10%', - phsId: '10%', - datasetName: '15%', - studyName: '15%', - dataSubmitter: '15%', - dataCustodian: '15%', - dataUse: '10%', - status: '10%' - }, - color: { - dataUseGroup: '#000000', - votes: '#000000', - numberOfDatasets: '#000000', - datasets: '#000000', - }, - fontSize: { - dataUseGroup: '1.4rem', - votes: '1.4rem', - numberOfDatasets: '1.4rem', - datasets: '1.4rem', - }, -}; - export const DACDatasetTableColumnOptions = { DUOS_ID: 'duosId', PHS_ID: 'phsId', diff --git a/src/components/data_search/DatasetSearchFooter.tsx b/src/components/data_search/DatasetSearchFooter.tsx new file mode 100644 index 000000000..ded789eaf --- /dev/null +++ b/src/components/data_search/DatasetSearchFooter.tsx @@ -0,0 +1,40 @@ +import * as _ from 'lodash'; +import {Button} from '@mui/material'; +import * as React from 'react'; +import {Dataset} from 'src/types/model'; + +interface DatasetSearchFooterProps { + selectedDatasets: number[]; + datasets: Dataset[]; + onClick: () => void; +} +export const DatasetSearchFooter = (props: DatasetSearchFooterProps) => { + const { selectedDatasets, datasets, onClick } = props; + const selectedStudies = _.uniq( + _.filter(datasets, dataset => selectedDatasets.includes(dataset.datasetId)) + .map(dataset => dataset.study.studyId)); + const datasetText = selectedDatasets.length > 1 ? 'datasets' : 'dataset'; + const studyText = selectedStudies.length > 1 ? 'studies' : 'study'; + + return
+
{selectedDatasets.length} {datasetText} selected from {selectedStudies.length} {studyText}
+ +
; +}; diff --git a/src/components/data_search/DatasetSearchTable.jsx b/src/components/data_search/DatasetSearchTable.jsx index a67f29e57..362cc7573 100644 --- a/src/components/data_search/DatasetSearchTable.jsx +++ b/src/components/data_search/DatasetSearchTable.jsx @@ -1,6 +1,6 @@ import Tab from '@mui/material/Tab'; import Tabs from '@mui/material/Tabs'; -import useOnMount from '@mui/utils/useOnMount' +import useOnMount from '@mui/utils/useOnMount'; import * as React from 'react'; import { Box, Button } from '@mui/material'; import { useEffect, useState } from 'react'; @@ -15,6 +15,7 @@ import DatasetFilterList from './DatasetFilterList'; import { Notifications } from '../../libs/utils'; import { Styles } from '../../libs/theme'; import * as _ from 'lodash'; +import {DatasetSearchFooter} from './DatasetSearchFooter'; const styles = { subTab: { @@ -140,7 +141,7 @@ export const DatasetSearchTable = (props) => { 'dac.dacName': term } })) - } + } }); if (filterTerms.length > 0) { @@ -288,14 +289,8 @@ export const DatasetSearchTable = (props) => { })()} - - { - !isEmpty(datasets) && - - } - + + {!isEmpty(selected) && } ); diff --git a/src/components/data_search/DatasetSearchTableConstants.tsx b/src/components/data_search/DatasetSearchTableConstants.tsx index c61bda76f..07925b37c 100644 --- a/src/components/data_search/DatasetSearchTableConstants.tsx +++ b/src/components/data_search/DatasetSearchTableConstants.tsx @@ -79,7 +79,7 @@ export const makeStudyTableHeaders = (datasets: DatasetTerm[], selected: number[ const selectableDatasetIds = datasets.filter(isSelectable).map(dataset => dataset.datasetId); return [ { - label: 0 && selected.length < selectableDatasetIds.length} onClick={() => onSelect(selectableDatasetIds.length === selected.length ? [] : selectableDatasetIds)}/>, sortable: false, diff --git a/src/libs/ajax/DAR.js b/src/libs/ajax/DAR.js index 274257bdb..4c65c0cf6 100644 --- a/src/libs/ajax/DAR.js +++ b/src/libs/ajax/DAR.js @@ -19,7 +19,8 @@ export const DAR = { //v2, v3 Draft DAR Update updateDarDraft: async (dar, referenceId) => { - await Metrics.captureEvent(eventList.dar, {'action': 'update'}); + // noinspection ES6MissingAwait + Metrics.captureEvent(eventList.dar, {'action': 'update'}); const url = DAAUtils.isEnabled() ? `${await getApiUrl()}/api/dar/v3/draft/${referenceId}` : `${await getApiUrl()}/api/dar/v2/draft/${referenceId}`; @@ -29,7 +30,8 @@ export const DAR = { //v2, v3 Draft DAR Creation postDarDraft: async (dar) => { - await Metrics.captureEvent(eventList.dar, {'action': 'draft'}); + // noinspection ES6MissingAwait + Metrics.captureEvent(eventList.dar, {'action': 'draft'}); const url = DAAUtils.isEnabled() ? `${await getApiUrl()}/api/dar/v3/draft` : `${await getApiUrl()}/api/dar/v2/draft`; @@ -46,7 +48,8 @@ export const DAR = { //v2, v3 DAR Creation postDar: async (dar) => { - await Metrics.captureEvent(eventList.dar, {'action': 'submit'}); + // noinspection ES6MissingAwait + Metrics.captureEvent(eventList.dar, {'action': 'submit'}); const filteredDar = fp.omit(['createDate', 'sortDate', 'data_access_request_id'])(dar); const url = DAAUtils.isEnabled() ? `${await getApiUrl()}/api/dar/v3` : diff --git a/src/pages/DatasetSearch.jsx b/src/pages/DatasetSearch.jsx index df86eeee6..780dad5d9 100644 --- a/src/pages/DatasetSearch.jsx +++ b/src/pages/DatasetSearch.jsx @@ -242,6 +242,15 @@ export const DatasetSearch = (props) => { icon: stanleyIcon, title: 'Stanley Center Data Library', }, + 'stanleycenter': { + query: { + 'match_phrase': { + 'study.description': 'Stanley Center' + } + }, + icon: stanleyIcon, + title: 'Stanley Center Data Library', + }, '/custom': { query: { 'bool': { @@ -275,9 +284,10 @@ export const DatasetSearch = (props) => { useEffect(() => { const init = async () => { + // noinspection ES6MissingAwait key === '/datalibrary' ? - await Metrics.captureEvent(eventList.dataLibrary) : - await Metrics.captureEvent(eventList.dataLibrary, {'brand': key.replaceAll('/', '').toLowerCase()}); + Metrics.captureEvent(eventList.dataLibrary) : + Metrics.captureEvent(eventList.dataLibrary, {'brand': key.replaceAll('/', '').toLowerCase()}); }; init(); }, [key]); diff --git a/src/pages/dar_application/DataAccessRequestApplication.jsx b/src/pages/dar_application/DataAccessRequestApplication.jsx index 1ac2d01e3..f88f360dd 100644 --- a/src/pages/dar_application/DataAccessRequestApplication.jsx +++ b/src/pages/dar_application/DataAccessRequestApplication.jsx @@ -375,7 +375,8 @@ const DataAccessRequestApplication = (props) => { if (isInvalidForm) { scrollToFormErrors(validation, eraCommonsIdValid, hasLibraryCard); } else { - await Metrics.captureEvent(eventList.dar, {'action': 'attest'}); + // noinspection ES6MissingAwait + Metrics.captureEvent(eventList.dar, {'action': 'attest'}); setIsAttested(true); addDucAddendumTab(); await goToDucAddendum(); diff --git a/src/pages/user_profile/UserProfile.jsx b/src/pages/user_profile/UserProfile.jsx index f247e1a9b..7135616b7 100644 --- a/src/pages/user_profile/UserProfile.jsx +++ b/src/pages/user_profile/UserProfile.jsx @@ -27,6 +27,7 @@ export default function UserProfile(props) { const [profile, setProfile] = useState({ profileName: '', email: undefined, + emailPreference: undefined, id: undefined }); @@ -54,6 +55,19 @@ export default function UserProfile(props) { } }; + const updateEmailPreference = (value) => { + const payload = { + emailPreference: value + }; + + User.updateSelf(payload).then((response) => { + setUserRoleStatuses(response, Storage); + Notifications.showSuccess({ text: 'Email preference updated successfully!' }); + }, () => { + Notifications.showError({ text: 'Some errors occurred, the user\'s email preference was not updated.' }); + }); + } + useEffect(() => { const init = async () => { @@ -64,6 +78,7 @@ export default function UserProfile(props) { setProfile({ profileName: user.displayName, email: user.email, + emailPreference: user.emailPreference, id: user.userId }); setName(user.displayName); @@ -179,7 +194,22 @@ export default function UserProfile(props) { defaultValue={profile.email} disabled={true} /> -
+
+

+ Send me email notifications +

+ updateEmailPreference(field.value)} + />