Commit 6c88e3ec authored by Matija Obreza's avatar Matija Obreza
Browse files

Merge branch '385-integrate-cooperator-with-fao-api' into 'main'

Integrate cooperator with FAO API

Closes #385

See merge request grin-global/grin-global-ui!376
parents bc71e2cf e4763ca7
......@@ -1323,6 +1323,40 @@
"ietfTag": "IEFT Tag",
"scriptDirection": "Script Direction",
"isEnabled": "Is Enabled?"
},
"FaoWiewsInstitute": {
"instcode": "Institute code",
"country_iso3": "Country code",
"valid_instcode": "Valid institute code",
"name": "Name",
"acronym": "Acronym",
"parent_instcode": "Parent institute code",
"parent_name": "Parent name",
"address": "Address",
"country": "Country",
"Postal_Code": "Postal code",
"telephone": "Telephone",
"email": "Email",
"website": "Website",
"status": "Status",
"lat": "Latitude",
"lon": "Longitude",
"Genebank_long_term_collections": "Genebank long term collections",
"Genebank_medium_term_collections": "Genebank medium term collections",
"Genebank_short_term_collections": "Genebank short term collections",
"Botanical_garden": "Botanical garden",
"Breeder": "Breeder",
"Network": "Network",
"Community": "Community",
"Educational": "Educational",
"Seed_producer": "Seed producer",
"Seed_supplier": "Seed supplier",
"Farmer_community": "Farmer community",
"Research": "Research",
"Extensionist": "Extensionist",
"Laboratory": "Laboratory",
"Publisher": "Publisher",
"Administration_or_Policy": "Administration or Policy"
}
}
}
\ No newline at end of file
{
"action": {
"add": "Add {{what, lowercase}}",
"apply": "Apply",
"navigateTo": "Navigate to {{where, lowercase}}",
"back": "Back",
"cancel": "Cancel",
......@@ -62,6 +63,7 @@
"deleteItemConfirm": "Do you really want to delete {{what, lowercase}}: {{title}}?",
"deleteListConfirm": "Do you really want to delete {{count}} {{what, lowercase}}?",
"resetTableConfirm": "Do you really want to reset all table data?",
"replaceRelatedFields": "Do you want this change to replace some related fields?",
"description": "Description",
"either": "Either",
"apiError": "The server reported an error",
......
import { AxiosInstance, AxiosRequestConfig } from "axios";
import * as QueryString from "query-string";
const API_SERVER = "https://wiews.fao.org";
const URL_LIST_INSTITUTES = `${API_SERVER}/api/v1/organizations/`;
/**
* API at https://wiews.fao.org/ base.
*/
class FaoWiewsInstituteService {
private _axios: AxiosInstance;
public constructor(axios: AxiosInstance) {
this._axios = axios;
}
/**
* list at /api/v1/organizations
*
*/
public list(params: FaoWiewsInstituteListRequest, xhrConfig?: AxiosRequestConfig): Promise<FaoWiewsInstituteResponse[]> {
const qs = QueryString.stringify({
case_insensitive: params.caseInsensitive ?? true,
output_format: params.outputFormat ?? 'json',
instcode: params.instcode ?? '',
country_iso: params.countryIso ?? '',
name: params.name ?? '',
download: params.download ?? undefined
}, {});
const apiUrl = URL_LIST_INSTITUTES + (qs ? `?${qs}` : '');
// console.log(`Fetching from ${apiUrl}`);
const content = {
/* No content in request body */
};
return this._axios
.request({
...xhrConfig,
url: apiUrl,
method: "GET",
...content,
})
.then(({ data }) => {
return data;
});
}
}
/**
* Recreated from API response
*/
type FaoWiewsInstituteListRequest = {
caseInsensitive?: boolean;
outputFormat?: 'json' | 'csv';
instcode?: string;
countryIso?: string;
name?: string
download?: boolean
}
type FaoWiewsInstituteResponse = {
instcode: string;
country_iso3: string;
valid_instcode: string;
name: string;
acronym: string;
parent_instcode: string;
parent_name: string;
address: string;
country: string;
Postal_Code: number;
telephone: string;
email: string;
website: string;
status: string;
lat: number;
lon: number;
Genebank_long_term_collections: boolean;
Genebank_medium_term_collections: boolean;
Genebank_short_term_collections: boolean;
Botanical_garden: boolean;
Breeder: boolean;
Network: boolean;
Community: boolean;
Educational: boolean;
Seed_producer: boolean;
Seed_supplier: boolean;
Farmer_community: boolean;
Research: boolean;
Extensionist: boolean;
Laboratory: boolean;
Publisher: boolean;
Administration_or_Policy: boolean;
};
export {
FaoWiewsInstituteService as default,
FaoWiewsInstituteResponse
};
import axios from 'axios';
import FaoWiewsInstituteService from '@gringlobal-ce/client/external/FaoWiewsInstituteService';
/**
* Axios instance for accessing the API
*/
const serviceAxios = axios.create({
timeout: 0,
withCredentials: false,
headers: {
'Content-Type': 'application/json',
},
});
const ConfiguredFaoWiewsInstituteService = new FaoWiewsInstituteService(serviceAxios);
export { ConfiguredFaoWiewsInstituteService as FaoWiewsInstituteService };
......@@ -31,7 +31,7 @@ class GeographyFilter {
public adm3TypeCode?: Partial<StringFilter>;
public adm4Abbrev?: Partial<StringFilter>;
public adm4TypeCode?: Partial<StringFilter>;
public countryCode?: Partial<StringFilter>;
public countryCode?: string;
public changedDate?: DateFilter;
public valid?: boolean;
public note?: Partial<StringFilter>;
......
......@@ -1310,5 +1310,39 @@
"ietfTag": "IEFT Tag",
"scriptDirection": "Script Direction",
"isEnabled": "Is Enabled?"
},
"FaoWiewsInstitute": {
"instcode": "Institute code",
"country_iso3": "Country code",
"valid_instcode": "Valid institute code",
"name": "Name",
"acronym": "Acronym",
"parent_instcode": "Parent institute code",
"parent_name": "Parent name",
"address": "Address",
"country": "Country",
"Postal_Code": "Postal code",
"telephone": "Telephone",
"email": "Email",
"website": "Website",
"status": "Status",
"lat": "Latitude",
"lon": "Longitude",
"Genebank_long_term_collections": "Genebank long term collections",
"Genebank_medium_term_collections": "Genebank medium term collections",
"Genebank_short_term_collections": "Genebank short term collections",
"Botanical_garden": "Botanical garden",
"Breeder": "Breeder",
"Network": "Network",
"Community": "Community",
"Educational": "Educational",
"Seed_producer": "Seed producer",
"Seed_supplier": "Seed supplier",
"Farmer_community": "Farmer community",
"Research": "Research",
"Extensionist": "Extensionist",
"Laboratory": "Laboratory",
"Publisher": "Publisher",
"Administration_or_Policy": "Administration or Policy"
}
}
......@@ -183,6 +183,10 @@
"noReportSelected": "Please select a report template",
"selectReportTemplate": "Select the report template",
"reportReady": "Report is ready to print"
},
"faoWiewsInstituteField": {
"noInstituteInstCodeFound": "No institute found by instcode {{instcode}}",
"foundInstitute": "Found institute by instcode {{instcode}}"
}
}
},
......
import * as React from 'react';
import { WithTranslation, withTranslation } from 'react-i18next';
import { compose } from 'redux';
// model
import { FaoWiewsInstituteResponse } from "@gringlobal-ce/client/external/FaoWiewsInstituteService";
import Dialog from '@material-ui/core/Dialog';
import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';
import { Button, DialogActions } from '@material-ui/core';
import { Properties, PropertiesItem } from "@gringlobal-ce/client/ui/common/Properties";
import Number from "@gringlobal-ce/client/ui/common/Number";
interface IFaoWiewsInstituteDialogProps extends React.ClassAttributes<any>, WithTranslation {
isOpen: boolean;
onClose: () => void;
onApply: () => void;
institute: FaoWiewsInstituteResponse
}
class FaoWiewsInstituteDialog extends React.Component <IFaoWiewsInstituteDialogProps, any> {
public componentWillUnmount() {
this.props.onClose();
}
public render() {
const { isOpen, t, institute, onClose, onApply } = this.props;
return (
<div>
{ isOpen &&
<Dialog
open={ isOpen }
onClose={ onClose }
fullWidth
maxWidth="md"
>
<DialogTitle>{ t('public.c.faoWiewsInstituteField.foundInstitute', { instcode: institute.instcode }) }</DialogTitle>
<DialogContent>
<Properties>
{ institute.instcode &&
<PropertiesItem title={ t('client:model.FaoWiewsInstitute.instcode') }>
{ institute.instcode }
</PropertiesItem>
}
{ institute.country_iso3 &&
<PropertiesItem title={ t('client:model.FaoWiewsInstitute.country_iso3') }>
{ institute.country_iso3 }
</PropertiesItem>
}
{ institute.valid_instcode &&
<PropertiesItem title={ t('client:model.FaoWiewsInstitute.valid_instcode') }>
{ institute.valid_instcode }
</PropertiesItem>
}
{ institute.name &&
<PropertiesItem title={ t('client:model.FaoWiewsInstitute.name') }>
{ institute.name }
</PropertiesItem>
}
{ institute.acronym &&
<PropertiesItem title={ t('client:model.FaoWiewsInstitute.acronym') }>
{ institute.acronym }
</PropertiesItem>
}
{ institute.parent_instcode &&
<PropertiesItem title={ t('client:model.FaoWiewsInstitute.parent_instcode') }>
{ institute.parent_instcode }
</PropertiesItem>
}
{ institute.address &&
<PropertiesItem title={ t('client:model.FaoWiewsInstitute.address') }>
{ institute.address }
</PropertiesItem>
}
{ institute.country &&
<PropertiesItem title={ t('client:model.FaoWiewsInstitute.country') }>
{ institute.country }
</PropertiesItem>
}
{ institute.Postal_Code &&
<PropertiesItem title={ t('client:model.FaoWiewsInstitute.Postal_Code') }>
{ institute.Postal_Code }
</PropertiesItem>
}
{ institute.telephone &&
<PropertiesItem title={ t('client:model.FaoWiewsInstitute.telephone') }>
{ institute.telephone }
</PropertiesItem>
}
{ institute.email &&
<PropertiesItem title={ t('client:model.FaoWiewsInstitute.email') }>
{ institute.email }
</PropertiesItem>
}
{ institute.website &&
<PropertiesItem title={ t('client:model.FaoWiewsInstitute.website') }>
{ institute.website }
</PropertiesItem>
}
{ institute.status &&
<PropertiesItem title={ t('client:model.FaoWiewsInstitute.status') }>
{ institute.status }
</PropertiesItem>
}
{ institute.lat &&
<PropertiesItem title={ t('client:model.FaoWiewsInstitute.lat') }>
<Number value={ +institute.lat } config={ { maximumFractionDigits: 4 } } />
</PropertiesItem>
}
{ institute.lon &&
<PropertiesItem title={ t('client:model.FaoWiewsInstitute.lon') }>
<Number value={ +institute.lon } config={ { maximumFractionDigits: 4 } } />
</PropertiesItem>
}
</Properties>
</DialogContent>
<DialogActions>
<Button variant="contained" color="primary" onClick={ onApply }>{ t('common:action.apply') }</Button>
<Button variant="contained" type="button" onClick={ onClose }>{ t('common:action.cancel') }</Button>
</DialogActions>
</Dialog>
}
</div>
);
}
}
export default compose(
withTranslation(),
)(FaoWiewsInstituteDialog);
import * as React from 'react';
import { FieldRenderProps } from 'react-final-form';
import { FaoWiewsInstituteService } from "@gringlobal-ce/client/external";
import { Field } from 'react-final-form';
import { TextField } from "@gringlobal-ce/client/ui/common/form/TextField";
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import { showSnackbar } from "@gringlobal-ce/client/action/snackbar";
import { WithTranslation, withTranslation } from "react-i18next";
import InputAdornment from "@material-ui/core/InputAdornment";
import IconButton from "@material-ui/core/IconButton";
import SearchIcon from '@material-ui/icons/SearchOutlined';
import FaoWiewsInstituteDialog from "common/FaoWiewsInstituteDialog";
import _isObject from 'lodash/isObject';
interface IFaoWiewsInstituteAutocomplete extends WithTranslation {
name: string;
label: string;
placeholder?: string;
helperText?: string;
showSnackbar: (snack: string) => void;
fieldChange: (value) => void;
initialValue?: string;
}
class FaoWiewsInstituteField extends React.Component<IFaoWiewsInstituteAutocomplete & FieldRenderProps<any>, any> {
public constructor(props) {
super(props);
this.state = {
isDialogOpen: false,
foundInstitute: null,
value: props.initialValue ?? ''
}
}
private findInstitute = () => {
const { showSnackbar, t } = this.props;
const { value } = this.state;
FaoWiewsInstituteService.list({ instcode: value }).then((data) => {
if (data.length > 0) {
this.setState({ foundInstitute: data[0] })
this.openDialog()
return;
}
showSnackbar(t('public.c.faoWiewsInstituteField.noInstituteInstCodeFound', { instcode: value }))
});
};
private handleApplyInstitute = () => {
const { fieldChange } = this.props;
const { foundInstitute } = this.state;
fieldChange(foundInstitute)
this.closeDialog();
}
private handleParse = (v) => {
this.setState({ value: v })
return v
}
private openDialog = () => {
this.setState({ isDialogOpen: true })
}
private closeDialog = () => {
this.setState({ isDialogOpen: false })
}
public render() {
const { name, label, placeholder, helperText } = this.props;
const { value, foundInstitute, isDialogOpen } = this.state;
return (
<>
<Field
name={ name }
label={ label }
component={ TextField }
type="text"
placeholder={ placeholder }
helperText={ helperText }
parse={ this.handleParse }
inputProps={ {
value
} }
endAdornment={
<InputAdornment position="end">
<IconButton type="button" onClick={ this.findInstitute }>
<SearchIcon/>
</IconButton>
</InputAdornment>
}
format={ (v) => _isObject(v) ? v.instcode : v }
/>
<FaoWiewsInstituteDialog
isOpen={ isDialogOpen }
onClose={ this.closeDialog }
institute={ foundInstitute }
onApply={ this.handleApplyInstitute }
/>
</>
);
}
}
const mapDispatchToProps = (dispatch) => bindActionCreators({
showSnackbar,
}, dispatch);
export default connect(null, mapDispatchToProps)(withTranslation()(FaoWiewsInstituteField));
import * as React from 'react';
import { Field, Form, FormProps, FormRenderProps } from 'react-final-form';
import { Field, Form, FormSpy, FormProps, FormRenderProps } from 'react-final-form';
import { createStyles, WithStyles, withStyles } from '@material-ui/core/styles';
import { withTranslation, WithTranslation } from 'react-i18next';
import { getI18n, withTranslation, WithTranslation } from 'react-i18next';
// Model
import Cooperator from '@gringlobal-ce/client/model/gringlobal/Cooperator';
// UI
......@@ -14,6 +14,11 @@ import GeographyField from 'common/GeographyField';
import SysLangSelect from 'common/SysLangSelect';
import { PageSection } from '@gringlobal-ce/client/ui/common/layout/Section';
import withDialog from 'ui/common/withDialog';
import FaoWiewsInstituteField from "common/FaoWiewsInstituteField";
import createDecorator from "final-form-calculate";
import { FaoWiewsInstituteResponse } from "@gringlobal-ce/client/external/FaoWiewsInstituteService";
import confirm from "@gringlobal-ce/client/utilities/confirmAlert";
import { GeographyService } from "@gringlobal-ce/client/service";
const styles = (theme) => createStyles({
textField: {},
......@@ -26,13 +31,106 @@ const styles = (theme) => createStyles({
},
});
const CooperatorForm = ({ t, onSubmit, initialValues, error, classes }: FormProps & WithStyles & WithTranslation) =>
<Form
const relatedFieldsToInstitute = {
// field in cooperator: field in institute
organization: 'name',
organizationAbbrev: 'acronym',
addressLine1: 'address',
postalIndex: 'Postal_Code',
}
let confirmPromise
const callConfirm = () => {
const i18n = getI18n()
if (!confirmPromise) {
confirmPromise = confirm(i18n.t('common:label.replaceRelatedFields'), {
confirmLabel: i18n.t('common:label.yes'),
abortLabel: i18n.t('common:label.no'),
})
}
return confirmPromise
}
const confirmInProgress = () => {
return confirmPromise !== null
}
const calculator = createDecorator(
{
field: 'faoInstituteNumber', // when fao institute changes...
updates: {
// ...update related fields
...Object.entries(relatedFieldsToInstitute).reduce((acc, [key, val]) => {
acc[key] = (institute: FaoWiewsInstituteResponse, allValues) => {
if (institute && institute[val]) {
if (!allValues[key] && !confirmInProgress()) {
return institute[val]
}
return new Promise((resolve) => {
return callConfirm()
.then(() => {
resolve(institute[val])
})
.catch((e) => resolve(allValues[key]))
})
}
return allValues[key]
}
return acc;
}, {}),
geography: (institute: FaoWiewsInstituteResponse, allValues) => {
if (institute && institute['country_iso3']) {
const geographyPromise = GeographyService.list({ countryCode: institute['country_iso3'] }, { page: 0, size: 1 })
.then((data) => data.content[0])
.catch((e) => {
console.log('Error updating geography field', e)
return allValues['geography']
})
if (!allValues['geography'] && !confirmInProgress()) {
return geographyPromise
}
return new Promise((resolve) => {
return callConfirm()
.then(() => {
resolve(geographyPromise)
})