diff --git a/cypress/component/DataSearch/dataset_search_filters.spec.js b/cypress/component/DataSearch/dataset_search_filters.spec.js index a8bb21c17..d61ceac93 100644 --- a/cypress/component/DataSearch/dataset_search_filters.spec.js +++ b/cypress/component/DataSearch/dataset_search_filters.spec.js @@ -4,18 +4,20 @@ import { mount } from 'cypress/react'; import React from 'react'; import DatasetFilterList from '../../../src/components/data_search/DatasetFilterList'; -const duosUser = { - isSigningOfficial: false, -}; - describe('Data Library Filters', () => { - beforeEach(() => { - cy.initApplicationConfig(); - }); + // Intercept configuration calls + beforeEach(() => { + cy.initApplicationConfig(); + }); - it('Renders the data library filters', () => { - const props = { datasets: [], filters: [], filterHandler: () => {}, isFiltered: () => {}}; - mount(); - cy.get('div').should('contain', 'Filters'); - }); + it('Renders the data library filters', () => { + const props = { datasets: [], filterHandler: () => {}, isFiltered: () => {}}; + mount(); + cy.get('div').should('contain', 'Filters'); + cy.get('div').should('contain', 'Access Type'); + cy.get('div').should('contain', 'Data Use'); + cy.get('div').should('contain', 'Data Access Committee'); + cy.get('div').should('contain', 'Data Type'); + cy.get('div').should('contain', 'Participant Count'); + }); }); diff --git a/cypress/component/DataSearch/dataset_search_table.spec.js b/cypress/component/DataSearch/dataset_search_table.spec.js index 85c1e92c6..c10b27638 100644 --- a/cypress/component/DataSearch/dataset_search_table.spec.js +++ b/cypress/component/DataSearch/dataset_search_table.spec.js @@ -9,7 +9,9 @@ const datasets = [ datasetId: 123456, datasetIdentifier: `DUOS-123456`, datasetName: 'Some Dataset 1', + participantCount: 100, study: { + studyName: 'Some Study 1', studyId: 1, dataCustodianEmail: ['Some Data Custodian Email 1'], } @@ -23,9 +25,11 @@ const props = { describe('Dataset Search Table tests', () => { - describe('Data library with three datasets', () => { + describe('Data library with one dataset footer tests', () => { beforeEach(() => { + cy.initApplicationConfig(); cy.stub(TerraDataRepo, 'listSnapshotsByDatasetIds').returns({}); + cy.clock(); mount(); }); @@ -38,6 +42,66 @@ describe('Dataset Search Table tests', () => { cy.get('#header-checkbox').click(); cy.contains('1 dataset selected from 1 study'); }); + }); + + + describe('Data library filter by participant count tests', () => { + + beforeEach(() => { + cy.initApplicationConfig(); + cy.stub(TerraDataRepo, 'listSnapshotsByDatasetIds').returns({}); + cy.clock(); + }); + + function handler(request, searchText) { + if (JSON.stringify(request.body).includes(searchText)) { + request.reply(['filtered']); + } else { + request.reply([]); + } + } + + + it('When a participant count filter is applied the query is updated', () => { + cy.intercept( + {method: 'POST', url: '**/search/index'}, (req) => { + return handler(req, '{"range":{"participantCount":{"gte":null,"lte":50}}}'); + }).as('searchIndex'); + mount(); + // first clear the default value (100), without clearing first, type('50') would result in input of 10050 + const range = cy.get('#participantCountMax-range-input'); + range.clear(); + range.type('50'); + cy.tick(150); + // this api call should have had a request that contained the searchText + let count = 0; + cy.wait('@searchIndex').then((response) => { + expect(response.response.body[0]).to.equal('filtered'); + count++; + }); + cy.get('@searchIndex').then(() => { + expect(count).to.equal(1); + }); + + }); + + it('When an invalid participant count filter is applied the query represents the default value', () => { + cy.intercept({method: 'POST', url: '**/search/index'}, (req) => { + // when non-numeric input is entered, the default value (in this case, 100) is used + return handler(req, '{"range":{"participantCount":{"gte":100,"lte":null}}}'); + }).as('searchIndex'); + mount(); + cy.get('#participantCountMin-range-input').type('test'); + cy.tick(150); + let count = 0; + cy.wait('@searchIndex').then((response) => { + expect(response.response.body[0]).to.equal('filtered'); + count++; + }); + cy.get('@searchIndex').then(() => { + expect(count).to.equal(1); + }); + }); }); }); diff --git a/src/components/data_search/DatasetFilterList.jsx b/src/components/data_search/DatasetFilterList.jsx index 4b6825579..c8ff3d562 100644 --- a/src/components/data_search/DatasetFilterList.jsx +++ b/src/components/data_search/DatasetFilterList.jsx @@ -6,10 +6,10 @@ import ListItemButton from '@mui/material/ListItemButton'; import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemText from '@mui/material/ListItemText'; import Divider from '@mui/material/Divider'; -import { Button, Typography } from '@mui/material'; +import { Button, TextField, Typography } from '@mui/material'; import { Checkbox } from '@mui/material'; -import {flatten, uniq, compact, orderBy} from 'lodash'; -import {getAccessManagementSummary} from '../../types/model'; +import { flatten, uniq, compact, orderBy } from 'lodash'; +import { getAccessManagementSummary } from '../../types/model'; export const FilterItemHeader = (props) => { const { title, headerStyle = { fontFamily: 'Montserrat', fontWeight: '600', marginTop: '1em' } } = props; @@ -21,20 +21,20 @@ export const FilterItemHeader = (props) => { }; export const FilterItemList = (props) => { - const { category, datasets, filter, filterHandler, isFiltered, filterNameFn, filterDisplayFn } = props; + const { category, filter, filterHandler, isFiltered, filterNameFn, filterDisplayFn } = props; return ( { - filter.map((filter) => { - const filterName = filterNameFn(filter); + filter.map((filterOption) => { + const filterName = filterNameFn(filterOption); return ( - - filterHandler(event, datasets, category, filter)}> + + filterHandler(category, filterOption)}> - + - {filterDisplayFn ? filterDisplayFn(filter) : filterName} + {filterDisplayFn ? filterDisplayFn(filterOption) : filterName} @@ -45,14 +45,37 @@ export const FilterItemList = (props) => { ); }; +export const FilterItemRange = (props) => { + const { min, max, minCategory, maxCategory, filterHandler } = props; + const getValue = (val, defaultVal) => isNaN(Number(val)) ? defaultVal : Number(val); + return ( + + filterHandler(minCategory, getValue(event.target.value, min))}/> + - + filterHandler(maxCategory, getValue(event.target.value, max))} + /> + + ); +}; + export const DatasetFilterList = (props) => { - const { datasets, filters, filterHandler, isFiltered, onClear } = props; + const { datasets, filterHandler, isFiltered, onClear } = props; const accessManagementFilters = uniq(compact(datasets.map((dataset) => dataset.accessManagement))); const dataUseFilters = uniq(compact(flatten(datasets.map((dataset) => dataset.dataUse?.primary))).map((dataUse) => dataUse.code)); const dataTypeFilters = uniq(flatten(datasets.map((dataset) => dataset.study.dataTypes))); const dacFilters = orderBy(uniq(compact(datasets.map((dataset) => dataset.dac?.dacName))), (dac) => dac.toLowerCase(), 'asc'); - + const defaultValues = datasets.reduce((acc, dataset) => { + return { + max: Math.max(acc.max, dataset.participantCount ? dataset.participantCount : 0), + min: Math.min(acc.min, dataset.participantCount ? dataset.participantCount : Infinity) }; + }, {max: 0, min: Infinity}); return ( @@ -101,15 +124,24 @@ export const DatasetFilterList = (props) => { isFiltered={isFiltered} filterNameFn={(filter) => filter} /> - + filter} /> + + ); }; diff --git a/src/components/data_search/DatasetSearchTable.jsx b/src/components/data_search/DatasetSearchTable.jsx index 989e8dfc6..d6677887c 100644 --- a/src/components/data_search/DatasetSearchTable.jsx +++ b/src/components/data_search/DatasetSearchTable.jsx @@ -3,8 +3,8 @@ import Tabs from '@mui/material/Tabs'; import useOnMount from '@mui/utils/useOnMount'; import * as React from 'react'; import { Box, Button } from '@mui/material'; -import { useEffect, useRef, useState } from 'react'; -import { isEmpty } from 'lodash'; +import {useEffect, useRef, useState} from 'react'; +import { isArray, isEmpty } from 'lodash'; import { TerraDataRepo } from '../../libs/ajax/TerraDataRepo'; import { DatasetSearchTableDisplay } from './DatasetSearchTableDisplay'; import { datasetSearchTableTabs } from './DatasetSearchTableConstants'; @@ -37,7 +37,9 @@ const defaultFilters = { accessManagement: [], dataUse: [], dataType: [], - dac: [] + dac: [], + participantCountMin: null, + participantCountMax: null, }; export const DatasetSearchTable = (props) => { @@ -49,8 +51,12 @@ export const DatasetSearchTable = (props) => { const [selectedTable, setSelectedTable] = useState(datasetSearchTableTabs.study); const [searchTerm, setSearchTerm] = useState(''); - const isFiltered = (filter, category) => (filters[category]).indexOf(filter) > -1; - const numSelectedFilters = (filters) => Object.values(filters).reduce((sum, array) => sum + array.length, 0); + const isFilteredArray = (filter, category) => (filters[category]).indexOf(filter) > -1; + + const anyFiltersSelected = (filters) => + Object.values(filters).some(filter => { + return isArray(filter) ? filter.length > 0 : filter !== null; + }); const getExportableDatasets = async (datasets) => { // Note the dataset identifier is in each sub-table row. @@ -108,7 +114,7 @@ export const DatasetSearchTable = (props) => { } let filterQuery = {}; - if (numSelectedFilters(filters) > 0) { + if (anyFiltersSelected(filters)) { const filterTerms = []; filterTerms.push({ @@ -155,6 +161,15 @@ export const DatasetSearchTable = (props) => { } }); + filterTerms.push({ + 'range': { + 'participantCount': { + 'gte': filters.participantCountMin, + 'lte': filters.participantCountMax, + } + } + }); + if (filterTerms.length > 0) { filterQuery = [ { @@ -179,13 +194,19 @@ export const DatasetSearchTable = (props) => { }; }; - const filterHandler = (event, data, category, filter) => { - var newFilters = _.clone(filters); - if (!isFiltered(filter, category) && filter !== '') { - newFilters[category] = filters[category].concat(filter); + const filterHandler = (category, filter) => { + let newFilter; + if (isArray(filters[category])) { + if (!isFilteredArray(filter, category) && filter !== '') { + newFilter = filters[category].concat(filter); + } else { + newFilter = filters[category].filter((f) => f !== filter); + } } else { - newFilters[category] = filters[category].filter((f) => f !== filter); + newFilter = filter; } + const newFilters = _.clone(filters); + newFilters[category] = newFilter; setFilters(newFilters); }; @@ -280,7 +301,7 @@ export const DatasetSearchTable = (props) => { - setFilters(defaultFilters)}/> + setFilters(defaultFilters)}/> {(() => {