Commit af91f42b authored by Matija Obreza's avatar Matija Obreza
Browse files

Merge branch '8-submitting-requests' into 'master'

Resolve "Submitting requests"

Closes #8

See merge request !9
parents 9606ef40 4aa982cb
......@@ -32,17 +32,19 @@
"analyze": "webpack --config config/webpack-analyze.config.js"
},
"dependencies": {
"d3": "^6.2.0",
"d3": "^6.0.0",
"es-cookie": "^1.0.0",
"history": "^4.0.0",
"i18next": "^19.0.0",
"react": "^16.0.0",
"react-dom": "^16.0.0",
"react-google-recaptcha": "^2.0.0",
"react-i18next": "^11.0.0",
"react-router-dom": "^5.0.0"
},
"devDependencies": {
"@types/react": "^16.0.0",
"@types/react-router-dom": "^5.0.0",
"@typescript-eslint/eslint-plugin": "^2.0.0",
"@typescript-eslint/eslint-plugin-tslint": "^2.0.0",
"@typescript-eslint/parser": "^2.0.0",
......
import React from 'react';
import { AccessionService } from '@genesys/client/service';
import { Property } from './Property';
import { Property } from 'ui/common/Property';
import AccessionDetails from '@genesys/client/model/accession/AccessionDetails';
import { WithTranslation, withTranslation } from 'react-i18next';
......
......@@ -6,7 +6,7 @@ import { AccessionService } from '@genesys/client/service';
import AccessionFilter from '@genesys/client/model/accession/AccessionFilter';
import Accession from '@genesys/client/model/accession/Accession';
import FilteredPage, { IPageRequest } from '@genesys/client/model/FilteredPage';
import Pagination from './Pagination';
import Pagination from 'ui/common/Pagination';
import { AccessionFilters } from './AccessionFilters';
import { withTranslation, WithTranslation } from 'react-i18next';
import { LocalStorageCart } from 'utilities';
......
......@@ -2,7 +2,7 @@ import * as React from 'react';
import { withTranslation, WithTranslation } from 'react-i18next';
// model
import AccessionOverview from '@genesys/client/model/accession/AccessionOverview';
import PropertiesCard from 'ui/PropertiesCard';
import PropertiesCard from 'ui/common/PropertiesCard';
import { VocabularyService } from '@genesys/client/service';
interface IAccessionOverviewPageState {
......
......@@ -5,10 +5,10 @@ import { log } from '@genesys/client/utilities/debug';
// import * as cookies from 'es-cookie';
import { reconfigureServiceAxios, LoginService } from '@genesys/client/service';
import App from './ui/App';
import ApiAccessError from './ui/ApiAccessError';
import App from 'ui/core/App';
import ApiAccessError from 'ui/core/ApiAccessError';
import { Config, DefaultConfig } from '../config/config';
import OverviewPage from './ui/OverviewPage';
import OverviewPage from 'accession/OverviewPage';
// declare const window: Window & { devToolsExtension: any, __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: any, initialLanguage: any, initialI18nStore: any, localeMapping: any };
// const AUTH_COOKIE = 'GENESYS_AUTH';
......
......@@ -7,7 +7,8 @@
"last": "Last"
},
"error": {
"notFound": "Not Found"
"notFound": "Not Found",
"errorHappened": "Error happened while processing request"
},
"estimatedNumberOfItems": "{{count, number}} {{what, lowercase}}",
"accession": {
......@@ -159,6 +160,39 @@
"title": "Shopping cart",
"addToCart": "Add to cart",
"removeFromCart": "Remove from cart",
"isEmpty": "Shopping cart is empty"
"isEmpty": "Shopping cart is empty",
"request": "Request material"
},
"request": {
"title": "Personal information",
"submit": "Submit request",
"internalExplanation": "If you are requesting material from your own institute please flag the request as internal.",
"userData": {
"name": "Name",
"surname": "Surname",
"email": "E-mail address",
"address": "Address",
"country": "ISO3 Country code",
"telephone": "Telephone",
"orgName": "Organization name"
},
"requestInfo": {
"email": "Your e-mail address as registered in Easy-SMTA",
"notes": "Additional notes to submit with your request",
"internalRequest": "Internal request (no SMTA required)",
"internalExplanation": "If you are requesting material from your own institute please flag the request as internal.",
"additionalInfo": "Any additional information about your request is highly appreciated",
"internal": "Internal request (no SMTA required)",
"preacceptSMTA": {
"label": "SMTA/MTA acceptance",
"yes": "I will accept the terms and conditions of SMTA/MTA",
"no": "I will NOT accept the terms and conditions of SMTA/MTA"
},
"purposeType": {
"label": "Specify use of material:",
"0": "Other (please elaborate in Notes field)",
"1": "Research for food and agriculture"
}
}
}
}
......@@ -5,20 +5,20 @@ import AccessionFilter from '@genesys/client/model/accession/AccessionFilter';
import Accession from '@genesys/client/model/accession/Accession';
import { withTranslation, WithTranslation } from 'react-i18next';
import { LocalStorageCart } from 'utilities';
import { Link } from 'react-router-dom';
import { Link, RouteComponentProps } from 'react-router-dom';
interface IAccessionListPageState {
interface ICartPageState {
accessions: Array<Accession & Record<string, any>>;
isEmpty: boolean;
selected: string[];
isAllSelected: boolean;
}
interface IAccessionListPageProps extends WithTranslation {
interface ICartPageProps extends WithTranslation, RouteComponentProps {
filter?: AccessionFilter;
}
export class AccessionListPage extends React.Component<IAccessionListPageProps, IAccessionListPageState> {
class CartPage extends React.Component<ICartPageProps, ICartPageState> {
public constructor(props) {
super(props);
......@@ -100,6 +100,11 @@ export class AccessionListPage extends React.Component<IAccessionListPageProps,
}));
};
private onRequest = () => {
const { history } = this.props;
history.push('/request');
};
public render() {
const { accessions, selected, isEmpty, isAllSelected } = this.state;
const { t } = this.props;
......@@ -110,11 +115,15 @@ export class AccessionListPage extends React.Component<IAccessionListPageProps,
<>
<h1 className="d-flex justify-content-between align-items-center">
{ t('cart.title') }
{ selected.length !== 0 &&
{ selected.length === 0 ? (
<button type="button" className="btn btn-primary" onClick={ this.onRequest }>
{ t('cart.request') }
</button>
) : (
<button type="button" className="btn btn-primary" onClick={ this.removeItems }>
{ t('cart.removeFromCart') }
</button>
}
) }
</h1>
{ isEmpty && <div>{ t('cart.isEmpty') }</div> }
{ accessions && (
......@@ -162,4 +171,4 @@ export class AccessionListPage extends React.Component<IAccessionListPageProps,
};
}
export default withTranslation()(AccessionListPage)
export default withTranslation()(CartPage)
import React from 'react';
import { withTranslation, WithTranslation } from 'react-i18next';
import { RouteComponentProps } from 'react-router-dom';
import ErrorDisplay from 'ui/common/ErrorDisplay';
import { ApiInfoService, RequestService } from '@genesys/client/service';
import { LocalStorageCart } from 'utilities';
import EasySMTAUserData from '@genesys/client/model/request/EasySMTAUserData';
import RequestInfo from '@genesys/client/model/request/RequestInfo';
import CaptchaInput from 'ui/common/CaptchaInput';
interface IRequestPageState {
apiError: string | Error;
requestInfo: Partial<RequestInfo>;
userData: Partial<EasySMTAUserData>;
captcha: string;
captchaSiteKey: string;
}
interface IRequestPageProps extends WithTranslation, RouteComponentProps {}
class RequestPage extends React.Component<IRequestPageProps, IRequestPageState> {
public constructor(props) {
super(props);
this.state = {
apiError: null,
requestInfo: {},
userData: {
// pid: 'Internal', // for now these values are being added to the request in RequestService.initiateRequest method
// type: 'in',
},
captcha: null,
captchaSiteKey: null,
}
}
public componentDidMount() {
ApiInfoService
.apiInfo()
.then((data) => {
if (data.captchaSiteKey) {
return this.setState({ captchaSiteKey: data.captchaSiteKey });
}
this.setState({ apiError: 'Api info call failed' });
})
.catch((e) => {
console.log('Api info call failed: ', e);
this.setState({ apiError: 'Api info call failed' });
});
}
private onRequestDataChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.currentTarget as HTMLInputElement;
this.setState({ requestInfo: { ...this.state.requestInfo, [name]: value } });
};
private onUserDataChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.currentTarget as HTMLInputElement;
this.setState({ userData: { ...this.state.userData, [name]: value } });
};
private onSubmit = (e) => {
e.preventDefault();
const { t } = this.props;
const { requestInfo, userData, captcha } = this.state;
const UUIDs = LocalStorageCart.getCartItemsLS();
if (UUIDs.length === 0) {
this.setState({ apiError: t('cart.isEmpty') });
return;
}
let info = { ...requestInfo };
if (requestInfo.internalRequest) {
info.userData = userData as EasySMTAUserData;
info.email = null;
} else {
info = { ...requestInfo };
info.email = userData.email;
// delete info.internalRequest;
}
// console.log('submit: ', UUIDs, info);
RequestService
.initiateRequest(UUIDs, info as RequestInfo, captcha)
.then((data) => {
console.log('request submitted', data);
// clear cart
})
.catch((e) => {
console.log('request failed: ', e);
this.setState({ apiError: (e.data && e.data.error) || e.statusText || e });
});
};
private onCheckboxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { checked } = e.currentTarget as HTMLInputElement;
this.setState({ requestInfo: { ...this.state.requestInfo, internalRequest: checked } })
};
private onPurposeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.currentTarget as HTMLInputElement;
this.setState({ requestInfo: { ...this.state.requestInfo, purposeType: +value } })
};
private onSMTAChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.currentTarget as HTMLInputElement;
this.setState({ requestInfo: { ...this.state.requestInfo, preacceptSMTA: !!+value } });
};
private onCaptchaChange = (captchaResponse: string) => {
if (captchaResponse) {
this.setState({ captcha: captchaResponse });
}
};
public render() {
const { t } = this.props;
const { apiError, requestInfo, userData, captcha, captchaSiteKey } = this.state;
const { internalRequest } = requestInfo;
const getValueRequest = (name: string) => requestInfo[name] || '';
const getValueUser = (name: string) => userData[name] || '';
return (
<>
<h1>{ t('request.title') }</h1>
<form onSubmit={ this.onSubmit }>
<div className="form-group">
<label htmlFor="email">
{ t(`${internalRequest ? 'request.userData' : 'request.requestInfo'}.email`) }
</label>
<input
id="email"
className="form-control"
name="email"
type="email"
onChange={ this.onUserDataChange }
value={ getValueUser('email') }
required
/>
</div>
<div className="form-group">
<p><strong>{ t('request.internalExplanation') }</strong></p>
<div className="form-check">
<input
id="internalRequest"
className="form-check-input"
name="internalRequest"
type="checkbox"
onChange={ this.onCheckboxChange }
checked={ !!internalRequest }
/>
<label htmlFor="internalRequest" className="form-check-label">
{ t('request.requestInfo.internal') }
</label>
</div>
</div>
{ internalRequest && (
<>
<div className="form-group">
<label htmlFor="name">{ t('request.userData.name') }</label>
<input
className="form-control"
id="name"
name="name"
type="text"
onChange={ this.onUserDataChange }
value={ getValueUser('name') }
required
/>
</div>
<div className="form-group">
<label htmlFor="surname">{ t('request.userData.surname') }</label>
<input
id="surname"
name="surname"
className="form-control"
type="text"
onChange={ this.onUserDataChange }
value={ getValueUser('surname') }
required
/>
</div>
</>
) }
{ internalRequest && (
<>
<div className="form-group">
<label htmlFor="address">{ t('request.userData.address') }</label>
<input
id="address"
className="form-control"
name="address"
type="text"
onChange={ this.onUserDataChange }
value={ getValueUser('address') }
required
/>
</div>
<div className="form-group">
<label htmlFor="country">{ t('request.userData.country') }</label>
<input
id="country"
className="form-control"
name="country"
type="text"
onChange={ this.onUserDataChange }
value={ getValueUser('country') }
required
/>
</div>
<div className="form-group">
<label htmlFor="telephone">{ t('request.userData.telephone') }</label>
<input
id="telephone"
className="form-control"
name="telephone"
type="text"
onChange={ this.onUserDataChange }
value={ getValueUser('telephone') }
/>
</div>
<div className="form-group">
<label htmlFor="orgName">{ t('request.userData.orgName') }</label>
<input
id="orgName"
className="form-control"
name="orgName"
type="text"
onChange={ this.onUserDataChange }
value={ getValueUser('orgName') }
/>
</div>
</>
) }
{ !internalRequest && (
<div className="form-group">
<div>{ t('request.requestInfo.preacceptSMTA.label') }</div>
<div className="form-check">
<input
id="preacceptSMTAYes"
name="preacceptSMTA"
type="radio"
className="form-check-input"
value="1"
onChange={ this.onSMTAChange }
checked={ requestInfo.preacceptSMTA === true } // there is no default value
required
/>
<label htmlFor="preacceptSMTAYes" className="form-check-label">
{ t('request.requestInfo.preacceptSMTA.yes') }
</label>
</div>
<div className="form-check">
<input
id="preacceptSMTANo"
name="preacceptSMTA"
type="radio"
className="form-check-input"
value="0"
onChange={ this.onSMTAChange }
checked={ requestInfo.preacceptSMTA === false }
required
/>
<label htmlFor="preacceptSMTANo" className="form-check-label">
{ t('request.requestInfo.preacceptSMTA.no') }
</label>
</div>
</div>
) }
<div className="form-group">
<div>{ t('request.requestInfo.purposeType.label') }</div>
<div className="form-check">
<input
id="purposeType0"
name="purposeType"
type="radio"
className="form-check-input"
value="0"
onChange={ this.onPurposeChange }
checked={ requestInfo.purposeType === 0 }
required
/>
<label htmlFor="purposeType0" className="form-check-label">{ t('request.requestInfo.purposeType.0') }</label>
</div>
<div className="form-check">
<input
id="purposeType1"
name="purposeType"
type="radio"
className="form-check-input"
value="1"
onChange={ this.onPurposeChange }
checked={ requestInfo.purposeType === 1 }
required
/>
<label htmlFor="purposeType1" className="form-check-label">{ t('request.requestInfo.purposeType.1') }</label>
</div>
</div>
<div className="form-group">
<label htmlFor="notes"><strong>{ t('request.requestInfo.notes') }</strong></label>
<textarea
id="notes"
className="form-control"
name="notes"
onChange={ this.onRequestDataChange }
value={ getValueRequest('notes') }
required
/>
</div>
<CaptchaInput onChange={ this.onCaptchaChange } captchaClientKey={ captchaSiteKey }/>
{ apiError && <ErrorDisplay error={ apiError }/> }
<button className="btn btn-primary mt-3" type="submit" disabled={ !captcha }>{ t('request.submit') }</button>
</form>
</>
);
};
}
export default withTranslation()(RequestPage)
import * as React from 'react';
import ReCAPTCHA from 'react-google-recaptcha';
interface ICaptchaInputProps extends React.ClassAttributes<any> {
onChange: (...args) => void;
captchaClientKey: string;
}
export default class CaptchaInput extends React.Component<ICaptchaInputProps> {
public render() {
const { captchaClientKey, onChange } = this.props;
return !captchaClientKey ?
null : (
<ReCAPTCHA
sitekey={ captchaClientKey }
onChange={ onChange }
/>
);
}
}
import * as React from 'react';
import { WithTranslation, withTranslation } from 'react-i18next';
const ErrorMessage = ({ error, t }: WithTranslation & { error: any }) => (
<pre style={ { backgroundColor: '#f9f2f4', border: '1px solid red', padding: '4px' } }>
<span>{ `${t('error.errorHappened')}: ` }</span>
{ typeof error === 'string' ? error : JSON.stringify(error).split(/\n|\\n/).map((text, index) => (
<span key={ `err-msg-${ index }` }>{ text }</span>
)) }
</pre>
);
export default withTranslation()(ErrorMessage);
import React from 'react';
import { Link } from 'react-router-dom';
import {WithTranslation, withTranslation} from "react-i18next";
import { WithTranslation, withTranslation } from 'react-i18next';
interface INavigation extends React.ClassAttributes<any>, WithTranslation {}
......@@ -13,9 +13,9 @@ class Navigation extends React.Component<INavigation, any> {
<header>
<nav>
<ul style={ { display: 'flex', listStyle: 'none' } }>
<li style={ { marginRight: '20px' } }><Link to="/">{t("nav.home")}</Link></li>
<li style={ { marginRight: '20px' } }><Link to="/">{t('nav.home')}</Link></li>
<li style={ { marginRight: '20px' } }><Link to="/overview">{t('nav.overview')}</Link></li>
<li style={ { marginRight: '20px' } }><Link to="/api-info">{t("nav.apiInfo")}</Link></li>
<li style={ { marginRight: '20px' } }><Link to="/api-info">{t('nav.apiInfo')}</Link></li>
<li style={ { marginRight: '20px' } }><Link to="/cart/">{t('nav.cart')}</Link></li>
</ul>
</nav>
......
import React from 'react';
import FilteredPage, { IPageRequest } from '@genesys/client/model/FilteredPage';
import { history } from './App';
import { history } from 'ui/core/App';
import { withTranslation, WithTranslation } from 'react-i18next';
interface IPagination extends React.ClassAttributes<any>, WithTranslation {
......
import React from 'react';
import { Property } from 'ui/Property';
import { Property } from 'ui/common/Property';
import { TreeMap } from 'd3js';
import { WithTranslation, withTranslation } from 'react-i18next';
......