diff --git a/src/Routes.js b/src/Routes.js index 60b7a8490..6d3b6eaf1 100644 --- a/src/Routes.js +++ b/src/Routes.js @@ -13,7 +13,7 @@ import Home from './pages/Home'; import NotFound from './pages/NotFound'; import NIHICWebform from './pages/NIHicWebform'; import PrivacyPolicy from './pages/PrivacyPolicy'; -import ResearcherConsole from './pages/ResearcherConsole'; +import ResearcherConsole from './pages/researcher_console/ResearcherConsole'; import UserProfile from './pages/user_profile/UserProfile'; import RequestRole from './pages/user_profile/RequestRole'; import SigningOfficialResearchers from './pages/signing_official_console/SigningOfficialResearchers'; @@ -33,6 +33,7 @@ import AdminManageDarCollections from './pages/AdminManageDarCollections'; import {AdminEditUser} from './pages/AdminEditUser'; import ChairConsole from './pages/ChairConsole'; import MemberConsole from './pages/MemberConsole'; +import DatasetSubmissions from './pages/researcher_console/DatasetSubmissions'; import TermsOfService from './pages/TermsOfService'; import TermsOfServiceAcceptance from './pages/TermsOfServiceAcceptance'; import {HealthCheck} from './pages/HealthCheck'; @@ -87,6 +88,7 @@ const Routes = (props) => ( {checkEnv(envGroups.NON_STAGING) && } + diff --git a/src/components/DuosHeader.js b/src/components/DuosHeader.js index f9d0fa87e..cb3c73eb0 100644 --- a/src/components/DuosHeader.js +++ b/src/components/DuosHeader.js @@ -125,22 +125,14 @@ export const headerTabsConfig = [ ], isRendered: (user) => user.isMember }, - { - label: 'DS Console', - link: '/data_submission_form', - search: 'data_submission_form', - children: [ - { label: 'Datasets', link: '/data_submission_form' } - ], - isRendered: (user) => user.isDataSubmitter - }, { label: 'Researcher Console', link: '/dataset_catalog', search: 'dataset_catalog', children: [ { label: 'Data Catalog', link: '/dataset_catalog' }, - { label: 'DAR Requests', link: '/researcher_console' } + { label: 'DAR Requests', link: '/researcher_console' }, + { label: 'Submitted Datasets', link: '/dataset_submissions', isRenderedForUser: (user) => user?.isDataSubmitter } ], isRendered: (user) => user.isResearcher && !isOnlySigningOfficial(user) } @@ -321,7 +313,10 @@ const NavigationTabsComponent = (props) => { const isRendered = (!isFunction(tab.isRendered) || isNil(tab.isRendered())) ? true : tab.isRendered(); - return isRendered ? h(Tab, { + const isRenderedForUser = (!isFunction(tab.isRenderedForUser) || isNil(tab.isRenderedForUser(currentUser))) ? + true : + tab.isRenderedForUser(currentUser); + return (isRendered && isRenderedForUser) ? h(Tab, { key: `${tab.link}_${tabIndex}`, label: tab.label, style: selectedSubTab === tabIndex ? styles.subTabActive : styles.subTab, diff --git a/src/components/sortable_table/SortableTable.js b/src/components/sortable_table/SortableTable.js index e3792d8b2..f68ba6316 100644 --- a/src/components/sortable_table/SortableTable.js +++ b/src/components/sortable_table/SortableTable.js @@ -39,7 +39,7 @@ Step 3: Pass both arrays into the headCells and rows props */ -import React from 'react'; +import React, {useState, useMemo} from 'react'; import Box from '@mui/material/Box'; import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; @@ -84,14 +84,16 @@ export default function SortableTable(props) { const { rows, - headCells + headCells, + defaultSort = 'darCode', + cellAlignment = 'center' } = props; - const [order, setOrder] = React.useState('asc'); - const [orderBy, setOrderBy] = React.useState('darCode'); - const [selected, setSelected] = React.useState([]); - const [page, setPage] = React.useState(0); - const [rowsPerPage, setRowsPerPage] = React.useState(10); + const [order, setOrder] = useState('asc'); + const [orderBy, setOrderBy] = useState(defaultSort); + const [selected, setSelected] = useState([]); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); const handleRequestSort = (event, property) => { const isAsc = orderBy === property && order === 'asc'; @@ -135,7 +137,7 @@ export default function SortableTable(props) { const emptyRows = page > 0 ? Math.max(0, (1 + page) * rowsPerPage - rows.length) : 0; - const visibleRows = React.useMemo( + const visibleRows = useMemo( () => stableSort(rows, getComparator(order, orderBy)).slice( page * rowsPerPage, @@ -187,7 +189,7 @@ export default function SortableTable(props) { id={labelId} scope='row' padding='none' - align='center'> + align={cellAlignment}> {row[category]} ))} diff --git a/src/libs/utils.js b/src/libs/utils.js index a8815583f..ad566635c 100644 --- a/src/libs/utils.js +++ b/src/libs/utils.js @@ -1,11 +1,11 @@ import Noty from 'noty'; import 'noty/lib/noty.css'; import 'noty/lib/themes/bootstrap-v3.css'; -import {map as lodashMap, forEach as lodashForEach, isArray} from 'lodash'; -import { DAR } from './ajax'; -import {Theme } from './theme'; -import { each, flatten, flow, forEach as lodashFPForEach, get, getOr, uniq, find, first, map, isEmpty, filter, cloneDeep, isNil, toLower, includes, every, capitalize } from 'lodash/fp'; -import { headerTabsConfig } from '../components/DuosHeader'; +import {forEach as lodashForEach, isArray, map as lodashMap} from 'lodash'; +import {DAR} from './ajax'; +import {Theme} from './theme'; +import {capitalize, cloneDeep, concat, each, every, filter, find, first, flatten, flow, forEach as lodashFPForEach, get, getOr, includes, isEmpty, isNil, join, map, toLower, uniq} from 'lodash/fp'; +import {headerTabsConfig} from '../components/DuosHeader'; export const UserProperties = { SUGGESTED_SIGNING_OFFICIAL: 'suggestedSigningOfficial', @@ -16,7 +16,7 @@ export const UserProperties = { ///////DAR Collection Utils/////////////////////////////////////////////////////////////////////////////////// export const isCollectionCanceled = (collection) => { - const { dars } = collection; + const {dars} = collection; return every((dar) => toLower(dar.data.status) === 'canceled')(dars); }; @@ -28,15 +28,15 @@ export const darCollectionUtils = { determineCollectionStatus: (collection, relevantDatasets) => { const electionStatusCount = {}; let output; - if(!isEmpty(collection.dars)) { + if (!isEmpty(collection.dars)) { const targetElections = flow([ map((dar) => { - const { elections } = dar; + const {elections} = dar; //election is empty => no elections made for dar //need to figure out if dar is relevant, can obtain datasetId from dar.data //see if its relevant, if it is, add 1 to submitted on hash //return empty array at the end - if(isEmpty(elections)) { + if (isEmpty(elections)) { // Dataset IDs should be on the DAR, but if not, pull from the dar.data const datasetIds = isNil(dar.datasetIds) ? dar.data.datasetIds : dar.datasetIds; lodashFPForEach((datasetId) => { @@ -53,7 +53,7 @@ export const darCollectionUtils = { //only Data Access elections impact the status of the collection //NOTE: Admin does not have relevantIds, DAC roles do const electionArr = filter(election => toLower(election.electionType) === 'dataaccess')(Object.values(elections)); - if(isNil(relevantDatasets)) { + if (isNil(relevantDatasets)) { return electionArr; } else { const relevantIds = map(dataset => dataset.dataSetId)(relevantDatasets); @@ -64,10 +64,10 @@ export const darCollectionUtils = { flatten ])(collection.dars); - if(isNil(relevantDatasets)) { + if (isNil(relevantDatasets)) { each(election => { const {status} = election; - if(isNil(electionStatusCount[status])) { + if (isNil(electionStatusCount[status])) { electionStatusCount[status] = 0; } electionStatusCount[status]++; @@ -85,7 +85,7 @@ export const darCollectionUtils = { ///////DAR Collection Utils END///////////////////////////////////////////////////////////////////////////////// export const goToPage = (value, pageCount, setCurrentPage) => { - if(value >= 1 && value <= pageCount) { + if (value >= 1 && value <= pageCount) { setCurrentPage(value); } }; @@ -93,7 +93,7 @@ export const goToPage = (value, pageCount, setCurrentPage) => { export const findPropertyValue = (propName, researcher) => { const prop = isNil(researcher.researcherProperties) ? null - : find({ propertyKey: propName })(researcher.researcherProperties); + : find({propertyKey: propName})(researcher.researcherProperties); return isNil(prop) ? '' : prop.propertyValue; }; @@ -118,7 +118,7 @@ export const applyHoverEffects = (e, style) => { //currently, dars contain a list of datasets (any length) and a list of length 1 of a datasetId //go through the list of datasets and get the name of the dataset whose id is in the datasetId list export const getNameOfDatasetForThisDAR = (datasets, datasetId) => { - const data = !isNil(datasetId) && !isEmpty(datasetId) ? find({'value' : first(datasetId).toString()})(datasets) : null; + const data = !isNil(datasetId) && !isEmpty(datasetId) ? find({'value': first(datasetId).toString()})(datasets) : null; return isNil(data) ? '- -' : getDatasetNames([data]); }; @@ -127,7 +127,9 @@ export const formatDate = (dateval) => { return '---'; } - if(toLower(dateval) === 'unsubmitted') {return dateval;} + if (toLower(dateval) === 'unsubmitted') { + return dateval; + } let dateFormat = new Date(dateval); let year = dateFormat.getFullYear(); @@ -161,16 +163,18 @@ export const USER_ROLES = { }; export const getDatasetNames = (datasets) => { - if(!datasets){return '';} + if (!datasets) { + return ''; + } const datasetNames = datasets.map((dataset) => { - return ((dataset.label) ? dataset.label : dataset.name); + return ((dataset.label) ? dataset.label : dataset.name); }); return datasetNames.join('\n'); }; //helper function to generate keys for rendered elements; splits on commas and whitespace export const convertLabelToKey = (label = '') => { - return label.split(/[\s,]+/) .join('-'); + return label.split(/[\s,]+/).join('-'); }; export const setUserRoleStatuses = (user, Storage) => { @@ -212,7 +216,7 @@ export const Navigation = { export const download = (fileName, text) => { const break_line = '\r\n \r\n'; text = break_line + text; - let blob = new Blob([text], { type: 'text/plain' }); + let blob = new Blob([text], {type: 'text/plain'}); const url = window.URL.createObjectURL(blob); let a = document.createElement('a'); a.href = url; @@ -279,9 +283,9 @@ export const Notifications = { */ export const PromiseSerial = funcs => funcs.reduce((promise, func) => - promise.then(result => - func().then(Array.prototype.concat.bind(result))), - Promise.resolve([])); + promise.then(result => + func().then(Array.prototype.concat.bind(result))), + Promise.resolve([])); ////////////////////////////////// //DAR CONSOLES UTILITY FUNCTIONS// @@ -300,7 +304,7 @@ export const outputCommaSeperatedElectionStatuses = (elections) => { export const getElectionDate = (election) => { let formattedString = '- -'; - if(election) { + if (election) { //NOTE: some elections have a createDate attribute but not a lastUpdate attributes const targetDate = election.lastUpdate || election.createDate; formattedString = formatDate(targetDate); @@ -308,7 +312,7 @@ export const getElectionDate = (election) => { return formattedString; }; -export const wasVoteSubmitted =(vote) => { +export const wasVoteSubmitted = (vote) => { //NOTE: as mentioned elsewhere, legacy code has resulted in multiple sources for timestamps //current code will always provide lastUpdate const targetDate = vote.lastUpdate || vote.createDate || vote.updateDate || vote.lastUpdateDate; @@ -323,20 +327,20 @@ export const wasFinalVoteTrue = (voteData) => { export const processElectionStatus = (election, votes, showVotes) => { let output; - const electionStatus = !isNil(get('status')(election)) ? toLower(election.status) : null; + const electionStatus = !isNil(get('status')(election)) ? toLower(election.status) : null; if (isNil(electionStatus)) { output = 'Unreviewed'; - } else if(electionStatus === 'open') { + } else if (electionStatus === 'open') { //Null check since react doesn't necessarily perform prop updates immediately - if(!isEmpty(votes) && !isNil(election)) { + if (!isEmpty(votes) && !isNil(election)) { const dacVotes = filter((vote) => toLower(vote.type) === 'dac' && vote.electionId === election.electionId)(votes); const completedVotes = (filter(wasVoteSubmitted)(dacVotes)).length; const outputSuffix = `(${completedVotes} / ${dacVotes.length} votes)`; output = `Open${showVotes ? outputSuffix : ''}`; } - //some elections have electionStatus === Final, others have electionStatus === Closed - //both are, in this step of the process, technically referring to a closed election - //therefore both values must be checked for + //some elections have electionStatus === Final, others have electionStatus === Closed + //both are, in this step of the process, technically referring to a closed election + //therefore both values must be checked for } else if (electionStatus === 'final' || electionStatus === 'closed') { const finalVote = find(wasFinalVoteTrue)(votes); output = finalVote ? 'Approved' : 'Denied'; @@ -364,7 +368,7 @@ export const calcVisibleWindow = (currentPage, tableSize, filteredList) => { export const getSearchFilterFunctions = () => { return { dar: (term, targetList) => filter(electionData => { - const { election, dac, votes} = electionData; + const {election, dac, votes} = electionData; const dar = electionData.dar ? electionData.dar.data : undefined; const targetDarAttrs = !isNil(dar) ? JSON.stringify([toLower(dar.projectTitle), toLower(dar.darCode), toLower(getNameOfDatasetForThisDAR(dar.datasets, dar.datasetIds))]) : []; const targetDacAttrs = !isNil(dac) ? JSON.stringify([toLower(dac.name)]) : []; @@ -372,7 +376,7 @@ export const getSearchFilterFunctions = () => { return includes(term, targetDarAttrs) || includes(term, targetDacAttrs) || includes(term, targetElectionAttrs); }, targetList), libraryCard: (term, targetList) => filter(libraryCard => { - const { userName, institution, createDate, updateDate, eraCommonsId, userEmail} = libraryCard; + const {userName, institution, createDate, updateDate, eraCommonsId, userEmail} = libraryCard; const institutionName = institution.name; return includes(term, toLower(userName)) || includes(term, toLower(institutionName)) || @@ -382,7 +386,7 @@ export const getSearchFilterFunctions = () => { includes(term, toLower(eraCommonsId)); }, targetList), signingOfficialResearchers: (term, targetList) => filter(researcher => { - const { displayName, eraCommonsId, email } = researcher; + const {displayName, eraCommonsId, email} = researcher; const roles = researcher.roles || []; const baseAttributes = [displayName, eraCommonsId, email]; const includesRoles = roles.reduce((memo, current) => { @@ -443,6 +447,7 @@ export const getSearchFilterFunctions = () => { * pre-populated with data use codes and translations */ const loweredTerm = toLower(term); + const name = dataset.name; const alias = dataset.alias; const identifier = dataset.datasetIdentifier; const allPropValues = dataset.properties.map((p) => p.propertyValue).join(''); @@ -453,10 +458,44 @@ export const getSearchFilterFunctions = () => { : 'rejected' : 'yes no'; return includes(loweredTerm, toLower(alias)) || - includes(loweredTerm, toLower(identifier)) || - includes(loweredTerm, toLower(allPropValues)) || - includes(loweredTerm, toLower(dataset.codeList)) || - includes(loweredTerm, toLower(status)); + includes(loweredTerm, toLower(name)) || + includes(loweredTerm, toLower(identifier)) || + includes(loweredTerm, toLower(allPropValues)) || + includes(loweredTerm, toLower(dataset.codeList)) || + includes(loweredTerm, toLower(status)); + }, targetList), + datasetTerms: (term, targetList) => filter(datasetTerm => { + /** + * This filter function is intended for Dataset Index Terms + */ + const loweredTerm = toLower(term); + // Approval status + const status = !isNil(datasetTerm.dacApproval) + ? datasetTerm.dacApproval + ? 'accepted' + : 'rejected' + : 'pending'; + const primaryCodes = datasetTerm.dataUse?.primary.map(du => du.code); + const secondaryCodes = datasetTerm.dataUse?.secondary.map(du => du.code); + const codes = join(', ')(concat(primaryCodes)(secondaryCodes)); + const dataTypes = join(', ')(datasetTerm.study?.dataTypes); + const custodians = join(', ')(datasetTerm.study?.dataCustodianEmail); + return includes(loweredTerm, toLower(datasetTerm.datasetName)) || + includes(loweredTerm, toLower(datasetTerm.datasetIdentifier)) || + includes(loweredTerm, toLower(datasetTerm.dacName)) || + includes(loweredTerm, toLower(datasetTerm.dataLocation)) || + includes(loweredTerm, toLower(codes)) || + includes(loweredTerm, toLower(datasetTerm.createUserDisplayName)) || + includes(loweredTerm, toLower(datasetTerm.url)) || + includes(loweredTerm, toLower(datasetTerm.study?.description)) || + includes(loweredTerm, toLower(datasetTerm.study?.dataSubmitterEmail)) || + includes(loweredTerm, toLower(dataTypes)) || + includes(loweredTerm, toLower(custodians)) || + includes(loweredTerm, toLower(datasetTerm.study?.phenotype)) || + includes(loweredTerm, toLower(datasetTerm.study?.piName)) || + includes(loweredTerm, toLower(datasetTerm.study?.species)) || + includes(loweredTerm, toLower(datasetTerm.study?.studyName)) || + includes(loweredTerm, toLower(status)); }, targetList), }; }; @@ -466,13 +505,13 @@ export const tableSearchHandler = (list, setFilteredList, setCurrentPage, modelN return (searchTerms) => { const rawSearchTerms = getOr(searchTerms, 'current.value', searchTerms); const searchTermValues = toLower(rawSearchTerms).split(/\s|,/); - if(isEmpty(searchTermValues)) { + if (isEmpty(searchTermValues)) { setFilteredList(list); } else { let newFilteredList = cloneDeep(list); lodashFPForEach((splitTerm) => { const term = splitTerm.trim(); - if(!isEmpty(term)) { + if (!isEmpty(term)) { const filterFn = filterFnMap[modelName]; newFilteredList = filterFn(term, newFilteredList); } @@ -487,7 +526,7 @@ export const searchOntologies = (query, callback) => { let options = []; DAR.getAutoCompleteOT(query).then( items => { - options = items.map(function(item) { + options = items.map(function (item) { return { key: item.id, value: item.id, @@ -500,7 +539,7 @@ export const searchOntologies = (query, callback) => { }; export const setStyle = (disabled, baseStyle, targetColorAttribute) => { - let appliedStyle = disabled ? {[targetColorAttribute] : Theme.palette.disabled} : {}; + let appliedStyle = disabled ? {[targetColorAttribute]: Theme.palette.disabled} : {}; try { return Object.assign(baseStyle, appliedStyle); } catch (e) { @@ -510,12 +549,12 @@ export const setStyle = (disabled, baseStyle, targetColorAttribute) => { export const setDivAttributes = (disabled, onClick, style, dataTip, onMouseEnter, onMouseLeave, key) => { let attributes; - if(!disabled) { + if (!disabled) { attributes = {onClick, onMouseEnter, onMouseLeave, style, 'data-tip': dataTip, key, id: key}; } else { attributes = {style, disabled, 'data-tip': dataTip, key}; } - if(!isEmpty(dataTip)) { + if (!isEmpty(dataTip)) { attributes['data-tip'] = dataTip; } return attributes; @@ -524,12 +563,11 @@ export const setDivAttributes = (disabled, onClick, style, dataTip, onMouseEnter //each item in the list is an array of metadata representing a single table row //the metadata for each cell needs a data (exactly what is displayed in the table) //or value (string or number alternative) property which determines sorting -export const sortVisibleTable = ({ list = [], sort }) => { +export const sortVisibleTable = ({list = [], sort}) => { // Sort: { dir, colIndex } if (!sort || sort.colIndex === undefined) { return list; - } - else { + } else { return list.sort((a, b) => { const aVal = a[sort.colIndex].value || a[sort.colIndex].data; const bVal = b[sort.colIndex].value || b[sort.colIndex].data; @@ -539,7 +577,7 @@ export const sortVisibleTable = ({ list = [], sort }) => { if (aVal === null || bVal === null) { return (aVal > bVal ? -1 : 1) * sort.dir; } else { - return (aVal.localeCompare(bVal, 'en', { sensitivity: 'base', numeric: true }) * sort.dir); + return (aVal.localeCompare(bVal, 'en', {sensitivity: 'base', numeric: true}) * sort.dir); } } }); @@ -553,7 +591,7 @@ export const recalculateVisibleTable = async ({ try { // Sort data before applying paging if (sort) { - filteredList = sortVisibleTable({ list: filteredList, sort }); + filteredList = sortVisibleTable({list: filteredList, sort}); } // Set paging variables and truncate the list @@ -568,13 +606,13 @@ export const recalculateVisibleTable = async ({ ); setVisibleList(visibleList); } catch (error) { - Notifications.showError({ text: 'Error updating table' }); + Notifications.showError({text: 'Error updating table'}); } }; export const searchOnFilteredList = (searchTerms, originalList, filterFn, setFilteredList) => { let searchList = (!isNil(originalList) ? [...originalList] : []); - if(!isEmpty(searchTerms)) { + if (!isEmpty(searchTerms)) { const terms = searchTerms.split(' '); lodashFPForEach((term => searchList = filterFn(term, searchList)))(terms); } @@ -595,6 +633,6 @@ export const getBooleanFromEventHtmlDataValue = (e) => { export const hasDataSubmitterRole = (user) => { const roles = get('roles')(user); - const dsRole = find({'roleId':8})(roles); + const dsRole = find({'roleId': 8})(roles); return !isNil(dsRole); }; diff --git a/src/pages/researcher_console/DatasetSubmissions.jsx b/src/pages/researcher_console/DatasetSubmissions.jsx new file mode 100644 index 000000000..29c140f91 --- /dev/null +++ b/src/pages/researcher_console/DatasetSubmissions.jsx @@ -0,0 +1,149 @@ +import React, {useCallback, useEffect, useRef, useState} from 'react'; +import {Styles, Theme} from '../../libs/theme'; +import lockIcon from '../../images/lock-icon.png'; +import {Link} from 'react-router-dom'; +import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'; +import {getSearchFilterFunctions, Notifications, searchOnFilteredList} from '../../libs/utils'; +import SearchBar from '../../components/SearchBar'; +import {DataSet} from '../../libs/ajax'; +import DatasetSubmissionsTable from './DatasetSubmissionsTable'; +import {Storage} from '../../libs/storage'; +import styles from './DatasetTerms.module.css'; + +export default function DatasetSubmissions() { + + const [terms, setTerms] = useState([]); + const [filteredTerms, setFilteredTerms] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [currentUser, setCurrentUser] = useState({}); + const searchRef = useRef(''); + + useEffect(() => { + const init = async () => { + const user = Storage.getCurrentUser(); + setCurrentUser(user); + const query = { + 'from': 0, + 'size': 10000, + 'query': { + 'bool': { + 'must': [ + { + 'match': { + '_type': 'dataset' + } + }, + { + 'bool': { + 'should': [ + { + 'term': { + 'createUserId': { + 'value': user.userId + } + } + }, + { + 'term': { + 'study.dataSubmitterId': { + 'value': user.userId + } + } + }, + { + 'term': { + 'study.dataCustodianEmail': { + 'value': user.email + } + } + } + ] + } + } + ] + } + } + }; + setIsLoading(true); + try { + const queryTerms = await DataSet.searchDatasetIndex(query); + setTerms(queryTerms); + setFilteredTerms(queryTerms); + } catch (error) { + Notifications.showError({text: 'Error initializing datasets table'}); + } + setIsLoading(false); + }; + init(); + }, []); + + const handleSearchChange = useCallback((searchTerms) => searchOnFilteredList( + searchTerms, + terms, + getSearchFilterFunctions().datasetTerms, + setFilteredTerms + ), [terms]); + + const addDatasetButtonStyle = { + color: Theme.palette.link, + backgroundColor: 'white', + border: '1px solid', + borderColor: Theme.palette.link, + borderRadius: 4, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontSize: '1.45rem', + padding: '3%', + cursor: 'default', + textTransform: 'uppercase', + fontWeight: 600, + marginRight: 5, + marginTop: 10 + }; + + const addDatasetButton = (currentUser.libraryCards?.length > 0) + ? + : ; + + return ( +
+
+
+
+ {'Lock +
+
+
+ My Submitted Datasets +
+
+ View the status of datasets registered in DUOS +
+
{addDatasetButton}
+
+
+
+ +
+
+
+ +
+
+ ); +} \ No newline at end of file diff --git a/src/pages/researcher_console/DatasetSubmissionsTable.jsx b/src/pages/researcher_console/DatasetSubmissionsTable.jsx new file mode 100644 index 000000000..175d91f35 --- /dev/null +++ b/src/pages/researcher_console/DatasetSubmissionsTable.jsx @@ -0,0 +1,123 @@ +import * as React from 'react'; +import {useCallback, useEffect, useState} from 'react'; +import {Notifications} from '../../libs/utils'; +import loadingIndicator from '../../images/loading-indicator.svg'; +import SortableTable from '../../components/sortable_table/SortableTable'; +import {concat, isNil, join} from 'lodash/fp'; +import Button from '@mui/material/Button'; +import styles from './DatasetTerms.module.css'; + + +export default function DatasetSubmissionsTable(props) { + + const spinner =
+ {'Loading'}/ +
; + + const columns = [ + { + id: 'datasetIdentifier', + numeric: false, + disablePadding: false, + label: 'DUOS ID', + }, + { + id: 'datasetName', + numeric: false, + disablePadding: false, + label: 'Dataset Name', + }, + { + id: 'datasetSubmitter', + numeric: false, + disablePadding: false, + label: 'Dataset Submitter', + }, + { + id: 'datasetCustodians', + numeric: false, + disablePadding: false, + label: 'Dataset Custodians', + }, + { + id: 'dac', + numeric: false, + disablePadding: false, + label: 'DAC', + }, + { + id: 'dataUse', + numeric: false, + disablePadding: false, + label: 'Data Use', + }, + { + id: 'status', + numeric: false, + disablePadding: false, + label: 'Status', + }, + { + id: 'actions', + numeric: false, + disablePadding: false, + label: 'Actions', + } + ]; + + const [terms, setTerms] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [rows, setRows] = useState([]); + + // Datasets can be filtered from the parent component and redrawn frequently. + const redrawRows = useCallback(() => { + const rows = terms.map((term) => { + const status = isNil(term.dacApproval) ? 'Pending' : (term.dacApproval ? 'Accepted' : 'Rejected'); + const primaryCodes = term.dataUse?.primary?.map(du => du.code); + const secondaryCodes = term.dataUse?.secondary?.map(du => du.code); + const editLink = (term.study?.studyId) ? '/study_update/' + term.study.studyId : '/dataset_update/' + term.datasetId; + const editButton = (status === 'Accepted') ? +
: +
+ +
; + const custodians = join(', ')(term.study?.dataCustodianEmail); + return { + datasetIdentifier: term.datasetIdentifier, + datasetName: term.datasetName, + dataSubmitter: term.createUserDisplayName, + datasetCustodians: custodians, + dac: term.dacName, + dataUse: join(', ')(concat(primaryCodes)(secondaryCodes)), + status: status, + actions: editButton + }; + }); + setRows(rows); + }, [terms]); + + useEffect(() => { + const init = async () => { + try { + setTerms(props.terms); + setIsLoading(props.isLoading); + redrawRows(); + } catch (error) { + Notifications.showError({text: 'Error: Unable to retrieve datasets from server'}); + } + }; + init(); + }, [props, redrawRows]); + + const sortableTable = ; + + return isLoading ? spinner : sortableTable; +} \ No newline at end of file diff --git a/src/pages/researcher_console/DatasetTerms.module.css b/src/pages/researcher_console/DatasetTerms.module.css new file mode 100644 index 000000000..8538460b9 --- /dev/null +++ b/src/pages/researcher_console/DatasetTerms.module.css @@ -0,0 +1,28 @@ +.submitted-datasets-header { + display: flex; + justify-content: space-between; + width: 112%; + margin-left: -6%; + padding: 0 2.5%; +} + +.term-table-container { + width: 100%; + margin-top: 10px; + margin-left: 22px; +} + +.search-box-container { + width: 50%; + display: flex; + justify-content: flex-end; +} + +.action-button { + font-size: 14px; + border: 1px solid #0948B7; + border-radius: 4px; + height: 25px; + cursor: pointer; + color: #0948B7; +} \ No newline at end of file diff --git a/src/pages/ResearcherConsole.js b/src/pages/researcher_console/ResearcherConsole.js similarity index 71% rename from src/pages/ResearcherConsole.js rename to src/pages/researcher_console/ResearcherConsole.js index f25da0485..e35f62d33 100644 --- a/src/pages/ResearcherConsole.js +++ b/src/pages/researcher_console/ResearcherConsole.js @@ -1,16 +1,18 @@ -import { useState, useEffect, useRef, useCallback } from 'react'; -import { div, h, img, a } from 'react-hyperscript-helpers'; +import {useCallback, useEffect, useRef, useState} from 'react'; +import {a, div, h, img} from 'react-hyperscript-helpers'; import {cloneDeep, findIndex} from 'lodash/fp'; -import { Styles } from '../libs/theme'; -import { Collections, DAR } from '../libs/ajax'; -import { DarCollectionTableColumnOptions, DarCollectionTable } from '../components/dar_collection_table/DarCollectionTable'; -import accessIcon from '../images/lock-icon.png'; -import {Notifications, searchOnFilteredList, getSearchFilterFunctions } from '../libs/utils'; -import SearchBar from '../components/SearchBar'; -import { consoleTypes } from '../components/dar_collection_table/DarCollectionTableCellData'; -import { USER_ROLES } from '../libs/utils'; -import BroadLibraryCardAgreementLink from '../assets/Library_Card_Agreement_2023_ApplicationVersion.pdf'; -import NhgriLibraryCardAgreementLink from '../assets/NIH_Library_Card_Agreement_11_17_22_version.pdf'; +import {Styles} from '../../libs/theme'; +import {Collections, DAR} from '../../libs/ajax'; +import { + DarCollectionTable, + DarCollectionTableColumnOptions +} from '../../components/dar_collection_table/DarCollectionTable'; +import accessIcon from '../../images/lock-icon.png'; +import {getSearchFilterFunctions, Notifications, searchOnFilteredList, USER_ROLES} from '../../libs/utils'; +import SearchBar from '../../components/SearchBar'; +import {consoleTypes} from '../../components/dar_collection_table/DarCollectionTableCellData'; +import BroadLibraryCardAgreementLink from '../../assets/Library_Card_Agreement_2023_ApplicationVersion.pdf'; +import NhgriLibraryCardAgreementLink from '../../assets/NIH_Library_Card_Agreement_11_17_22_version.pdf'; const filterFn = getSearchFilterFunctions().darCollections; @@ -53,9 +55,12 @@ export default function ResearcherConsole() { //cancel collection function, passed to collections table to be used in buttons const cancelCollection = async (darCollection) => { try { - const { darCollectionId, darCode } = darCollection; + const {darCollectionId, darCode} = darCollection; await Collections.cancelCollection(darCollectionId); - const updatedCollection = await Collections.getCollectionSummaryByRoleNameAndId({roleName: USER_ROLES.researcher, id: darCollectionId}); + const updatedCollection = await Collections.getCollectionSummaryByRoleNameAndId({ + roleName: USER_ROLES.researcher, + id: darCollectionId + }); const targetIndex = researcherCollections.findIndex((collection) => collection.darCollectionId === darCollectionId); if (targetIndex < 0) { @@ -75,7 +80,7 @@ export default function ResearcherConsole() { //revise collection function, passed to collections table to be used in buttons const reviseCollection = async (darCollection) => { try { - const { darCollectionId, darCode } = darCollection; + const {darCollectionId, darCode} = darCollection; const draftCollection = await Collections.reviseCollection(darCollectionId); const targetIndex = researcherCollections.findIndex((collection) => collection.darCollectionId === darCollectionId); @@ -96,7 +101,7 @@ export default function ResearcherConsole() { //Draft delete, by referenceIds - const deleteDraftById = async ({ referenceId }) => { + const deleteDraftById = async ({referenceId}) => { const collectionsClone = cloneDeep(researcherCollections); await DAR.deleteDar(referenceId); const targetIndex = findIndex((draft) => { @@ -111,11 +116,11 @@ export default function ResearcherConsole() { }; //Draft delete, passed down to draft table to be used with delete button - const deleteDraft = async ({ referenceIds, darCode }) => { + const deleteDraft = async ({referenceIds, darCode}) => { try { - const targetIndex = deleteDraftById({ referenceId: referenceIds[0] }); + const targetIndex = deleteDraftById({referenceId: referenceIds[0]}); if (targetIndex === -1) { - Notifications.showError({ text: 'Error processing delete request' }); + Notifications.showError({text: 'Error processing delete request'}); } else { Notifications.showSuccess({text: `Deleted Data Access Request Draft ${darCode}`}); } @@ -127,20 +132,20 @@ export default function ResearcherConsole() { }; - return div({ style: Styles.PAGE }, [ - div({ style: { display: 'flex', justifyContent: 'space-between', margin: '0px -3%' } }, [ + return div({style: Styles.PAGE}, [ + div({style: {display: 'flex', justifyContent: 'space-between', margin: '0px -3%'}}, [ div( - { className: 'left-header-section', style: Styles.LEFT_HEADER_SECTION }, + {className: 'left-header-section', style: Styles.LEFT_HEADER_SECTION}, [ - div({ style: Styles.ICON_CONTAINER }, [ + div({style: Styles.ICON_CONTAINER}, [ img({ id: 'access-icon', src: accessIcon, style: Styles.HEADER_IMG, }), ]), - div({ style: Styles.HEADER_CONTAINER }, [ - div({ style: Styles.TITLE }, ['My Data Access Requests']), + div({style: Styles.HEADER_CONTAINER}, [ + div({style: Styles.TITLE}, ['My Data Access Requests']), div( { style: Object.assign({}, Styles.MEDIUM_DESCRIPTION, { @@ -155,14 +160,20 @@ export default function ResearcherConsole() { fontSize: '18px', }), }, - ['By submitting a DAR in DUOS you agree to the ', a({target: '_blank', href: BroadLibraryCardAgreementLink}, ['Broad']), ' and ', a({target: '_blank', href: NhgriLibraryCardAgreementLink}, ['NHGRI']), ' Library Card Agreements.'] + ['By submitting a DAR in DUOS you agree to the ', a({ + target: '_blank', + href: BroadLibraryCardAgreementLink + }, ['Broad']), ' and ', a({ + target: '_blank', + href: NhgriLibraryCardAgreementLink + }, ['NHGRI']), ' Library Card Agreements.'] ), ]), ] ), - h(SearchBar, { handleSearchChange, searchRef }), + h(SearchBar, {handleSearchChange, searchRef}), ]), - div({ className: 'table-container' }, [ + div({className: 'table-container'}, [ h(DarCollectionTable, { collections: filteredList, columns: [