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 @@ ...@@ -32,17 +32,19 @@
"analyze": "webpack --config config/webpack-analyze.config.js" "analyze": "webpack --config config/webpack-analyze.config.js"
}, },
"dependencies": { "dependencies": {
"d3": "^6.2.0", "d3": "^6.0.0",
"es-cookie": "^1.0.0", "es-cookie": "^1.0.0",
"history": "^4.0.0", "history": "^4.0.0",
"i18next": "^19.0.0", "i18next": "^19.0.0",
"react": "^16.0.0", "react": "^16.0.0",
"react-dom": "^16.0.0", "react-dom": "^16.0.0",
"react-google-recaptcha": "^2.0.0",
"react-i18next": "^11.0.0", "react-i18next": "^11.0.0",
"react-router-dom": "^5.0.0" "react-router-dom": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^16.0.0", "@types/react": "^16.0.0",
"@types/react-router-dom": "^5.0.0",
"@typescript-eslint/eslint-plugin": "^2.0.0", "@typescript-eslint/eslint-plugin": "^2.0.0",
"@typescript-eslint/eslint-plugin-tslint": "^2.0.0", "@typescript-eslint/eslint-plugin-tslint": "^2.0.0",
"@typescript-eslint/parser": "^2.0.0", "@typescript-eslint/parser": "^2.0.0",
......
import React from 'react'; import React from 'react';
import { AccessionService } from '@genesys/client/service'; 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 AccessionDetails from '@genesys/client/model/accession/AccessionDetails';
import { WithTranslation, withTranslation } from 'react-i18next'; import { WithTranslation, withTranslation } from 'react-i18next';
......
...@@ -6,7 +6,7 @@ import { AccessionService } from '@genesys/client/service'; ...@@ -6,7 +6,7 @@ import { AccessionService } from '@genesys/client/service';
import AccessionFilter from '@genesys/client/model/accession/AccessionFilter'; import AccessionFilter from '@genesys/client/model/accession/AccessionFilter';
import Accession from '@genesys/client/model/accession/Accession'; import Accession from '@genesys/client/model/accession/Accession';
import FilteredPage, { IPageRequest } from '@genesys/client/model/FilteredPage'; import FilteredPage, { IPageRequest } from '@genesys/client/model/FilteredPage';
import Pagination from './Pagination'; import Pagination from 'ui/common/Pagination';
import { AccessionFilters } from './AccessionFilters'; import { AccessionFilters } from './AccessionFilters';
import { withTranslation, WithTranslation } from 'react-i18next'; import { withTranslation, WithTranslation } from 'react-i18next';
import { LocalStorageCart } from 'utilities'; import { LocalStorageCart } from 'utilities';
......
...@@ -2,7 +2,7 @@ import * as React from 'react'; ...@@ -2,7 +2,7 @@ import * as React from 'react';
import { withTranslation, WithTranslation } from 'react-i18next'; import { withTranslation, WithTranslation } from 'react-i18next';
// model // model
import AccessionOverview from '@genesys/client/model/accession/AccessionOverview'; 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'; import { VocabularyService } from '@genesys/client/service';
interface IAccessionOverviewPageState { interface IAccessionOverviewPageState {
......
...@@ -5,10 +5,10 @@ import { log } from '@genesys/client/utilities/debug'; ...@@ -5,10 +5,10 @@ import { log } from '@genesys/client/utilities/debug';
// import * as cookies from 'es-cookie'; // import * as cookies from 'es-cookie';
import { reconfigureServiceAxios, LoginService } from '@genesys/client/service'; import { reconfigureServiceAxios, LoginService } from '@genesys/client/service';
import App from './ui/App'; import App from 'ui/core/App';
import ApiAccessError from './ui/ApiAccessError'; import ApiAccessError from 'ui/core/ApiAccessError';
import { Config, DefaultConfig } from '../config/config'; 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 }; // declare const window: Window & { devToolsExtension: any, __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: any, initialLanguage: any, initialI18nStore: any, localeMapping: any };
// const AUTH_COOKIE = 'GENESYS_AUTH'; // const AUTH_COOKIE = 'GENESYS_AUTH';
......
...@@ -7,7 +7,8 @@ ...@@ -7,7 +7,8 @@
"last": "Last" "last": "Last"
}, },
"error": { "error": {
"notFound": "Not Found" "notFound": "Not Found",
"errorHappened": "Error happened while processing request"
}, },
"estimatedNumberOfItems": "{{count, number}} {{what, lowercase}}", "estimatedNumberOfItems": "{{count, number}} {{what, lowercase}}",
"accession": { "accession": {
...@@ -159,6 +160,39 @@ ...@@ -159,6 +160,39 @@
"title": "Shopping cart", "title": "Shopping cart",
"addToCart": "Add to cart", "addToCart": "Add to cart",
"removeFromCart": "Remove from 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'; ...@@ -5,20 +5,20 @@ import AccessionFilter from '@genesys/client/model/accession/AccessionFilter';
import Accession from '@genesys/client/model/accession/Accession'; import Accession from '@genesys/client/model/accession/Accession';
import { withTranslation, WithTranslation } from 'react-i18next'; import { withTranslation, WithTranslation } from 'react-i18next';
import { LocalStorageCart } from 'utilities'; 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>>; accessions: Array<Accession & Record<string, any>>;
isEmpty: boolean; isEmpty: boolean;
selected: string[]; selected: string[];
isAllSelected: boolean; isAllSelected: boolean;
} }
interface IAccessionListPageProps extends WithTranslation { interface ICartPageProps extends WithTranslation, RouteComponentProps {
filter?: AccessionFilter; filter?: AccessionFilter;
} }
export class AccessionListPage extends React.Component<IAccessionListPageProps, IAccessionListPageState> { class CartPage extends React.Component<ICartPageProps, ICartPageState> {
public constructor(props) { public constructor(props) {
super(props); super(props);
...@@ -100,6 +100,11 @@ export class AccessionListPage extends React.Component<IAccessionListPageProps, ...@@ -100,6 +100,11 @@ export class AccessionListPage extends React.Component<IAccessionListPageProps,
})); }));
}; };
private onRequest = () => {
const { history } = this.props;
history.push('/request');
};
public render() { public render() {
const { accessions, selected, isEmpty, isAllSelected } = this.state; const { accessions, selected, isEmpty, isAllSelected } = this.state;
const { t } = this.props; const { t } = this.props;
...@@ -110,11 +115,15 @@ export class AccessionListPage extends React.Component<IAccessionListPageProps, ...@@ -110,11 +115,15 @@ export class AccessionListPage extends React.Component<IAccessionListPageProps,
<> <>
<h1 className="d-flex justify-content-between align-items-center"> <h1 className="d-flex justify-content-between align-items-center">
{ t('cart.title') } { 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 }> <button type="button" className="btn btn-primary" onClick={ this.removeItems }>
{ t('cart.removeFromCart') } { t('cart.removeFromCart') }
</button> </button>
} ) }
</h1> </h1>
{ isEmpty && <div>{ t('cart.isEmpty') }</div> } { isEmpty && <div>{ t('cart.isEmpty') }</div> }
{ accessions && ( { accessions && (
...@@ -162,4 +171,4 @@ export class AccessionListPage extends React.Component<IAccessionListPageProps, ...@@ -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' } }>