diff --git a/docs/portal_config.md b/docs/portal_config.md index 88178aeaa..2d9246762 100644 --- a/docs/portal_config.md +++ b/docs/portal_config.md @@ -420,7 +420,16 @@ Below is an example, with inline comments describing what each JSON block config }, "search": { "searchBar": { - "enabled": true + "enabled": true, + "inputSubtitle": "Search Bar", // optional, subtitle of search bar + "placeholder": "Search studies by keyword", // optional, placeholder text of search input + "searchableTextFields": ["study", "age", "publication"] // optional, list of properties in data to make searchable + // if not present, only fields visible in the table will be searchable + }, + "tagSearchDropdown": { // optional, config section for searchable tags + "enabled": true, + "collapseOnDefault": false, // optional, whether the searchable tag panel is collapsed when loading, default value is "true" + "collapsibleButtonText": "Study Characteristics" // optional, display text for the searchable tag panel collapse control button, default value is "Tag Panel" } }, "advSearchFilters": { diff --git a/src/Discovery/Discovery.css b/src/Discovery/Discovery.css index b97652886..5ed35d3d0 100644 --- a/src/Discovery/Discovery.css +++ b/src/Discovery/Discovery.css @@ -97,6 +97,11 @@ height: 90%; } +.discovery-header__dropdown-tags-container { + flex: 3 1 60%; + height: 90%; +} + .discovery-header__tags-header { padding-left: 50px; cursor: default; @@ -142,7 +147,7 @@ .discovery-tag { cursor: pointer; line-height: 18px; - font-size: 10px; + font-size: 12px; box-sizing: border-box; border-radius: 5px; border-width: 2px; @@ -183,7 +188,7 @@ overflow-y: auto; } -.discovery-search-container { +.discovery-search-container__standalone { margin-bottom: 10px; } @@ -191,7 +196,7 @@ border-color: var(--g3-color__base-blue); border-width: 1px; border-radius: 7px; - max-width: 500px; + max-width: 800px; } .discovery-input-subtitle { @@ -339,6 +344,38 @@ border-color: unset; } +.discovery-header__dropdown-tags-control-button { + color: var(--g3-color__base-blue); + border-color: var(--g3-color__base-blue); + min-width: 150px; + margin-left: 8px; + height: 40px; + border-width: 1px; + border-radius: 7px; +} + +.discovery-header__dropdown-tags-control-panel { + width: 100%; + padding-right: 16px; + padding-left: 16px; + display: flex; + justify-content: space-between; + align-items: baseline; +} + +.discovery-header__dropdown-tags-display-panel .ant-collapse-header { + display: none; +} + +.discovery-header__dropdown-tags-search { + width: 50%; +} + +.discovery-header__dropdown-tags .ant-select-show-search.ant-select:not(.ant-select-customize-input) .ant-select-selector { + border-width: 1px; + border-radius: 7px; +} + .discovery-modal { box-sizing: border-box; } diff --git a/src/Discovery/Discovery.tsx b/src/Discovery/Discovery.tsx index 2bda1be8c..539509668 100644 --- a/src/Discovery/Discovery.tsx +++ b/src/Discovery/Discovery.tsx @@ -1,13 +1,16 @@ import React, { useState, useEffect } from 'react'; import * as JsSearch from 'js-search'; -import { Tag, Popover } from 'antd'; import { - UnlockOutlined, ClockCircleOutlined, DashOutlined, + Tag, Popover, Space, Collapse, Button, +} from 'antd'; +import { + UnlockOutlined, ClockCircleOutlined, DashOutlined, UpOutlined, DownOutlined, UndoOutlined, } from '@ant-design/icons'; import { DiscoveryConfig } from './DiscoveryConfig'; import './Discovery.css'; import DiscoverySummary from './DiscoverySummary'; import DiscoveryTagViewer from './DiscoveryTagViewer'; +import DiscoveryDropdownTagViewer from './DiscoveryDropdownTagViewer'; import DiscoveryListView from './DiscoveryListView'; import DiscoveryDetails from './DiscoveryDetails'; import DiscoveryAdvancedSearchPanel from './DiscoveryAdvancedSearchPanel'; @@ -24,6 +27,8 @@ export enum AccessLevel { NOT_AVAILABLE = 4, } +const { Panel } = Collapse; + const ARBORIST_READ_PRIV = 'read'; const getTagColor = (tagCategory: string, config: DiscoveryConfig): string => { @@ -199,6 +204,12 @@ const Discovery: React.FunctionComponent = (props: Props) => { const [discoveryActionStatusMessage, setDiscoveryActionStatusMessage] = useState({ url: '', message: '', title: '', active: false, }); + const [searchableTagCollapsed, setSearchableTagCollapsed] = useState( + config.features.search.tagSearchDropdown + && config.features.search.tagSearchDropdown.enabled + && (config.features.search.tagSearchDropdown.collapseOnDefault + || config.features.search.tagSearchDropdown.collapseOnDefault === undefined), + ); const handleSearchChange = (ev) => { const { value } = ev.currentTarget; @@ -476,6 +487,14 @@ const Discovery: React.FunctionComponent = (props: Props) => { config, ); + const enableSearchBar = props.config.features.search + && props.config.features.search.searchBar + && props.config.features.search.searchBar.enabled; + + const enableSearchableTags = props.config.features.search + && props.config.features.search.tagSearchDropdown + && props.config.features.search.tagSearchDropdown.enabled; + // Disabling noninteractive-tabindex rule because the span tooltip must be focusable as per https://www.w3.org/TR/2017/REC-wai-aria-1.1-20171214/#tooltip /* eslint-disable jsx-a11y/no-noninteractive-tabindex */ return ( @@ -491,21 +510,72 @@ const Discovery: React.FunctionComponent = (props: Props) => { visibleResources={visibleResources} config={config} /> - + {(enableSearchableTags) ? ( +
+ +
+ {(enableSearchBar) + && ( +
+ +
+ )} +
+ + +
+
+
+ + +
+ +
+
+
+
+
+
+ ) : ( + + )}
{/* Free-form text search box */} - { (props.config.features.search - && props.config.features.search.searchBar - && props.config.features.search.searchBar.enabled) + { (enableSearchBar && !enableSearchableTags + ) && ( -
+
= (props: DiscoveryTagViewerProps) => { + // getTagsInCategory returns a list of the unique tags in studies which belong + // to the specified category. + const getTagsInCategory = (category: any, displayName: string, studies: any[] | null):React.ReactNode => { + if (!studies) { + return ; + } + const tagMap = {}; + studies.forEach((study) => { + const tagField = props.config.minimalFieldMapping.tagsListFieldName; + study[tagField].forEach((tag) => { + if (tag.category === category.name) { + tagMap[tag.name] = 1; + } + }); + }); + const tagArray = Object.keys(tagMap).sort((a, b) => a.localeCompare(b)); + // get selected tags which value is not 'undefined' + const trulySelectedTags = Object.keys(props.selectedTags).reduce((acc, el) => { + if (props.selectedTags[el] !== undefined) acc.push(el); + return acc; + }, []); + const valueArray = _.intersection(tagArray, trulySelectedTags); + + return ( + + ); + }; + + return ( + + { + props.config.tagCategories.map((category) => { + if (category.display === false) { + return null; + } + + let categoryDisplayName = category.displayName; + if (!categoryDisplayName) { + // Capitalize category name + const categoryWords = category.name.split('_').map((x) => x.toLowerCase()); + categoryWords[0] = categoryWords[0].charAt(0).toUpperCase() + + categoryWords[0].slice(1); + categoryDisplayName = categoryWords.join(' '); + } + + const tags = getTagsInCategory(category, categoryDisplayName, props.studies); + + return ( + + { tags } + + ); + }) + } + + ); +}; + +DiscoveryDropdownTagViewer.defaultProps = { + studies: null, +}; + +export default DiscoveryDropdownTagViewer; diff --git a/src/Discovery/DiscoveryTagViewer.tsx b/src/Discovery/DiscoveryTagViewer.tsx index 4b374a549..8ec220aa0 100644 --- a/src/Discovery/DiscoveryTagViewer.tsx +++ b/src/Discovery/DiscoveryTagViewer.tsx @@ -26,14 +26,11 @@ const DiscoveryTagViewer: React.FunctionComponent = (pr const tagField = props.config.minimalFieldMapping.tagsListFieldName; study[tagField].forEach((tag) => { if (tag.category === category.name) { - if (tagMap[tag.name] === undefined) { - tagMap[tag.name] = 1; - } - tagMap[tag.name] += 1; + tagMap[tag.name] = 1; } }); }); - const tagArray = Object.keys(tagMap).sort((a, b) => tagMap[b] - tagMap[a]); + const tagArray = Object.keys(tagMap).sort((a, b) => a.localeCompare(b)); return (