Commit 02f3cfc5 authored by Matija Obreza's avatar Matija Obreza
Browse files

Enhanced Crop pages and management

- `<CropCard`
- Delete a crop
- Wrapper for /crops pages
- Only ROLE_ADMIN can access crop edit form
parent 7795c6d1
Pipeline #4002 passed with stages
in 2 minutes and 42 seconds
......@@ -5,8 +5,10 @@ import {CREATE_CROP, GET_CROP, RECEIVE_CROP, RECEIVE_CROPS} from 'constants/crop
import {IReducerAction} from 'model/common.model';
export const createCrop = () => (dispatch) => {
dispatch({type: CREATE_CROP});
return dispatch(push('/dashboard/crops/edit'));
return Promise.all([
dispatch({type: CREATE_CROP}),
dispatch(push('/crops/create')),
]);
};
const receiveCrop = (crop: Crop): IReducerAction => ({
......@@ -32,11 +34,11 @@ export const loadCrop = (uuid: string) => (dispatch, getState) => {
export const editCrop = (uuid: string) => (dispatch, getState) => {
dispatch(loadCrop(uuid));
dispatch(push(`/dashboard/crops/${uuid}/edit`));
dispatch(push(`/crops/${uuid}/edit`));
};
// const showCrop = (uuid: string) => (dispatch) => {
// dispatch(push(`/dashboard/crops/${uuid}`));
// dispatch(push(`/crops/${uuid}`));
// };
export const listCrops = () => (dispatch, getState) => {
......@@ -55,7 +57,7 @@ export const saveCrop = (crop: Crop) => (dispatch, getState) => {
return CropService.saveCrop(getState().login.access_token, crop)
.then((saved) => {
dispatch(receiveCrop(saved));
dispatch(push('/dashboard/crops'));
dispatch(push('/crops'));
}).catch((error) => {
console.log('Save error', error);
});
......@@ -64,7 +66,7 @@ export const saveCrop = (crop: Crop) => (dispatch, getState) => {
export const deleteCrop = (crop: Crop) => (dispatch, getState) => {
return CropService.deleteCrop(getState().login.access_token, crop)
.then((crop) => {
dispatch(push('/dashboard/crops'));
dispatch(push('/crops'));
})
.catch((error) => {
console.log('Error', error);
......
import * as update from 'immutability-helper';
import {IReducerAction} from 'model/common.model';
import {Crop} from 'model/crop.model';
import {CREATE_CROP, RECEIVE_CROP, RECEIVE_CROPS} from 'constants/crop';
import { IReducerAction } from 'model/common.model';
import { Crop } from 'model/crop.model';
import { CREATE_CROP, RECEIVE_CROP, RECEIVE_CROPS } from 'constants/crop';
const INITIAL_STATE = {
currentCrop: null,
......
......@@ -34,13 +34,10 @@ export class CropService {
}
public static deleteCrop(token: string, crop: Crop): Promise<Crop> {
console.log(`Deleting crop ${crop.uuid}`);
return authenticatedRequest(token, {
url: `${API_URL}/${crop.uuid},${crop.version}`,
method: 'DELETE',
data: {
...crop,
},
}).then(({data}) => new Crop(data));
}
......
......@@ -22,6 +22,7 @@ interface IContentHeaderWithButtonProps extends React.ClassAttributes<any> {
title: string;
buttonName: string;
buttonUrl: string;
buttons: any;
}
class ContentHeaderWithButton extends React.Component<IContentHeaderWithButtonProps, any> {
......@@ -32,7 +33,7 @@ class ContentHeaderWithButton extends React.Component<IContentHeaderWithButtonPr
public render() {
const {classes, title, buttonName, buttonUrl} = this.props;
const {classes, title, buttonName, buttonUrl, buttons} = this.props;
return (
<Grid container spacing={ 0 }>
......@@ -52,6 +53,7 @@ class ContentHeaderWithButton extends React.Component<IContentHeaderWithButtonPr
) : null
}
{ buttons && buttons }
</Grid>
</Grid>
);
......
import * as React from 'react';
import {withStyles} from 'material-ui/styles';
import { Link } from 'react-router';
import Button from 'material-ui/Button';
import Menu, { MenuItem } from 'material-ui/Menu';
......@@ -39,16 +40,6 @@ class UserMenuComponent extends React.Component<IUserMenuComponentProps, any> {
this.setState({ open: false });
}
public goToAdministratorDashboard = () => {
this.props.router.push('/dashboard');
this.handleRequestClose();
}
public goToGuiTesting = () => {
this.props.router.push('/gui');
this.handleRequestClose();
}
public render() {
const { classes, logoutRequest, userName } = this.props;
......@@ -68,9 +59,10 @@ class UserMenuComponent extends React.Component<IUserMenuComponentProps, any> {
open={ this.state.open }
onClose={ this.handleRequestClose }
>
<MenuItem onClick={ this.goToAdministratorDashboard } >My Dashboard</MenuItem>
<Link to="/dashboard"><MenuItem >My Dashboard</MenuItem></Link>
<Link to="/crops"><MenuItem>Crops</MenuItem></Link>
<Link to="/gui"><MenuItem>UI Tests</MenuItem></Link>
<MenuItem onClick={ logoutRequest } >Logout</MenuItem>
<MenuItem onClick={ this.goToGuiTesting }>UI Tests</MenuItem>
</Menu>
</div>
);
......
import * as React from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {withStyles} from 'material-ui/styles';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { withStyles } from 'material-ui/styles';
import Grid from 'material-ui/Grid';
import Card, {CardHeader, CardContent} from 'material-ui/Card';
import Divider from 'material-ui/Divider';
import {Link} from 'react-router';
import {listCrops} from 'actions/crop';
import {Crop} from 'model/crop.model';
import { listCrops, createCrop } from 'actions/crop';
import { Crop } from 'model/crop.model';
import ContentHeaderWithButton from 'ui/common/heading/ContentHeaderWithButton';
import CropCard from './c/CropCard';
import Button from 'material-ui/Button';
const styles = (theme) => ({
card: {
......@@ -21,6 +22,7 @@ interface IBrowsePageProps extends React.ClassAttributes<any> {
classes: any;
crops?: Crop[];
listCrops: any;
createCrop: any;
}
class BrowsePage extends React.Component<IBrowsePageProps, any> {
......@@ -34,28 +36,19 @@ class BrowsePage extends React.Component<IBrowsePageProps, any> {
}
public render() {
const {classes, crops} = this.props;
const {classes, crops, createCrop} = this.props;
return (
<div>
<ContentHeaderWithButton title="What do you want to do?" buttons={ <Button raised onClick={ createCrop }>Add crop</Button> } />
<Grid container>
{ crops &&
crops.map((e, i) => (
<Grid item xs={ 12 } md={ 2 }>
<Card key={ i } className={ classes.card }>
<CardHeader title={ (
<Link to={ `/dashboard/crops/${e.uuid}/edit` }>
{ e.title }
</Link>)
}/>
<Divider/>
<CardContent>
{ e.code }
</CardContent>
</Card>
</Grid>
))
}
{ crops && crops.map((crop: Crop) => (
<Grid key={ `${crop.code}` } item xs={ 12 } md={ 2 }>
<CropCard className={ classes.card } crop={ crop } />
</Grid>
)) }
</Grid>
</div>
);
}
}
......@@ -66,6 +59,7 @@ const mapStateToProps = (state, ownProps) => ({
const mapDispatchToProps = (dispatch) => bindActionCreators({
listCrops,
createCrop,
}, dispatch);
const styled = withStyles(styles)(BrowsePage);
......
import * as React from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {withStyles} from 'material-ui/styles';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { withStyles } from 'material-ui/styles';
import Grid from 'material-ui/Grid';
import Paper from 'material-ui/Paper';
import {loadCrop, saveCrop} from 'actions/crop';
import {Crop} from 'model/crop.model';
import { loadCrop, saveCrop, deleteCrop } from 'actions/crop';
import { Crop } from 'model/crop.model';
import CropForm from './c/CropForm';
interface IDescriptorEditPageProps extends React.ClassAttributes<any> {
......@@ -14,6 +14,7 @@ interface IDescriptorEditPageProps extends React.ClassAttributes<any> {
uuid?: string;
loadCrop: (uuid: string) => void;
saveCrop: (crop: Crop) => void;
deleteCrop: (crop: Crop) => void;
crop: Crop;
}
......@@ -42,8 +43,14 @@ class CropEditPage extends React.Component<IDescriptorEditPageProps, any> {
}
}
public onSave = (updatedCrop: Crop) => {
this.props.saveCrop(updatedCrop);
private onSave = (updatedCrop: Crop) => {
const { saveCrop } = this.props;
saveCrop(updatedCrop);
}
private onDelete = () => {
const { crop, deleteCrop } = this.props;
deleteCrop(crop);
}
public render() {
......@@ -63,7 +70,7 @@ class CropEditPage extends React.Component<IDescriptorEditPageProps, any> {
<Grid container className={ classes.contentContainer }>
<Grid item xs={ 12 }>
<Paper className={ classes.form }>
<CropForm initialValues={ crop } onSubmit={ this.onSave }/>
<CropForm initialValues={ crop } onSubmit={ this.onSave } onDelete={ this.onDelete } />
</Paper>
</Grid>
</Grid>
......@@ -80,6 +87,7 @@ const mapStateToProps = (state, ownProps) => ({
const mapDispatchToProps = (dispatch) => bindActionCreators({
loadCrop,
saveCrop,
deleteCrop,
}, dispatch);
const styled = withStyles(styles)(CropEditPage);
......
import * as React from 'react';
import ContentHeader from 'ui/common/heading/ContentHeader';
const Wrapper = ({children}: any) => (
<div>
<ContentHeader title="Crops" subTitle="We give priority to some and ignore others" />
{ children }
</div>
);
export default Wrapper;
import * as React from 'react';
import { Crop } from 'model/crop.model';
import { Link } from 'react-router';
import Card, {CardHeader, CardContent} from 'material-ui/Card';
import Divider from 'material-ui/Divider';
const CropCard = ({crop, className = ''}: {crop: Crop, className: string}) => {
if (! crop) {
return null;
}
return (
<Card className={ className }>
<CardHeader title={
crop._permissions.write ?
<Link to={ `/crops/${crop.uuid}/edit` }>{ crop.title }</Link>
: <span>{ crop.title }</span>
} />
<Divider/>
<CardContent>
{ crop.code }
</CardContent>
</Card>
);
};
export default CropCard;
......@@ -6,7 +6,7 @@ import Button from 'material-ui/Button';
import {CROP_FORM} from 'constants/crop';
import {TextField} from 'ui/common/text-field';
const CropForm = ({error, handleSubmit, initialValues}) => {
const CropForm = ({error, handleSubmit, initialValues, onDelete}) => {
return (
<form onSubmit={ handleSubmit }>
......@@ -17,7 +17,8 @@ const CropForm = ({error, handleSubmit, initialValues}) => {
<div>{ error && <strong>{ error }</strong> }</div>
<Button raised type="submit">Save changes</Button>
<Link to="/dashboard/crops"><Button type="button">Back to dashboard</Button></Link>
{ (initialValues._permissions && initialValues._permissions.delete) && <Button onClick={ onDelete } type="button">Delete</Button> }
<Link to="/crops"><Button type="button">Back to crop list</Button></Link>
</form>
);
};
......
import * as React from 'react';
import {connect} from 'react-redux';
import {withStyles} from 'material-ui/styles';
import { connect } from 'react-redux';
import { withStyles } from 'material-ui/styles';
import Grid from 'material-ui/Grid';
import {bindActionCreators} from 'redux';
import { bindActionCreators } from 'redux';
import {createPartner} from 'actions/partner';
import {createDataset, listMyDatasets} from 'actions/dataset';
import {listMyDescriptors, createDescriptor} from 'actions/descriptors';
import {listMyDescriptorLists, createDescriptorList} from 'actions/descriptorList';
import {Page} from 'model/common.model';
import { createPartner } from 'actions/partner';
import { createDataset, listMyDatasets } from 'actions/dataset';
import { listMyDescriptors, createDescriptor } from 'actions/descriptors';
import { listMyDescriptorLists, createDescriptorList } from 'actions/descriptorList';
import { Page } from 'model/common.model';
import { Dataset } from 'model/dataset.model';
import { Descriptor, DescriptorList } from 'model/descriptors.model';
import ContentHeaderWithButton from 'ui/common/heading/ContentHeaderWithButton';
import {BaseMyDataPage} from './MyDataPage';
import { BaseMyDataPage } from './MyDataPage';
import DashboardButton from './c/DashboardButton';
import MyDataTable from './c/MyDataTable';
const styles = {
root: {
width: '100%',
},
content: {
backgroundColor: '#D4D2C6',
},
bigNavButtonWrapper: {
padding: '10px 10px 10px 10px',
},
bigNavButton: {
width: '100%',
backgroundColor: '#88BC40',
color: '#fff',
},
root: {
width: '100%',
},
content: {
backgroundColor: '#D4D2C6',
},
bigNavButtonWrapper: {
padding: '10px 10px 10px 10px',
},
bigNavButton: {
width: '100%',
backgroundColor: '#88BC40',
color: '#fff',
},
};
interface IAdminDashProps extends React.ClassAttributes<any> {
classes: any;
createDataset: any;
createPartner: any;
createDescriptor: any;
createDescriptorList: any;
classes: any;
createDataset: any;
createPartner: any;
createDescriptor: any;
createDescriptorList: any;
}
class AdministrationDashboard extends BaseMyDataPage<IAdminDashProps> {
constructor(props) {
super(props);
}
public goToNewDataset = () => {
this.props.createDataset();
}
public goToNewDescriptorForm = () => {
this.props.router.push('/descriptor/edit');
}
public goToDataInPreparation = () => {
this.props.router.push('/dashboard/data-in-preparation');
}
public goToPartners = () => {
this.props.router.push('/partners');
}
public goToPublishedData = () => {
this.props.router.push('/dashboard/data-published');
}
protected onPaginationChange = (page, results, sortBy, dir) => {
/*console.log('onPaginationChange', page, results, sortBy);*/
const {router, router: { location }} = this.props;
location.query.p = page;
location.query.l = results;
location.query.s = sortBy;
location.query.d = dir;
router.push(location);
}
public render() {
const {classes, tab, datasets, descriptors, descriptorLists, createPartner, createDescriptor, createDescriptorList} = this.props;
let paged: Page<any> = datasets as Page<Dataset>;
switch (tab) {
case 'descriptorlists': paged = descriptorLists as Page<DescriptorList>; break;
case 'descriptors': paged = descriptors as Page<Descriptor>; break;
case 'datasets':
default: break;
}
return (
<div>
<ContentHeaderWithButton title="What do you want to do?"/>
<div className={ classes.content }>
<Grid container spacing={ 0 }>
<Grid item xs={ 6 } lg={ 3 }>
<DashboardButton title="CREATE DATASET" onClickFunction={ this.goToNewDataset }/>
</Grid>
<Grid item xs={ 6 } lg={ 3 }>
<DashboardButton title="CREATE DESCRIPTOR" onClickFunction={ createDescriptor }/>
</Grid>
<Grid item xs={ 6 } lg={ 3 }>
<DashboardButton title="CREATE DESCRIPTOR LIST" onClickFunction={ createDescriptorList }/>
</Grid>
<Grid item xs={ 6 } lg={ 3 }>
<DashboardButton title="CREATE PARTNER" onClickFunction={ createPartner }/>
</Grid>
</Grid>
<Grid container spacing={ 0 }>
<Grid item xs={ 12 } md={ 6 }>
<DashboardButton title="VIEW PUBLISHED" subTitle="DATASET OR CROP DESCRIPTOR" onClickFunction={ this.goToPublishedData }/>
</Grid>
<Grid item xs={ 12 } md={ 6 }>
<DashboardButton title="RESUME ONGOING" subTitle="DATASET OR CROP DESCRIPTOR" onClickFunction={ this.goToDataInPreparation }/>
</Grid>
</Grid>
</div>
<MyDataTable tab={ tab } basePath="/dashboard"
paged={ paged } onPaginationChange={ this.onPaginationChange } />
</div>
);
}
constructor(props) {
super(props);
}
public goToNewDataset = () => {
this.props.createDataset();
}
public goToNewDescriptorForm = () => {
this.props.router.push('/descriptor/edit');
}
public goToDataInPreparation = () => {
this.props.router.push('/dashboard/data-in-preparation');
}
public goToPartners = () => {
this.props.router.push('/partners');
}
public goToPublishedData = () => {
this.props.router.push('/dashboard/data-published');
}
protected onPaginationChange = (page, results, sortBy, dir) => {
/*console.log('onPaginationChange', page, results, sortBy);*/
const {router, router: { location }} = this.props;
location.query.p = page;
location.query.l = results;
location.query.s = sortBy;
location.query.d = dir;
router.push(location);
}
public render() {
const {classes, tab, datasets, descriptors, descriptorLists, createPartner, createDescriptor, createDescriptorList} = this.props;
let paged: Page<any> = datasets as Page<Dataset>;
switch (tab) {
case 'descriptorlists': paged = descriptorLists as Page<DescriptorList>; break;
case 'descriptors': paged = descriptors as Page<Descriptor>; break;
case 'datasets':
default: break;
}
return (
<div>
<ContentHeaderWithButton title="What do you want to do?"/>
<div className={ classes.content }>
<Grid container spacing={ 0 }>
<Grid item xs={ 6 } lg={ 3 }>
<DashboardButton title="CREATE DATASET" onClickFunction={ this.goToNewDataset }/>
</Grid>
<Grid item xs={ 6 } lg={ 3 }>
<DashboardButton title="CREATE DESCRIPTOR" onClickFunction={ createDescriptor }/>
</Grid>
<Grid item xs={ 6 } lg={ 3 }>
<DashboardButton title="CREATE DESCRIPTOR LIST" onClickFunction={ createDescriptorList }/>
</Grid>
<Grid item xs={ 6 } lg={ 3 }>
<DashboardButton title="CREATE PARTNER" onClickFunction={ createPartner }/>
</Grid>
</Grid>
<Grid container spacing={ 0 }>
<Grid item xs={ 12 } md={ 6 }>
<DashboardButton title="VIEW PUBLISHED" subTitle="DATASET OR CROP DESCRIPTOR" onClickFunction={ this.goToPublishedData }/>
</Grid>
<Grid item xs={ 12 } md={ 6 }>
<DashboardButton title="RESUME ONGOING" subTitle="DATASET OR CROP DESCRIPTOR" onClickFunction={ this.goToDataInPreparation }/>
</Grid>
</Grid>
</div>
<MyDataTable tab={ tab } basePath="/dashboard"
paged={ paged } onPaginationChange={ this.onPaginationChange } />
</div>
);
}
}
const mapStateToProps = (state, ownProps) => ({
preFilter: ownProps.route.filter || {}, // route-configured
tab: ownProps.params.tab || 'datasets', // current tab, or ownProps.location.pathname
pageCurrent: +ownProps.location.query.p || 0, // current page
pageSize: +ownProps.location.query.l || 20, // page size
pageSort: ownProps.location.query.s, // page sort
pageDir: ownProps.location.query.d, // page sort direction
datasets: state.datasets.paged,
descriptors: state.pagination.descriptorPage,