Commit 7d374366 authored by Matija Obreza's avatar Matija Obreza
Browse files

Merge branch '59-overviews-drill-down'

* 59-overviews-drill-down:
  Overviews: enable drill-down
  Mobile friendly
parents 23eacf6a 4d39250c
......@@ -6,7 +6,7 @@ const origin = typeof window !== 'undefined' ?
export const API_ROOT = `${origin}/proxy`;
export const API_BASE_URL = `${API_ROOT}/api/v0`;
export const SERVER_INFO_URL = `${API_BASE_URL}/info/version`;
export const SERVER_INFO_URL = `${API_ROOT}/api/v1/info/version`;
export const LOGIN_URL = `${API_ROOT}/oauth/token`;
export const LOGOUT_URL = `${API_BASE_URL}/me/logout`;
......
......@@ -32,6 +32,8 @@ class AccessionFilter {
public taxa: TaxonomyFilter;
public uuid: string[];
public version: number[];
public sgsv: boolean;
public storage: number[];
public NOT: AccessionFilter;
}
......
......@@ -12,35 +12,47 @@ import { navigateTo } from 'actions/navigation';
import MuiTabs from '@material-ui/core/Tabs';
import MuiTab from '@material-ui/core/Tab';
import {withStyles} from '@material-ui/core/styles';
import Grid from '@material-ui/core/Grid';
/*tslint:disable*/
const styles = (theme) => ({
root: {
width: '100%',
height: '48px',
display: 'flex' as 'flex',
height: 'auto',
borderBottom: '1px #ccc solid',
[theme.breakpoints.down('md')]: {
height: '96px',
},
},
actionsArea: {
display: 'flex' as 'flex',
alignItems: 'center' as 'center',
position: 'absolute' as 'absolute',
position: 'sticky' as 'sticky',
overflow: 'hidden' as 'hidden',
height: '48px',
right: 0,
left: '100%',
padding: '0 16px',
'& > * > *': {
margin: '0 8px',
margin: '8px',
[theme.breakpoints.down('sm')]: {
width: '100%',
margin: '8px 0',
},
},
[theme.breakpoints.down('md')]: {
right: 'auto' as 'auto',
[theme.breakpoints.down('sm')]: {
position: 'initial' as 'initial',
overflow: 'initial' as 'initial',
display: 'initial' as 'initial',
padding: '0',
height: 'auto' as 'auto',
marginTop: '48px',
width: '100%',
'& > *': {
width: '100%',
}
},
},
tabsArea: {
[theme.breakpoints.down('md')]: {
position: 'relative' as 'relative',
bottom: '-48px'
[theme.breakpoints.down('sm')]: {
position: 'absolute' as 'absolute',
},
}
});
......@@ -50,7 +62,6 @@ interface ITabProps extends React.Props<any> {
name?: string;
to: string;
children: any;
actions?: any;
}
class Tab extends React.Component<ITabProps, any> {
......@@ -83,16 +94,16 @@ class Tabs extends React.Component<any> {
};
return (
<div className={ classes.root }>
<div className={ `pr-20 pl-20 float-right ${classes.actionsArea}` }>
{ actions }
</div>
<div className={ `float-left ${classes.tabsArea}` }>
<Grid container className={ classes.root }>
<Grid item className={ `float-left ${classes.tabsArea}` }>
<MuiTabs value={ currentTab } indicatorColor="primary" onChange={ tabChange }>
{ tabs.map((tab) => <MuiTab key={ tab.to } label={ tab.label } />) }
</MuiTabs>
</div>
</div>
</Grid>
<Grid item className={ `float-right ${classes.actionsArea}` }>
{ actions }
</Grid>
</Grid>
);
}
}
......
......@@ -78,6 +78,7 @@ class DateFilterInternal extends React.Component<IDateFilterInternal, any> {
return (
<div>
<TextField
style={ {width: '50%'} }
label={ label }
type="date"
value={ this.toDateString(this.state.dateA) }
......@@ -85,6 +86,7 @@ class DateFilterInternal extends React.Component<IDateFilterInternal, any> {
InputLabelProps={ { shrink: true } }
/>
<TextField
style={ {width: '50%'} }
label={ label }
type="date"
value={ this.toDateString(this.state.dateB) }
......
......@@ -12,14 +12,17 @@ import {cleanFilters} from 'utilities';
const styles = (theme) => ({
filtersBlock: {
marginBottom: '75px',
overflow: 'visible' as 'visible',
},
stickyButtonContainer: {
position: 'absolute' as 'absolute',
bottom: '10px',
position: 'sticky' as 'sticky',
bottom: '2rem',
width: 'calc(100% - 35px)',
backgroundColor: '#fff',
overflow: 'hidden' as 'hidden',
[theme.breakpoints.down('sm')]: {
bottom: '3rem',
},
},
btnGreen: theme.buttons.green,
btnReset: theme.buttons.reset,
......
......@@ -62,12 +62,14 @@ class NumberFilterInternal extends React.Component<INumberFilterInternal, any> {
return (
<div>
<TextField
style={ {width: '50%'} }
label={ label }
value={ this.state.textA }
placeholder={ 'From including' }
onChange={ this.textAChange }
/>
<TextField
style={ {width: '50%'} }
label={ label }
value={ this.state.textB }
placeholder={ 'To including' }
......
......@@ -10,7 +10,6 @@ import AppBar from '@material-ui/core/AppBar';
import { withStyles } from '@material-ui/core/styles';
import Toolbar from '@material-ui/core/Toolbar';
import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu';
import {ROLE_ADMINISTRATOR, ROLE_CLIENT, ROLE_USER} from 'constants/userRoles';
import {saveCookies} from 'utilities';
......@@ -19,7 +18,7 @@ import UserMenuComponent from './UserMenuComponent';
import LeftMenu from './LeftMenu';
import LangListComponent from './LangListComponent';
const styleSheet = {
const styleSheet = (theme) => ({
root: {
marginTop: 90,
width: '100%',
......@@ -30,9 +29,15 @@ const styleSheet = {
headerRoot: {
backgroundColor: '#2b2924',
height: 'auto',
[theme.breakpoints.down('sm')]: {
height: '3.5rem',
},
},
blockHeader: {
padding: '0 20px',
[theme.breakpoints.down('sm')]: {
alignItems: 'baseline' as 'baseline',
},
},
/*tslint:disable*/
navLogo: {
......@@ -88,8 +93,13 @@ const styleSheet = {
backgroundColor: 'transparent' as 'transparent',
}
},
mobileHidden: {
[theme.breakpoints.down('sm')]: {
display: 'none'
},
},
/* tslint:enable */
};
});
interface IHeaderProps extends React.ClassAttributes<any> {
login: any;
......@@ -150,24 +160,24 @@ class Header extends React.Component<IHeaderProps | any, any> {
<Toolbar className={ classes.blockHeader }>
<div className="mobile-navigation-block">
<IconButton className={ classes.menuBtn } aria-label="Menu" color="secondary" onClick={ this.handleLeftMenuOpen } >
<MenuIcon />
<img src="images/GENESYS-ICON.svg" className={ classes.logoIcon } />
</IconButton>
<LeftMenu open={ this.state.open } closeMenu={ this.handleLeftMenuClose } />
</div>
<div className={ classes.flex }>
<Link to="/" className={ `float-left ${this.props.classes.navLogo}` }>
<Link to="/" className={ `float-left ${this.props.classes.navLogo} ${classes.mobileHidden}` }>
<img src="images/GENESYS-ICON.svg" className={ classes.mainIcon } />
<img src="images/GENESYS-LOGO.svg" className={ classes.logoIcon } />
</Link>
<div className="navigation-block">
<div className="navigation-block float-right">
<NavLink activeClassName="active" to="/a">{ t('menu.Accessions') }</NavLink>
<NavLink activeClassName="active" to="/subsets">{ t('menu.Subsets') }</NavLink>
<NavLink activeClassName="active" to="/wiews">{ t('menu.Institutes') }</NavLink>
</div>
</div>
<div>{ this.renderLogin([ROLE_USER, ROLE_ADMINISTRATOR]) }</div>
<div className="float-right">{ this.renderLogin([ROLE_USER, ROLE_ADMINISTRATOR]) }</div>
<div>
<LangListComponent />
......
......@@ -17,6 +17,9 @@ const styles = (theme) => ({
width: '100%',
overflow: 'visible' as 'visible',
position: 'absolute' as 'absolute',
[theme.breakpoints.down('sm')]: {
paddingTop: '3rem',
},
},
content: {
flexGrow: 1,
......
import * as React from 'react';
import Drawer from '@material-ui/core/Drawer';
import SwipeableDrawer from '@material-ui/core/SwipeableDrawer';
const SidebarDrawer = ({variant, className, isOpen, children}) => (
<Drawer
<SwipeableDrawer
variant={ variant }
className={ className }
open={ isOpen }
onClose={ () => console.log('Sidebar closed') }
onOpen={ () => console.log('Sidebar opened') }
>
{ children }
</Drawer>
</SwipeableDrawer>
);
export default SidebarDrawer;
import * as React from 'react';
import {bindActionCreators} from 'redux';
import {bindActionCreators, compose} from 'redux';
import {connect} from 'react-redux';
import {withStyles} from '@material-ui/core/styles';
import SidebarDrawer from './SidebarDrawer';
......@@ -7,26 +7,45 @@ import ChevronRight from '@material-ui/icons/ChevronRight';
import ChevronLeft from '@material-ui/icons/ChevronLeft';
import { collapseSidebar } from 'actions/layout';
import withWidth from '@material-ui/core/withWidth/withWidth';
import {Breakpoint} from '@material-ui/core/styles/createBreakpoints';
interface ISidebarProps extends React.ClassAttributes<any> {
classes: any;
sidebarContent: any;
collapseSidebar: (isOpen: boolean) => void;
isOpen: boolean;
width: Breakpoint;
}
const mobile = ['sm', 'xs'] as Breakpoint[];
const styles = (theme) => ({
/* tslint:disable */
drawer: {
position: 'sticky' as 'sticky',
top: '72px',
zIndex: 10,
height: 'calc(100vh - 72px)',
'& > div': {
top: 'auto' as 'auto',
position: 'initial' as 'initial',
height: '100%' as '100%'
},
[theme.breakpoints.down('sm')]: {
height: 'calc(100vh - 3rem)',
top: '3rem',
maxWidth: '90%',
position: 'fixed' as 'fixed',
zIndex: 15,
}
},
drawerCollapsed: {
[theme.breakpoints.down('sm')]: {
width: 'auto',
position: 'sticky' as 'sticky',
}
},
sidebar: {
whiteSpace: 'nowrap' as 'nowrap',
......@@ -60,13 +79,21 @@ const styles = (theme) => ({
},
sidebarContents: {
width: '100%',
minHeight: 'calc(100% - 2rem - 75px)',
minHeight: 'calc(100% - 2rem)',
height: 'auto' as 'auto',
[theme.breakpoints.down('sm')]: {
minHeight: 'calc(100% - 3rem)',
},
},
sidebarContentsCollapsed: {
minHeight: 'calc(100% - 52px)',
[theme.breakpoints.down('sm')]: {
minHeight: 'calc(100% - 38px)',
},
},
/* tslint:disable */
collapseButton: {
width: '100%',
marginTop: '75px',
position: 'sticky' as 'sticky',
backgroundColor: '#e6e6e6',
height: '2rem',
......@@ -82,15 +109,22 @@ const styles = (theme) => ({
display: 'flex' as 'flex',
alignItems: 'center' as 'center',
height: '100%',
fontSize: '16px'
fontSize: '16px',
[theme.breakpoints.down('sm')]: {
fontSize: '1.5rem',
},
},
'& > span > svg':{
'html[dir="rtl"] &' : {
transform: 'rotate(180deg)',
},
}
},
[theme.breakpoints.down('sm')]: {
height: '3rem',
},
},
buttonCollapsed: {
top: '100%',
// width: '72px',
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
......@@ -101,7 +135,20 @@ const styles = (theme) => ({
transform: 'rotate(180deg)',
},
}
}
},
pageContent: {
zIndex: -2,
backgroundColor: '#878787',
opacity: 0.4,
position: 'fixed' as 'fixed',
width: '100%',
height: '100%',
top: 0,
left: 0,
[theme.breakpoints.up('md')]: {
display: 'none' as 'none',
},
},
});
class SidebarWrapper extends React.Component<ISidebarProps, any> {
......@@ -115,11 +162,12 @@ class SidebarWrapper extends React.Component<ISidebarProps, any> {
}
public render() {
const { sidebarContent, classes, isOpen } = this.props;
const { sidebarContent, classes, isOpen, width } = this.props;
const isMobile = mobile.indexOf(width) !== -1;
return (
<SidebarDrawer
className={ classes.drawer }
className={ `${classes.drawer} ${!isOpen ? classes.drawerCollapsed : ''}` }
isOpen={ isOpen }
variant={ 'permanent' }
>
......@@ -133,11 +181,12 @@ class SidebarWrapper extends React.Component<ISidebarProps, any> {
className={ `${classes.collapseButton}` }>
<span><ChevronLeft /> Collapse</span>
</div>
<div className={classes.pageContent} onClick={() => { isMobile ? this.setIsCollapsed(true) : null; } }> </div>
</div>
}
{ !isOpen &&
<div className={ `${ classes.sidebar } ${classes.sidebarCollapsed}` }>
<div className={ classes.sidebarContents } onClick={() => { this.setIsCollapsed(false); }}>
<div className={ `${classes.sidebarContents} ${classes.sidebarContentsCollapsed}` } onClick={() => { this.setIsCollapsed(false); }}>
<p className={ classes.collapsedText }>Open sidebar</p>
</div>
<div
......@@ -161,4 +210,4 @@ const mapDispatchToProps = (dispatch) => bindActionCreators({
collapseSidebar,
}, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(SidebarWrapper));
export default connect(mapStateToProps, mapDispatchToProps)(compose(withWidth(), withStyles(styles))(SidebarWrapper));
......@@ -62,9 +62,9 @@ class BrowsePage extends BrowsePageTemplate<Accession> {
tab={ currentTab }
actions={
<span>
<Button> Select all </Button>
<Button> Delete </Button>
<Button> Share </Button>
<Button variant="raised"> Select all </Button>
<Button variant="raised"> Delete </Button>
<Button variant="raised"> Share </Button>
</span>
}
>
......
......@@ -3,6 +3,7 @@ import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import { parse } from 'query-string';
import { translate } from 'react-i18next';
import * as _ from 'lodash';
// Actions
import {applyFilters, loadAccessionsPage, listAccessionsPromise, updateRoute, applyOverviewFilters, loadAccessionsOverviewPage} from 'actions/accessions';
......@@ -20,6 +21,7 @@ import ContentHeader from 'ui/common/heading/ContentHeader';
import Tabs, {Tab} from 'ui/common/Tabs';
import PropertiesCard from 'ui/common/PropertiesCard';
import PrettyFilters from 'ui/common/filter/PrettyFilters';
import Number from 'ui/common/Number';
import AccessionFilters from './c/Filters';
......@@ -35,7 +37,7 @@ interface IOverviewPageProps extends React.ClassAttributes<any> {
currentTab: string;
t: any;
}
/* tslint:disable */
class BrowsePage extends React.Component<IOverviewPageProps, any> {
protected static needs = [
......@@ -60,6 +62,36 @@ class BrowsePage extends React.Component<IOverviewPageProps, any> {
}
}
private addTerm = (property, term) => {
const { overview, applyOverviewFilters } = this.props;
const updatedFilter: AccessionFilter = { ...overview.filter };
switch (property) {
case 'sampStat':
case 'holder.code':
case 'holder.country.iso3':
case 'origin.iso3':
case 'taxa.genus':
case 'taxa.species':
case 'storage':
_.set(updatedFilter, property, _.concat(_.get(updatedFilter, property), term).filter(x => x != null));
break;
case 'sgsv':
case 'available':
case 'mlsStatus':
_.set(updatedFilter, property, term === '1' ? true : false);
break;
// set
default:
_.set(updatedFilter, property, term);
}
// console.log(`Updated filter for ${property} +${term}`, updatedFilter);
applyOverviewFilters(updatedFilter);
};
public render() {
const { filterCode, currentTab, applyOverviewFilters, overview: overviewWrapper, t } = this.props;
const overview = overviewWrapper && overviewWrapper.overview || null;
......@@ -67,6 +99,8 @@ class BrowsePage extends React.Component<IOverviewPageProps, any> {
const overviewKeys = ['institute.code', 'institute.country.code3', 'cropName', 'crop.shortName', 'sampStat', 'taxonomy.genus', 'taxonomy.genusSpecies',
'countryOfOrigin.code3', 'donorCode', 'mlsStatus', 'available', 'duplSite', 'sgsv', 'storage', 'breederCode'];
const skipTerms = ['Other', 'Missing', 'Not specified'];
const overviewsTerms = new Map();
if (overview) {
overviewKeys.forEach((key) => {
......@@ -76,6 +110,19 @@ class BrowsePage extends React.Component<IOverviewPageProps, any> {
});
}
const filterByTerm = (property, term, count) => {
return (
<div>
{
(skipTerms.indexOf(term.term) === -1)
? (<a onClick={ () => this.addTerm(property, term.term) }><Number value={ count } /></a>)
: (<span>{ count }</span>)
}
</div>
);
};
return (
<PageLayout
sidebar={
......@@ -105,32 +152,32 @@ class BrowsePage extends React.Component<IOverviewPageProps, any> {
<PageContents>
<GridLayout>
{ overviewsTerms && overviewsTerms.get('institute.code') && overviewsTerms.get('institute.code').length > 2 &&
<PropertiesCard propertiesList={ overviewsTerms.get('institute.code').map((term) => ({title: term.term, value: term.count })) } title={ t(`accession.overview.institute code`) } small/>
<PropertiesCard propertiesList={ overviewsTerms.get('institute.code').map((term) => ({title: term.term, value: filterByTerm('holder.code', term, term.count) })) } title={ t(`accession.overview.institute code`) } small/>
}
{ overviewsTerms && overviewsTerms.get('institute.country.code3') && overviewsTerms.get('institute.country.code3').length > 2 &&
<PropertiesCard propertiesList={ overviewsTerms.get('institute.country.code3').map((term) => ({title: term.term, value: term.count })) } title={ t(`accession.overview.institute country code3`) } small/>
<PropertiesCard propertiesList={ overviewsTerms.get('institute.country.code3').map((term) => ({title: term.term, value: filterByTerm('holder.country.iso3', term, term.count) })) } title={ t(`accession.overview.institute country code3`) } small/>
}
{ overviewsTerms && overviewsTerms.get('crop.shortName') && overviewsTerms.get('crop.shortName').length > 2 &&
<PropertiesCard propertiesList={ overviewsTerms.get('crop.shortName').map((term) => ({title: term.term, value: term.count })) } title={ t(`accession.overview.crop shortName`) } small/>
<PropertiesCard propertiesList={ overviewsTerms.get('crop.shortName').map((term) => ({title: term.term, value: filterByTerm('crop', term, term.count)})) } title={ t(`accession.overview.crop shortName`) } small/>
}
{ overviewsTerms && overviewsTerms.get('cropName') && overviewsTerms.get('cropName').length > 2 &&
<PropertiesCard propertiesList={ overviewsTerms.get('cropName').map((term) => ({title: term.term, value: term.count })) } title={ t(`accession.overview.cropName`) } small/>
}
{ overviewsTerms && overviewsTerms.get('taxonomy.genus') && overviewsTerms.get('taxonomy.genus').length > 2 &&
<PropertiesCard propertiesList={ overviewsTerms.get('taxonomy.genus').map((term) => ({title: term.term, value: term.count })) } title={ t(`accession.overview.taxonomy genus`) } small/>
<PropertiesCard propertiesList={ overviewsTerms.get('taxonomy.genus').map((term) => ({title: term.term, value: filterByTerm('taxa.genus', term, term.count) })) } title={ t(`accession.overview.taxonomy genus`) } small/>
}
{ overviewsTerms && overviewsTerms.get('taxonomy.genusSpecies') && overviewsTerms.get('taxonomy.genusSpecies').length > 2 &&
<PropertiesCard propertiesList={ overviewsTerms.get('taxonomy.genusSpecies').map((term) => ({title: term.term, value: term.count })) } title={ t(`accession.overview.taxonomy genusSpecies`) } small/>
}
{ overviewsTerms && overviewsTerms.get('sampStat') && overviewsTerms.get('sampStat').length > 2 &&
<PropertiesCard propertiesList={ overviewsTerms.get('sampStat')
.filter((term) => term.term !== 'other').map((term) => ({title: t(`accession.sampleStatus.${term.term}`), value: term.count })) } title={ t(`accession.overview.sampStat`) } small/>
.filter((term) => term.term !== 'other').map((term) => ({title: t(`accession.sampleStatus.${term.term}`), value: filterByTerm('sampStat', term, term.count) })) } title={ t(`accession.overview.sampStat`) } small/>
}
{ overviewsTerms && overviewsTerms.get('storage') && overviewsTerms.get('storage').length > 2 &&
<PropertiesCard propertiesList={ overviewsTerms.get('storage').map((term) => ({title: t(`accession.storage.${term.term}`), value: term.count })) } title={ t(`accession.overview.storage`) } small/>
<PropertiesCard propertiesList={ overviewsTerms.get('storage').map((term) => ({title: t(`accession.storage.${term.term}`), value: filterByTerm('storage', term, term.count) })) } title={ t(`accession.overview.storage`) } small/>
}
{ overviewsTerms && overviewsTerms.get('countryOfOrigin.code3') && overviewsTerms.get('countryOfOrigin.code3').length > 2 &&
<PropertiesCard propertiesList={ overviewsTerms.get('countryOfOrigin.code3').map((term) => ({title: term.term, value: term.count })) } title={ t(`accession.overview.countryOfOrigin code3`) } small/>
<PropertiesCard propertiesList={ overviewsTerms.get('countryOfOrigin.code3').map((term) => ({title: term.term, value: filterByTerm('origin.iso3', term, term.count)})) } title={ t(`accession.overview.countryOfOrigin code3`) } small/>
}
{ overviewsTerms && overviewsTerms.get('donorCode') && overviewsTerms.get('donorCode').length > 2 &&
<PropertiesCard propertiesList={ overviewsTerms.get('donorCode').map((term) => ({title: term.term, value: term.count })) } title={ t(`accession.overview.donorCode`) } small/>
......@@ -142,13 +189,13 @@ class BrowsePage extends React.Component<IOverviewPageProps, any> {
<PropertiesCard propertiesList={ overviewsTerms.get('breederCode').map((term) => ({title: term.term, value: term.count })) } title={ t(`accession.overview.breederCode`) } small/>
}
{ overviewsTerms && overviewsTerms.get('mlsStatus') && overviewsTerms.get('mlsStatus').length > 2 &&
<PropertiesCard propertiesList={ overviewsTerms.get('mlsStatus').map((term) => ({title: term.term === '1' ? 'Yes' : 'No', value: term.count })) } title={ t(`accession.overview.mlsStatus`) } small/>
<PropertiesCard propertiesList={ overviewsTerms.get('mlsStatus').map((term) => ({title: term.term === '1' ? 'Yes' : 'No', value: filterByTerm('mlsStatus', term, term.count) })) } title={ t(`accession.overview.mlsStatus`) } small/>
}
{ overviewsTerms && overviewsTerms.get('available'