Commit d49d7d34 authored by Oleksii Savran's avatar Oleksii Savran Committed by Matija Obreza

Diversity trees: Public pages

parent 0ffacb5b
import Article from 'model/cms/Article';
import Crop from 'model/genesys/Crop';
import ImageGallery from 'model/repository/ImageGallery';
import RepositoryFile from 'model/repository/RepositoryFile';
import Dataset from 'model/catalog/Dataset';
import Subset from 'model/subset/Subset';
import DescriptorList from 'model/catalog/DescriptorList';
import DiversityTree from 'model/DiversityTree';
/*
* Defined in Swagger as '#/definitions/CropDetails'
*/
class CropDetails extends Crop {
public accessionCount: number;
public blurb: Article;
public files: RepositoryFile[];
public imageGallery: ImageGallery;
public overview: any;
public recentDatasets: Dataset[];
public recentSubsets: Subset[];
public recentDescriptorLists: DescriptorList[];
public diversityTrees: DiversityTree[];
}
export default CropDetails;
......@@ -4,11 +4,9 @@ import { bindActionCreators } from 'redux';
import { WithTranslation, withTranslation } from 'react-i18next';
import withStyles from '@material-ui/core/styles/withStyles';
import * as _ from 'lodash';
import axios from 'axios';
// Actions
import { loadCropDetails } from 'crop/actions/public';
import { repositoryDownloadUrl } from 'repository/actions/public';
import { relinkAccessions } from 'crop/actions/admin';
import { applyFiltersAsync as applyFilters, applyOverviewFiltersAsync as applyOverviewFilters } from 'accessions/actions/public';
import { showSnackbar } from 'actions/snackbar';
......@@ -38,9 +36,9 @@ import { IPageRequest } from '@genesys/client/model/Page';
import ErrorMessage from 'ui/common/error/ErrorMessage';
import ImageGalleryView from 'repository/ui/c/ImageGalleryView';
import TreePreviewer from 'crop/ui/c/TreePreviewer';
import RepositoryFile from '@genesys/client/model/repository/RepositoryFile';
// import RepositoryFile from 'model/repository/RepositoryFile';
import { ScrollToTopOnMount } from 'ui/common/page/scrollers';
import { DatasetLink, SubsetLink } from 'ui/genesys/Links';
import { DatasetLink, DiversityTreeLink, SubsetLink } from 'ui/genesys/Links';
import { DescriptorListLink } from 'ui/catalog/Links';
import { Grid } from '@material-ui/core';
......@@ -67,8 +65,6 @@ interface IDisplayPageProps extends React.ClassAttributes<any>, WithTranslation
relinkAccessions: (shortName: string) => void;
applyOverviewFilters: (filters: string | AccessionFilter, page?: IPageRequest) => void;
showSnackbar: (snack: string) => void;
repositoryDownloadUrl: (file: RepositoryFile) => string;
treeData: object;
}
class DisplayPage extends React.Component<IDisplayPageProps, any> {
......@@ -79,56 +75,12 @@ class DisplayPage extends React.Component<IDisplayPageProps, any> {
},
];
public state: {
treesData: {treeData: any, treeFile: any}[],
} = {
treesData: null,
};
public componentDidMount() {
const { loadCropDetails, shortName, cropDetails, repositoryDownloadUrl } = this.props;
const { loadCropDetails, shortName, cropDetails } = this.props;
if (shortName && (! cropDetails || shortName !== cropDetails.shortName)) {
loadCropDetails(shortName);
this.setState({ treesData: null });
}
if (typeof window !== 'undefined') {
if (cropDetails && cropDetails.files && cropDetails.shortName === shortName) {
console.log(`Crop files`, cropDetails.files);
const treeFiles = cropDetails.files && cropDetails.files.filter((file) => {
return file.contentType === 'application/json' && file.originalFilename.match(/tree\.json$/);
});
if (treeFiles && treeFiles.length > 0) {
const treeFilePromises = treeFiles.map((treeFile) => axios.get(repositoryDownloadUrl(treeFile))
.then((response) => ({ treeData: response.data, treeFile }))
.catch((err) => { console.log(`Err`, err)}));
Promise.all(treeFilePromises)
.then((treesData) => this.setState({treesData: treesData.filter((treeData) => !!treeData)}))
.catch((err) => { console.log(`Error getting tree data`, err)});
}
}
}
}
public componentDidUpdate(prevProps: IDisplayPageProps) {
const { cropDetails: currentCrop } = prevProps;
const { cropDetails, repositoryDownloadUrl } = this.props;
const { treesData } = this.state;
if (cropDetails && (! currentCrop || (currentCrop.shortName !== cropDetails.shortName))) {
console.log(`Crop files`, cropDetails.files);
const treeFiles = cropDetails.files && cropDetails.files.filter((file) => {
return file.contentType === 'application/json' && file.originalFilename.match(/tree\.json$/);
});
if (treeFiles && treeFiles.length > 0 && ! treesData) {
const treeFilePromises = treeFiles.map((treeFile) => axios.get(repositoryDownloadUrl(treeFile))
.then((response) => ({ treeData: response.data, treeFile }))
.catch((err) => { console.log(`Err`, err)}));
Promise.all(treeFilePromises)
.then((treesData) => this.setState({treesData: treesData.filter((treeData) => !!treeData)}))
.catch((err) => { console.log(`Error getting tree data`, err)});
}
}
}
private applyCropFilter = () => {
......@@ -172,7 +124,6 @@ class DisplayPage extends React.Component<IDisplayPageProps, any> {
public render() {
const { cropDetails, error, shortName, classes, t } = this.props;
const { treesData } = this.state;
const crop = cropDetails;
return (
......@@ -256,9 +207,6 @@ class DisplayPage extends React.Component<IDisplayPageProps, any> {
propertyItemProps={ {classes: {propertiesRowLabel: classes.italicProperties} } }
/>
}
{ treesData && treesData.map((tree) => (tree && tree.treeData.children && tree.treeData.children.length > 0 &&
<TreePreviewer key={ tree.treeFile.uuid } source={ tree.treeFile } treeData={ tree.treeData } name={ tree.treeFile.subject || tree.treeFile.title || crop.name } />
))}
</GridContainer >
}
<PropertiesCard title={ t(`crop.public.p.display.otherNames`) }
......@@ -314,6 +262,21 @@ class DisplayPage extends React.Component<IDisplayPageProps, any> {
/>
</Grid>
}
{ cropDetails.diversityTrees && cropDetails.diversityTrees.length > 0 &&
<Grid item lg={ 4 } xs={ 12 } style={ { minHeight: '100%' } }>
<PropertiesCard
style={ {height: '100%'} }
title={ t('DiversityTrees') } // todo: fix translations
propertyItemProps={ { keepEmpty: true } }
propertiesList={
cropDetails.diversityTrees
.map((diversityTree) => (
{ value: <DiversityTreeLink to={ diversityTree }/> }
))
}
/>
</Grid>
}
</GridContainer>
</PageContents>
}
......@@ -335,7 +298,6 @@ const mapDispatchToProps = (dispatch) => bindActionCreators({
relinkAccessions,
applyOverviewFilters,
showSnackbar,
repositoryDownloadUrl,
}, dispatch);
......
import * as React from 'react';
// import { createStyles, withStyles } from '@material-ui/core/styles';
//
// const styles = createStyles({
// leafNode: {},
// node: {},
// });
let Tree;
......@@ -6,21 +12,59 @@ interface IConfiguredTreeProps extends React.ClassAttributes<any> {
treeData: any[];
initialZoom: number;
zoomable?: boolean;
onClickHandler?: any;
}
class NodeLabel extends React.PureComponent<any, any> {
private timeout: any = null;
public state = {
count: 0,
};
private onClick = (e) => {
const { nodeData } = this.props;
if (this.state.count === 1) {
console.log('expand!');
clearTimeout(this.timeout);
this.setState({ count: 0 });
return;
}
e.stopPropagation();
this.setState({ count: 1 });
this.timeout = setTimeout(() => {
console.log('custom handler!', nodeData);
this.setState({ count: 0 });
}, 300);
};
public render() {
const { nodeData } = this.props;
return (
<div onClick={ this.onClick }>
<h6 className="pl-20 pr-20 m-0" >{ nodeData.name }</h6>
</div>
)
}
}
class ConfiguredTree extends React.Component<IConfiguredTreeProps, any> {
private treeContainer = null;
// private timeout: any = null;
public constructor(props, context) {
super(props, context);
if (typeof window !== 'undefined') {
Tree = require('react-d3-tree').Tree;
}
this.state = {
translate: null,
};
}
public state = {
count: 0,
translate: null,
};
public componentDidMount() {
if (this.treeContainer) {
const dimensions = this.treeContainer.getBoundingClientRect();
......@@ -34,12 +78,13 @@ class ConfiguredTree extends React.Component<IConfiguredTreeProps, any> {
}
public render() {
const {treeData, initialZoom, zoomable = false} = this.props;
const { treeData, initialZoom, zoomable = false } = this.props;
return (
<div style={ { height: '100%' } } ref={ (ref) => this.treeContainer = ref }>
<div style={ { height: '100%' } } ref={ (ref) => this.treeContainer = ref } /* onClickCapture={ this.test } */>
{ treeData && typeof window !== 'undefined' &&
<Tree
// onClick={ this.onNodeClick }
separation={ {
siblings: 0.25,
nonSiblings: 1,
......@@ -61,20 +106,20 @@ class ConfiguredTree extends React.Component<IConfiguredTreeProps, any> {
circle: {
fill: '#88ba42',
},
name: {
strokeWidth: 0.4,
transform: 'translate(-13px, -1px)',
textAnchor: 'end',
},
// name: {
// strokeWidth: 0.4,
// transform: 'translate(-13px, -1px)',
// textAnchor: 'end',
// },
},
leafNode: {
circle: {
fill: '#777777',
},
name: {
strokeWidth: 0.4,
transform: 'translate(13px)',
},
// name: {
// strokeWidth: 0.4,
// transform: 'translate(13px)',
// },
},
},
} }
......@@ -85,6 +130,15 @@ class ConfiguredTree extends React.Component<IConfiguredTreeProps, any> {
textAnchor: 'start',
y: 0,
} }
allowForeignObjects
nodeLabelComponent={{
render: <NodeLabel />,
foreignObjectWrapper: {
y: -6,
x: -10,
height: '1.1rem',
},
}}
/>
}
</div>
......
......@@ -17,7 +17,7 @@ const publicRoutes = [
// },
// },
{
path: '/divtree/:uuid([a-z\\-0-9]+)',
path: '/divtree/:uuid([a-z\\-0-9]+)/:tab?',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "divtree" */'divtree/ui/DisplayPage'),
}),
......
......@@ -2,6 +2,8 @@ import * as React from 'react';
import { WithTranslation, withTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import axios from 'axios';
import { createStyles, WithStyles, withStyles } from '@material-ui/core/styles';
// Actions
import {
rematchDiversityTreeAccessions,
......@@ -13,6 +15,7 @@ import {
editDiversityTree,
} from 'divtree/actions/public';
import navigateTo from 'actions/navigation';
import { repositoryDownloadUrl } from 'repository/actions/public';
// import { addSubsetAccessionsToMyList } from 'list/actions/public';
// Models
import DiversityTree from '@genesys/client/model/DiversityTree';
......@@ -28,8 +31,37 @@ import ErrorMessage from 'ui/common/error/ErrorMessage';
import { ScrollToTopOnMount } from 'ui/common/page/scrollers';
import BackButton from 'ui/common/buttons/BackButton';
import DiversityTreeDisplay from './c/DiversityTreeDisplay';
import Tabs, { Tab } from 'ui/common/Tabs';
import RepositoryFile from 'model/repository/RepositoryFile';
import ConfiguredTree from 'crop/ui/c/ConfiguredTree';
interface IDisplayPageProps extends React.ClassAttributes<any>, WithTranslation {
const styles = () => createStyles({
layoutOverrides: {
'& > div': {
'& > div:first-child': {
'flexShrink': 0,
'flexGrow': 2,
'height': '100%',
'display': 'flex',
'flexDirection': 'column',
'& > div:first-child': {
flexBasis: 'auto',
},
},
},
},
treeWrapper: {
'direction': 'ltr',
'fontSize': '1.2rem',
'flexGrow': 2,
'display': 'flex',
'& > div': {
flexGrow: 2,
},
},
});
interface IDisplayPageProps extends React.ClassAttributes<any>, WithTranslation, WithStyles {
uuid: string;
divtree: DiversityTree;
userRole: string[];
......@@ -46,6 +78,8 @@ interface IDisplayPageProps extends React.ClassAttributes<any>, WithTranslation
rematchDiversityTreeAccessions: (divtree: DiversityTree) => void;
addDiversityTreeAccessionsToMyList: (divtree: DiversityTree) => void;
accessionRefs: Page<DiversityTreeAccessionRef>;
currentTab?: string;
repositoryDownloadUrl: (file: RepositoryFile) => string;
}
class DisplayPage extends React.Component<IDisplayPageProps, any> {
......@@ -63,33 +97,76 @@ class DisplayPage extends React.Component<IDisplayPageProps, any> {
super(props, context);
}
public state = {
treeData: null,
treeDataLoading: false,
};
public componentDidMount() {
const { divtree, uuid, loadDiversityTree, loadMoreAccessions } = this.props;
if (uuid && (! divtree || uuid !== divtree.uuid)) {
loadDiversityTree(uuid);
loadMoreAccessions(uuid);
}
if (typeof window !== 'undefined') {
if (divtree && divtree.treeFile && divtree.uuid === uuid) {
console.log('DivTree file', divtree.treeFile);
// const treeFiles = cropDetails.files && cropDetails.files.filter((file) => {
// return file.contentType === 'application/json' && file.originalFilename.match(/tree\.json$/);
// });
this.loadTreeFile(divtree.treeFile);
}
}
}
public componentDidUpdate() {
public componentDidUpdate(prevProps: IDisplayPageProps) {
const { loading, divtree, uuid, loadDiversityTree, loadMoreAccessions } = this.props;
const { divtree: prevDivTree } = prevProps;
if (!loading && uuid && (! divtree || uuid !== divtree.uuid)) {
loadDiversityTree(uuid);
loadMoreAccessions(uuid);
}
if (divtree && (! prevDivTree || (prevDivTree.uuid !== divtree.uuid))) {
this.loadTreeFile(divtree.treeFile);
}
}
private loadTreeFile = (treeFile: RepositoryFile) => {
const { repositoryDownloadUrl } = this.props;
console.log('load divtree', treeFile);
this.setState({ treeDataLoading: true });
axios
.get(repositoryDownloadUrl(treeFile))
.then((response) => {
console.log(`Tree data`, treeFile, response.data);
if (typeof response.data === 'object') {
this.setState({ treeData: response.data, treeDataLoading: false });
}
})
.catch((err) => {
console.log(`Error getting tree data`, err);
this.setState({ treeDataLoading: false });
});
};
private loadMoreAccessions = (paged: Page<DiversityTreeAccessionRef>) => {
const { loadMoreAccessions, divtree } = this.props;
loadMoreAccessions(divtree.uuid, paged);
}
public render() {
const { error, divtree, loading, userRole, t, accessionRefs, rematchDiversityTreeAccessions, publishDiversityTree, rejectDiversityTree, approveDiversityTree, deleteDiversityTree, editDiversityTree } = this.props;
const {
error, divtree, loading, userRole, t, accessionRefs, rematchDiversityTreeAccessions, publishDiversityTree,
rejectDiversityTree, approveDiversityTree, deleteDiversityTree, editDiversityTree, uuid, classes, currentTab,
} = this.props;
const { treeData, treeDataLoading } = this.state;
const isActionsActive: boolean = userRole.findIndex((role) => role === 'ROLE_ADMINISTRATOR') !== -1 || (divtree && divtree.state === PublishState.REVIEWING);
return (
<PageLayout>
<PageLayout className={ classes.layoutOverrides }>
<ScrollToTopOnMount/>
<PageTitle title={ !loading ? divtree && divtree.title : t('common:label.loading', { what: t('divtree.public.p.display.title') }) }/>
<ContentHeaderWithButton
......@@ -103,13 +180,25 @@ class DisplayPage extends React.Component<IDisplayPageProps, any> {
/>
}
/>
<PageContents className="pt-1rem">
{ loading ? <Loading /> :
<Tabs
tab={ currentTab }
tabs={
[
<Tab key="data" name="data" to={ `/divtree/${uuid}` }>
{ t('Info') }
</Tab>,
<Tab key="view" name="view" to={ `/divtree/${uuid}/view` } disabled={ !treeData }>
{ t('View') }
</Tab>,
]
}
/>
{ currentTab === 'data' && (loading ? <Loading /> :
<PageContents className="pt-1rem">
<div>
{ error && <ErrorMessage error={ error }/> }
{ divtree &&
<DiversityTreeDisplay
<DiversityTreeDisplay
isActionsActive={ isActionsActive }
divtree={ divtree }
accessions={ accessionRefs }
......@@ -121,11 +210,21 @@ class DisplayPage extends React.Component<IDisplayPageProps, any> {
approveDiversityTree={ approveDiversityTree }
deleteDiversityTree={ deleteDiversityTree }
addAllToMyList={ this.props.addDiversityTreeAccessionsToMyList }
/>
/>
}
</div>
}
</PageContents>
</PageContents>
) }
{ currentTab === 'view' && (treeDataLoading ? <Loading /> : treeData &&
<div className={ classes.treeWrapper }>
<ConfiguredTree
treeData={ [treeData] }
initialZoom={ 0.25 }
zoomable
// onClickHandler={ this.onNodeClick }
/>
</div>
) }
</PageLayout>
);
}
......@@ -138,6 +237,7 @@ const mapStateToProps = (state, ownProps) => ({
userRole: state.login.authorities,
error: state.divtree.public.divtree ? state.divtree.public.divtree.error : undefined,
uuid: ownProps.match.params.uuid,
currentTab: ownProps.match.params.tab || 'data',
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
......@@ -149,8 +249,9 @@ const mapDispatchToProps = (dispatch) => bindActionCreators({
deleteDiversityTree,
loadMoreAccessions,
rematchDiversityTreeAccessions,
repositoryDownloadUrl,
// addDiversityTreeAccessionsToMyList,
}, dispatch);
export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(DisplayPage));
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(withTranslation()(DisplayPage)));
......@@ -67,13 +67,14 @@ interface ILayoutProps extends React.ClassAttributes<any>, WithStyles {
children?: any;
sidebar?: any;
withFooter?: any;
className?: string;
}
function Layout({classes, children = null, sidebar = null, withFooter = false}: ILayoutProps) {
function Layout({classes, children = null, sidebar = null, withFooter = false, className = ''}: ILayoutProps) {
const sidebarIsOpen = useSelector((state) => state.user.public.sidebarOpen) || false;
return (
<div>
<div className={ classes.root }>
<div className={ `${classes.root} ${className}` }>
{ sidebar && <SidebarWrapper sidebarContent={ sidebar }/> }
{ children && (
<div className={ `${classes.content} ${sidebar && !sidebarIsOpen ? classes.contentWithClosedSidebar : ''}` }>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment