Commit 306d19a9 authored by Viacheslav Pavlov's avatar Viacheslav Pavlov
Browse files

Crop details/edit pages

parent 5b8261fa
import Crop from 'model/genesys/Crop';
import {IReducerAction} from 'model/common.model';
import {ADMIN_RECEIVE_CROP} from 'crop/constants';
import {CropService} from 'service/CropService';
import navigateTo from 'actions/navigation';
import {loadCrops} from './public';
const receiveCrop = (cropDetails: Crop): IReducerAction => ({
type: ADMIN_RECEIVE_CROP, payload: cropDetails,
});
export const loadCrop = (shortName: string) => (dispatch) => {
CropService.getCrop(shortName)
.then((details) => dispatch(receiveCrop(details)));
};
export const saveCrop = (crop: Crop) => (dispatch) => {
return CropService.saveCrop(crop)
.then((crop) => dispatch(receiveCrop(crop)));
};
export const deleteCrop = (crop: Crop) => (dispatch) => {
return CropService.deleteCrop(crop.shortName)
.then(() => {
dispatch(loadCrops());
dispatch(navigateTo('/c'));
});
};
import Crop from 'model/genesys/Crop';
import {CropService} from 'service/CropService';
import {LOAD_CROPS_CACHE_IDLE, RECEIVE_CROPS} from 'crop/constants';
import {LOAD_CROPS_CACHE_IDLE, RECEIVE_CROPS, RECEIVE_CROP_DETAILS} from 'crop/constants';
import {IReducerAction} from 'model/common.model';
import {log} from 'utilities/debug';
import CropDetails from 'model/genesys/CropDetails';
const receiveCrops = (crops: Crop[]): IReducerAction => ({
type: RECEIVE_CROPS, payload: crops,
});
const receiveCropDetails = (cropDetails: CropDetails): IReducerAction => ({
type: RECEIVE_CROP_DETAILS, payload: cropDetails,
});
export const loadCropDetails = (shortName: string, locale: string) => (dispatch) => {
CropService.getCropDetails(locale, shortName)
.then((details) => dispatch(receiveCropDetails(details)));
};
export const loadCrops = (forceReload: boolean = false) => (dispatch, getState) => {
......@@ -26,23 +35,3 @@ export const loadCrops = (forceReload: boolean = false) => (dispatch, getState)
});
}
};
export const saveCrop = (crop: Crop) => (dispatch, getState) => {
return CropService.saveCrop(crop)
.then(() => {
dispatch(loadCrops(true));
}).catch((error) => {
log('Save error', error);
});
};
export const deleteCrop = (crop: Crop) => (dispatch, getState) => {
return CropService.deleteCrop(crop.shortName)
.then(() => {
dispatch(loadCrops(true));
})
.catch((error) => {
log('Error', error);
});
};
......@@ -4,5 +4,10 @@ export const DELETE_CROP = 'App/Crop/DELETE_CROP';
export const LOAD_CROPS = 'App/Crop/LOAD_CROPS';
export const RECEIVE_CROPS = 'App/Crop/RECEIVE_CROPS';
export const RECEIVE_CROP_DETAILS = 'App/Crop/RECEIVE_CROP_DETAILS';
export const ADMIN_RECEIVE_CROP = 'App/Crop/Admin/RECEIVE_CROP';
export const CROP_FORM = 'Form/CROP';
// 300000 ms = 5 min
export const LOAD_CROPS_CACHE_IDLE = 300000;
import update from 'immutability-helper';
import { IReducerAction } from 'model/common.model';
import {ADMIN_RECEIVE_CROP} from 'crop/constants';
const INITIAL_STATE = {
crop: null,
};
export default function crop(state = INITIAL_STATE, action: IReducerAction = {type: ''}) {
switch (action.type) {
case ADMIN_RECEIVE_CROP: {
return update(state, {
crop: {$set: action.payload},
});
}
default:
return state;
}
}
import { combineReducers } from 'redux';
// import admin from './admin';
import admin from './admin';
// import dashboard from './dashboard';
import root from './public';
const rootReducer = combineReducers({
// admin,
admin,
// dashboard,
public: root,
});
......
import update from 'immutability-helper';
import { IReducerAction } from 'model/common.model';
import { RECEIVE_CROPS } from 'crop/constants';
import {RECEIVE_CROP_DETAILS, RECEIVE_CROPS, ADMIN_RECEIVE_CROP} from 'crop/constants';
const INITIAL_STATE = {
list: null,
lastFetched: 0,
details: null,
};
export default function crop(state = INITIAL_STATE, action: IReducerAction = {type: ''}) {
......@@ -17,6 +18,16 @@ export default function crop(state = INITIAL_STATE, action: IReducerAction = {ty
lastFetched: {$set: Date.now()},
});
}
case ADMIN_RECEIVE_CROP: {
return update(state, {
details: {$set: null},
});
}
case RECEIVE_CROP_DETAILS: {
return update(state, {
details: {$set: action.payload},
});
}
default:
return state;
......
// Root
import CropBrowsePage from 'crop/ui/BrowsePage';
import CropDisplayPage from 'crop/ui/DisplayPage';
import CropEditPage from 'crop/ui/EditPage';
// Admin
......@@ -14,6 +15,16 @@ const rootRoutes = [
component: CropBrowsePage,
exact: true,
},
{
path: '/c/:shortName/edit',
component: CropEditPage,
exact: true,
},
{
path: '/c/edit',
component: CropEditPage,
exact: true,
},
{
path: '/c/:shortName',
component: CropDisplayPage,
......
import * as React from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import { translate } from 'react-i18next';
import withStyles from '@material-ui/core/styles/withStyles';
// Actions
import {loadCropDetails} from 'crop/actions/public';
// Models
import Crop from 'model/genesys/Crop';
import CropDetails from 'model/genesys/CropDetails';
// UI
import PageLayout, {PageContents} from 'ui/layout/PageLayout';
import PageLayout, {PageContents, PageSection} from 'ui/layout/PageLayout';
import {Properties, PropertiesItem} from 'ui/common/Properties';
import CropCard from 'crop/ui/c/CropCard';
import ContentHeader from 'ui/common/heading/ContentHeader';
import Section from 'ui/common/layout/Section';
import Loading from 'ui/common/Loading';
import PropertiesCard from 'ui/common/PropertiesCard';
/*tslint:disable*/
const styles = (theme) => ({
cropBlurb: {
padding: '16px',
columnCount: 2,
columnGap: '3rem',
'& > h1':{
fontSize: '41px',
marginTop: '22px',
marginBottom: '11px',
fontFamily: 'Roboto-Bold',
},
'& > p': {
color: '#4d4c46',
fontSize: '18px',
fontFamily: 'Roboto-Regular',
lineHeight: '30px',
margin: '0 0 30px 0',
'& ~ a': {
color: '#006db4',
textDecoration: 'none',
},
},
},
});
/*tslint:enable*/
interface IDisplayPageProps extends React.ClassAttributes<any> {
loadCropDetails: (shortName: string, lang: string) => void;
cropDetails: CropDetails;
crops: Crop[];
shortName: string;
i18n: any;
classes: any;
}
class DisplayPage extends React.Component<IDisplayPageProps, any> {
public state = {
crop: null,
};
protected static needs = [
({ params: { shortName } }) => {
return shortName ? loadCropDetails(shortName, 'en') : null;
},
];
public componentWillMount() {
const { crops, shortName } = this.props;
if (crops && shortName) {
const crop = crops.find((item) => item.shortName === shortName);
this.setState({crop});
}
}
public componentWillReceiveProps(nextProps) {
const { crops, shortName } = nextProps;
if (crops && shortName) {
const crop = crops.find((item) => item.shortName === shortName);
this.setState({crop});
const { loadCropDetails, shortName, i18n } = this.props;
const lang = i18n ? i18n.language : 'en';
if (shortName) {
loadCropDetails(shortName, lang);
}
}
public render() {
const { crop } = this.state;
const { cropDetails, shortName, classes } = this.props;
const crop = cropDetails ? cropDetails.crop : null;
return !crop ? null : (
return (
<PageLayout withFooter>
<ContentHeader title="Crop details"/>
<PageContents>
<CropCard crop={ crop }/>
</PageContents>
{ !crop || crop.shortName !== shortName ? (<Loading/>) :
<PageContents>
{ cropDetails.blurb && cropDetails.blurb.body &&
<Section title={ shortName }>
<div className={ classes.cropBlurb } dangerouslySetInnerHTML={ {__html: cropDetails.blurb.body} }/>
</Section>
}
<CropCard crop={ crop }/>
<PageSection title="General information">
<Properties>
<PropertiesItem title="Accessions in genesys" keepEmpty>{ cropDetails.accessionCount || 0 }</PropertiesItem>
</Properties>
</PageSection>
<PropertiesCard title="Other names" propertiesList={ crop.otherNames.map((otherName) => ({title: otherName})) } propertyItemProps={ {keepEmpty: true, small: true} }/>
</PageContents>
}
</PageLayout>
);
}
......@@ -54,9 +106,12 @@ class DisplayPage extends React.Component<IDisplayPageProps, any> {
const mapStateToProps = (state, ownProps) => ({
shortName: ownProps.match.params.shortName,
crops: state.crop.public.list,
cropDetails: state.crop.public.details,
});
const mapDispatchToProps = (dispatch) => bindActionCreators({}, dispatch);
const mapDispatchToProps = (dispatch) => bindActionCreators({
loadCropDetails,
}, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(DisplayPage);
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(translate()(DisplayPage)));
import * as React from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import Grid from '@material-ui/core/Grid';
import Paper from '@material-ui/core/Paper';
import {log} from 'utilities/debug';
import confirm from 'utilities/confirmAlert';
import navigateTo from 'actions/navigation';
import {deleteCrop, loadCrop, saveCrop} from 'crop/actions/admin';
import Crop from 'model/genesys/Crop';
import CropForm from './c/CropForm';
import PageLayout, {PageContents} from 'ui/layout/PageLayout';
interface ICropEditPageProps extends React.ClassAttributes<any> {
classes: any;
shortName?: string;
loadCrop: (shortName: string) => void;
saveCrop: (crop: Crop) => Promise<any>;
deleteCrop: (crop: Crop) => void;
navigateTo: (path: string, query?: string) => void;
crop: Crop;
}
class CropEditPage extends React.Component<ICropEditPageProps, any> {
protected static needs = [
({params: {shortName}}) => loadCrop(shortName),
];
private onSave = (updatedCrop: Crop) => {
const {saveCrop, navigateTo} = this.props;
saveCrop(updatedCrop)
.then(() => navigateTo(`/c/${updatedCrop.shortName}/edit`));
}
private onDelete = () => {
const {crop, deleteCrop} = this.props;
confirm(`Delete crop '${crop.name}'?`, {
description: `Note, deleting any crop causes mayhem.`,
confirmLabel: 'Delete',
abortLabel: 'Cancel',
}).then(() => {
deleteCrop(crop);
}).catch(() => {
// don't delete
});
}
public componentDidMount() {
const {crop, loadCrop, shortName} = this.props;
if (shortName && (!crop || crop.shortName !== shortName)) {
loadCrop(shortName);
}
}
public render() {
const {shortName} = this.props;
let {crop} = this.props;
if (!crop && !shortName) {
crop = new Crop();
}
if (!crop) {
log('No crop.');
return null;
}
return (
<PageLayout>
<PageContents>
<Grid item xs={ 12 }>
<Paper className="p-20 mb-10">
<CropForm initialValues={ crop } onSubmit={ this.onSave } onDelete={ this.onDelete }/>
</Paper>
</Grid>
</PageContents>
</PageLayout>
);
}
}
const mapStateToProps = (state, ownProps) => ({
shortName: ownProps.match.params.shortName,
crop: state.crop.admin.crop,
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
loadCrop,
saveCrop,
deleteCrop,
navigateTo,
}, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(CropEditPage);
......@@ -5,11 +5,13 @@ import {Link} from 'react-router-dom';
import Crop from 'model/genesys/Crop';
// UI
import Card, {CardHeader, CardContent} from 'ui/common/Card';
import Card, {CardHeader, CardContent, CardActions} from 'ui/common/Card';
import {Properties, PropertiesItem} from 'ui/common/Properties';
import PrettyDate from 'ui/common/time/PrettyDate';
import Grid from '@material-ui/core/Grid';
import withStyles from '@material-ui/core/styles/withStyles';
import Button from '@material-ui/core/Button';
import Authorize from 'ui/common/authorized/Authorize';
const style = (theme) => ({
root: {
......@@ -53,6 +55,13 @@ const CropCard = ({crop, classes, compact = false, edit = false, ...other}: { cr
<PropertiesItem title="Last modified"><PrettyDate value={ new Date(crop.lastModifiedDate) }/></PropertiesItem>
</Properties>
</CardContent>
<Authorize role="ROLE_ADMINISTRATOR">
<CardActions>
<Link to={ `/c/${crop.shortName}/edit` }>
<Button variant="raised">Edit</Button>
</Link>
</CardActions>
</Authorize>
</Card>
</Grid>
);
......
import * as React from 'react';
import {Link} from 'react-router-dom';
import {Field, reduxForm} from 'redux-form';
import Button from '@material-ui/core/Button';
import {CROP_FORM} from 'crop/constants';
import {TextField} from 'ui/common/text-field';
import {log} from 'utilities/debug';
import Validators from 'utilities/Validators';
import ItemsEditor from 'ui/common/ItemsEditor';
import Divider from '@material-ui/core/Divider';
const stringField = (member, index, fields, itemLabel) => (
<Field required name={ `${member}` } type="text" component={ TextField } label={ itemLabel } validate={ [Validators.required] }/>
);
const onAddString = () => {
log('Adding new string item');
return '';
};
const onRemoveString = (item) => {
log('Removing string', item);
};
const CropForm = ({error, handleSubmit, initialValues, onDelete}) => {
return (
<form onSubmit={ handleSubmit }>
{ initialValues && initialValues.version && <div>Version: { initialValues.version || 0 }</div> }
<Field disabled={ initialValues.shortName } required name="shortName" label="Crop code" component={ TextField } validate={ [Validators.required, Validators.maxLength20] }/>
<Field required name="name" label="Crop title" component={ TextField } validate={ [Validators.required] }/>
<div className="pb-20 pt-20">
<h4>Other names </h4>
<ItemsEditor name="otherNames" itemLabel="Other names" addItem={ onAddString } removeItem={ onRemoveString } component={ stringField }/>
</div>
<div>{ error && <strong>{ error }</strong> }</div>
<Divider/>
<div className="pt-20">
<Button variant="raised" type="submit">Save changes</Button>
{ (initialValues._permissions && initialValues._permissions.delete) && <Button onClick={ onDelete } type="button">Delete</Button> }
<Link to={ `/c/${initialValues.shortName}` }><Button type="button">Back</Button></Link>
</div>
</form>
);
};
export default reduxForm({
enableReinitialize: true,
destroyOnUnmount: false,
form: CROP_FORM,
})(CropForm);
import Article from 'model/cms/Article';
import Crop from 'model/genesys/Crop';
import RepositoryImage from 'model/repository/RepositoryImage';
/*
* Defined in Swagger as '#/definitions/CropDetails'
*/
class CropDetails {
public accessionCount: number;
public blurb: Article;
public covers: RepositoryImage[];
public crop: Crop;
public overview: any;
}
export default CropDetails;
......@@ -2,10 +2,13 @@ import * as UrlTemplate from 'url-template';
import { axiosBackend } from 'utilities/requestUtils';
import Crop from 'model/genesys/Crop';
import CropDetails from 'model/genesys/CropDetails';
const URL_LIST_CROPS = UrlTemplate.parse(`/api/v0/crops`);
const URL_SAVE_CROP = `/api/v0/crops/save`;
const URL_DELETE_CROP = UrlTemplate.parse(`/api/v0/crops/{shortName}`);
const URL_LIST_CROPS = UrlTemplate.parse(`/api/v1/crops`);
const URL_SAVE_CROP = `/api/v1/crops/save`;
const URL_DELETE_CROP = UrlTemplate.parse(`/api/v1/crops/{shortName}`);
const URL_GET_CROP = UrlTemplate.parse(`/api/v1/crops/{shortName}`);
const URL_GET_CROP_DETAILS = UrlTemplate.parse(`/api/v1/crops/{shortName}/details/{lang}`);
export class CropService {
......@@ -46,6 +49,25 @@ export class CropService {
}).then(({ data }) => data as Crop);
}
/**
* getCrop at /api/v1/crops/{shortName}
*
* @param shortName shortName
*/
public static getCrop(shortName: string): Promise<Crop> {
const apiUrl = URL_GET_CROP.expand({shortName});
// console.log(`Fetching from ${apiUrl}`);
const content = { /* No content in request body */ };
return axiosBackend({
url: apiUrl,
method: 'GET',
...content,
}).then(({ data }) => data as Crop);
}
/**
* deleteCrop at /api/v0/crops/{shortName}
*
......@@ -65,4 +87,23 @@ export class CropService {
}).then(({ data }) => data as Crop);
}
/**
* getCropDetails at /api/v1/crops/{shortName}/details/{lang}
*
* @param lang lang
* @param shortName shortName
*/
public static getCropDetails(lang: string, shortName: string): Promise<CropDetails> {
const apiUrl = URL_GET_CROP_DETAILS.expand({lang, shortName});
// console.log(`Fetching from ${apiUrl}`);
const content = { /* No content in request body */ };
return axiosBackend({
url: apiUrl,
method: 'GET',
...content,
}).then(({ data }) => data as CropDetails);
}
}
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