Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HP-555 Feat/disco tag dropdown viewer #927

Merged
merged 9 commits into from
Sep 29, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion docs/portal_config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
36 changes: 34 additions & 2 deletions src/Discovery/Discovery.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -339,6 +344,33 @@
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-modal {
box-sizing: border-box;
}
Expand Down
92 changes: 81 additions & 11 deletions src/Discovery/Discovery.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 => {
Expand Down Expand Up @@ -199,6 +204,12 @@ const Discovery: React.FunctionComponent<Props> = (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;
Expand Down Expand Up @@ -476,6 +487,14 @@ const Discovery: React.FunctionComponent<Props> = (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 (
Expand All @@ -491,19 +510,70 @@ const Discovery: React.FunctionComponent<Props> = (props: Props) => {
visibleResources={visibleResources}
config={config}
/>
<DiscoveryTagViewer
config={config}
studies={props.studies}
selectedTags={selectedTags}
setSelectedTags={setSelectedTags}
/>
{(enableSearchableTags) ? (
<div className='discovery-header__dropdown-tags-container' id='discovery-tag-filters'>
<Space direction='vertical' style={{ width: '100%' }}>
<div className='discovery-header__dropdown-tags-control-panel'>
{(enableSearchBar)
&& (
<div className='discovery-header__dropdown-tags-search'>
<DiscoveryMDSSearch
searchTerm={searchTerm}
handleSearchChange={handleSearchChange}
inputSubtitle={config.features.search.searchBar.inputSubtitle}
/>
</div>
)}
<div className='discovery-header__dropdown-tags-buttons'>
<Button
type='default'
className={'discovery-header__dropdown-tags-control-button'}
disabled={Object.keys(selectedTags).length === 0}
onClick={() => { setSelectedTags({}); }}
icon={<UndoOutlined />}
>
{'Reset Selection'}
</Button>
<Button
type='default'
className={'discovery-header__dropdown-tags-control-button'}
onClick={() => { setSearchableTagCollapsed(!searchableTagCollapsed); }}
icon={(searchableTagCollapsed) ? <DownOutlined /> : <UpOutlined />}
>
{`${props.config.features.search.tagSearchDropdown.collapsibleButtonText || 'Tag Panel'}`}
</Button>
</div>
</div>
<div className='discovery-header__dropdown-tags-display-panel'>
<Collapse activeKey={(searchableTagCollapsed) ? '' : '1'} ghost>
<Panel header='This is panel header 1' key='1'>
<div className='discovery-header__tags-dropdown'>
<DiscoveryDropdownTagViewer
config={config}
studies={props.studies}
selectedTags={selectedTags}
setSelectedTags={setSelectedTags}
/>
</div>
</Panel>
</Collapse>
</div>
</Space>
</div>
) : (
<DiscoveryTagViewer
config={config}
studies={props.studies}
selectedTags={selectedTags}
setSelectedTags={setSelectedTags}
/>
)}
</div>

<div className='discovery-studies-container'>
{/* Free-form text search box */}
{ (props.config.features.search
&& props.config.features.search.searchBar
&& props.config.features.search.searchBar.enabled)
{ (enableSearchBar && !enableSearchableTags
)
&& (
<div className='discovery-search-container'>
<DiscoveryMDSSearch
Expand Down
6 changes: 5 additions & 1 deletion src/Discovery/DiscoveryConfig.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,14 @@ export interface DiscoveryConfig {
enabled: boolean,
inputSubtitle?: string,
placeholder?: string
// searchTags: boolean, // not supported, consider removing
searchableTextFields?: string[] // list of properties in data to make searchable.
// if not present, only fields visible in the table
// will be searchable.
},
tagSearchDropdown?: {
enabled: boolean,
collapsibleButtonText?: string
collapseOnDefault?: boolean
}
},
authorization: {
Expand Down
137 changes: 137 additions & 0 deletions src/Discovery/DiscoveryDropdownTagViewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import React from 'react';
import _ from 'lodash';
import {
Select, Row, Col, Tag,
} from 'antd';
import { DiscoveryConfig } from './DiscoveryConfig';

const { Option } = Select;

interface DiscoveryTagViewerProps {
config: DiscoveryConfig
studies?: {__accessible: boolean, [any: string]: any}[]
selectedTags: any
setSelectedTags: any
}

const DiscoveryDropdownTagViewer: React.FunctionComponent<DiscoveryTagViewerProps> = (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 <React.Fragment />;
}
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 (
<Select
mode='multiple'
showSearch
showArrow
allowClear
maxTagCount={3}
style={{ width: '100%' }}
id={`discovery-tag-column--${category.name}`}
placeholder={displayName}
value={valueArray}
onSelect={(tag: string) => {
props.setSelectedTags({
...props.selectedTags,
[tag]: props.selectedTags[tag] ? undefined : true,
});
}}
onDeselect={(tag: string) => {
props.setSelectedTags({
...props.selectedTags,
[tag]: props.selectedTags[tag] ? undefined : true,
});
}}
>
{ tagArray.map((tag) => (
<Option
key={category.name + tag}
value={tag}
>
<Tag
key={category.name + tag}
role='button'
tabIndex={0}
aria-pressed={props.selectedTags[tag] ? 'true' : 'false'}
className={`discovery-header__tag-btn discovery-tag ${(props.selectedTags[tag]) ? 'discovery-tag--selected' : ''}`}
aria-label={tag}
style={{
backgroundColor: props.selectedTags[tag] ? category.color : 'initial',
borderColor: category.color,
}}
>
{tag}
</Tag>
</Option>
),
)}
</Select>
);
};

return (
<Row
gutter={[16, 8]}
justify='space-between'
>
{
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 (
<Col
className='discovery-header__tag-group'
key={category.name}
xs={24}
sm={24}
md={12}
lg={12}
xl={12}
xxl={12}
>
{ tags }
</Col>
);
})
}
</Row>
);
};

DiscoveryDropdownTagViewer.defaultProps = {
studies: null,
};

export default DiscoveryDropdownTagViewer;
7 changes: 2 additions & 5 deletions src/Discovery/DiscoveryTagViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,11 @@ const DiscoveryTagViewer: React.FunctionComponent<DiscoveryTagViewerProps> = (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 (
<div>
Expand Down