Commit 8bf6c8d5 authored by Oleksii Savran's avatar Oleksii Savran Committed by Matija Obreza
Browse files

Submitting requests

parent 9606ef40
......@@ -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",
......
......@@ -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,8 +5,9 @@ import ApiInfoPage from './ApiInfoPage';
import AccessionList from './AccessionListPage';
import AccessionDetails from './AccessionDetailsPage';
import { createHashHistory } from 'history';
import CartPage from './CartPage';
import CartPage from 'ui/CartPage';
import OverviewPage from 'ui/OverviewPage';
import RequestPage from 'ui/RequestPage';
const hashHistory = createHashHistory({});
......@@ -27,8 +28,9 @@ export default class App extends React.Component<IAppProps, any> {
<Route path="/a/:uuid" exact render={ (props) => <AccessionDetails { ...props } apiUrl={ apiUrl } /> } />
<Route path="/" exact render={ (props) => <AccessionList { ...props } filter={ this.props.filter } /> }/>
<Route path="/api-info" exact component={ ApiInfoPage }/>
<Route path="/cart/" exact component={ CartPage }/>
<Route path="/cart" exact component={ CartPage }/>
<Route path="/overview" exact render={ (props) => <OverviewPage { ...props } filter={ this.props.filter } /> }/>
<Route path="/request" exact component={ RequestPage }/>
<Route component={ NotFound }/>
</Switch>
......
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 }
/>
);
}
}
......@@ -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 * 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 { withTranslation, WithTranslation } from 'react-i18next';
import { RouteComponentProps } from 'react-router-dom';
import ErrorDisplay from 'ui/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 './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: { internalRequest: true },
userData: {
// pid: 'Internal',
// 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>) => {
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 }>
{ 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>
</>
) }
<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') }
/>
</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
/>
<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 }
/>
<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 }
/>
<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 }
/>
<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>
<input
id="notes"
className="form-control"
name="notes"
type="text"
onChange={ this.onRequestDataChange }
value={ getValueRequest('notes') }
/>
</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>
<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)
......@@ -51,6 +51,11 @@
"@types/minimatch" "*"
"@types/node" "*"
"@types/history@*":
version "4.7.8"
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934"
integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA==
"@types/json-schema@^7.0.3", "@types/json-schema@^7.0.5":
version "7.0.6"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0"
......@@ -81,7 +86,24 @@
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24"
integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==
"@types/react@^16.0.0":
"@types/react-router-dom@^5.0.0":
version "5.1.6"
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.6.tgz#07b14e7ab1893a837c8565634960dc398564b1fb"
integrity sha512-gjrxYqxz37zWEdMVvQtWPFMFj1dRDb4TGOcgyOfSXTrEXdF92L00WE3C471O3TV/RF1oskcStkXsOU0Ete4s/g==
dependencies:
"@types/history" "*"
"@types/react" "*"
"@types/react-router" "*"
"@types/react-router@*":
version "5.1.8"
resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.8.tgz#4614e5ba7559657438e17766bb95ef6ed6acc3fa"
integrity sha512-HzOyJb+wFmyEhyfp4D4NYrumi+LQgQL/68HvJO+q6XtuHSDvw6Aqov7sCAhjbNq3bUPgPqbdvjXC5HeB2oEAPg==
dependencies:
"@types/history" "*"
"@types/react" "*"
"@types/react@*", "@types/react@^16.0.0":
version "16.9.56"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.56.tgz#ea25847b53c5bec064933095fc366b1462e2adf0"
integrity sha512-gIkl4J44G/qxbuC6r2Xh+D3CGZpJ+NdWTItAPmZbR5mUS+JQ8Zvzpl0ea5qT/ZT3ZNTUcDKUVqV3xBE8wv/DyQ==
......@@ -1720,7 +1742,7 @@ d3-zoom@2:
d3-selection "2"
d3-transition "2"
d3@^6.2.0:
d3@^6.0.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/d3/-/d3-6.2.0.tgz#f19b0ecb16ca4ad2171ce8b37c63247e71c6f01d"
integrity sha512-aH+kx55J8vRBh4K4k9GN4EbNO3QnZsXy4XBfrnr4fL2gQuszUAPQU3fV2oObO2iSpreRH/bG/wfvO+hDu2+e9w==
......@@ -3071,7 +3093,7 @@ hmac-drbg@^1.0.0:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
hoist-non-react-statics@^3.1.0:
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
......@@ -5078,7 +5100,7 @@ promise-inflight@^1.0.1:
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
prop-types@^15.6.2, prop-types@^15.7.2:
prop-types@^15.5.0, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
......@@ -5215,6 +5237,14 @@ raw-body@2.4.0:
iconv-lite "0.4.24"
unpipe "1.0.0"
react-async-script@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/react-async-script/-/react-async-script-1.2.0.tgz#ab9412a26f0b83f5e2e00de1d2befc9400834b21"
integrity sha512-bCpkbm9JiAuMGhkqoAiC0lLkb40DJ0HOEJIku+9JDjxX3Rcs+ztEOG13wbrOskt3n2DTrjshhaQ/iay+SnGg5Q==
dependencies:
hoist-non-react-statics "^3.3.0"
prop-types "^15.5.0"
react-dom@^16.0.0:
version "16.14.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89"
......@@ -5225,6 +5255,14 @@ react-dom@^16.0.0:
prop-types "^15.6.2"
scheduler "^0.19.1"
react-google-recaptcha@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/react-google-recaptcha/-/react-google-recaptcha-2.1.0.tgz#9f6f4954ce49c1dedabc2c532347321d892d0a16"
integrity sha512-K9jr7e0CWFigi8KxC3WPvNqZZ47df2RrMAta6KmRoE4RUi7Ys6NmNjytpXpg4HI/svmQJLKR+PncEPaNJ98DqQ==
dependencies:
prop-types "^15.5.0"
react-async-script "^1.1.1"
react-i18next@^11.0.0:
version "11.7.3"
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.7.3.tgz#256461c46baf5b3208c3c6860ca4e569fc7ed053"
......
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