Commit 79f74e30 authored by Oleksii Savran's avatar Oleksii Savran

CropTrait: remove, add

parent 7fee466f
......@@ -33,6 +33,7 @@ class CropTrait {
public static CodeValues = {
dataTypeCode: 'CROP_TRAIT_DATA_TYPE',
categoryCode: 'DESCRIPTOR_CATEGORY',
originalValueTypeCode: 'CROP_TRAIT_DATA_TYPE',
}
}
......
import Cooperator from '@gringlobal/client/model/gringlobal/Cooperator';
import Crop from '@gringlobal/client/model/gringlobal/Crop';
import TranslatedCropTraitCode from '@gringlobal/client/model/gringlobal/TranslatedCropTraitCode';
/**
* TranslatedCropTrait
*
* GRIN-Global CE API
*/
class TranslatedCropTrait {
public createdBy: number;
public createdDate: Date;
public modifiedBy: number;
public modifiedDate: Date;
public ownedBy: Cooperator;
public ownedDate: Date;
public categoryCode: string;
public codedName: string;
public crop: Crop;
public dataTypeCode: string;
public id: number;
public isArchived: string;
public isCoded: string;
public isPeerReviewed: string;
public maxLength: number;
public note: string;
public numericFormat: string;
public numericMaximum: number;
public numericMinimum: number;
public ontologyUrl: string;
public originalValueFormat: string;
public originalValueTypeCode: string;
public title: string;
public description: string;
public codes: TranslatedCropTraitCode[];
}
export default TranslatedCropTrait;
import Cooperator from '@gringlobal/client/model/gringlobal/Cooperator';
import CropTrait from '@gringlobal/client/model/gringlobal/CropTrait';
/**
* TranslatedCropTraitCode
*
* GRIN-Global CE API
*/
class TranslatedCropTraitCode {
public createdBy: number;
public createdDate: Date;
public modifiedBy: number;
public modifiedDate: Date;
public ownedBy: Cooperator;
public ownedDate: Date;
public code: string;
public cropTrait: CropTrait;
public id: number;
public title: string;
public description: string;
}
export default TranslatedCropTraitCode;
......@@ -21,6 +21,7 @@ const URL_UPDATE_CROP = '/api/v1/crop';
const URL_CREATE_CROP = '/api/v1/crop';
const URL_LIST_CROPS = '/api/v1/crop/list';
const URL_FILTER_CROPS = '/api/v1/crop/filter';
const URL_AUTOCOMPLETE_CROPS = '/api/v1/crop/autocomplete';
/**
......@@ -263,6 +264,28 @@ class CropService {
method: 'POST',
...content,
}).then(({ data }) => data as FilteredPage<Crop>);
};
/**
* autocomplete at /api/v1/crop/autocomplete
*
* @param term undefined
* @param xhrConfig additional xhr config
*/
public autocomplete = (term: string, xhrConfig?: AxiosRequestConfig): Promise<Crop[]> => {
const qs = QueryString.stringify({
term: term || undefined,
}, {});
const apiUrl = URL_AUTOCOMPLETE_CROPS + (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 }) => data as Crop[]);
}
}
......
......@@ -6,6 +6,7 @@ import { AxiosInstance, AxiosRequestConfig } from 'axios';
import { IPageRequest } from '@gringlobal/client/model/page';
import CropTrait from '@gringlobal/client/model/gringlobal/CropTrait';
import CropTraitFilter from '@gringlobal/client/model/gringlobal/CropTraitFilter';
import TranslatedCropTrait from '@gringlobal/client/model/gringlobal/TranslatedCropTrait';
const URL_UPLOAD_FILE = UrlTemplate.parse('/api/v1/crop-trait/attach/{cropTraitId}');
const URL_GET_TRANSLATED = UrlTemplate.parse('/api/v1/crop-trait/{id}');
......@@ -56,7 +57,7 @@ class CropTraitService {
* @param id undefined
* @param xhrConfig additional xhr config
*/
public getTranslated = (id: number, xhrConfig?: AxiosRequestConfig): Promise<any> => {
public getTranslated = (id: number, xhrConfig?: AxiosRequestConfig): Promise<TranslatedCropTrait> => {
const apiUrl = URL_GET_TRANSLATED.expand({ id });
// console.log(`Fetching from ${apiUrl}`);
......@@ -67,7 +68,7 @@ class CropTraitService {
url: apiUrl,
method: 'GET',
...content,
}).then(({ data }) => data as undefined);
}).then(({ data }) => data as TranslatedCropTrait);
};
/**
......@@ -76,7 +77,7 @@ class CropTraitService {
* @param id undefined
* @param xhrConfig additional xhr config
*/
public remove = (id: number, xhrConfig?: AxiosRequestConfig): Promise<any> => {
public remove = (id: number, xhrConfig?: AxiosRequestConfig): Promise<CropTrait> => {
const apiUrl = URL_REMOVE.expand({ id });
// console.log(`Fetching from ${apiUrl}`);
......@@ -87,7 +88,7 @@ class CropTraitService {
url: apiUrl,
method: 'DELETE',
...content,
}).then(({ data }) => data as undefined);
}).then(({ data }) => data as CropTrait);
};
/**
......@@ -159,7 +160,7 @@ class CropTraitService {
* @param data Request body
* @param xhrConfig additional xhr config
*/
public update = (data: CropTrait, xhrConfig?: AxiosRequestConfig): Promise<any> => {
public update = (data: CropTrait, xhrConfig?: AxiosRequestConfig): Promise<CropTrait> => {
const apiUrl = URL_UPDATE;
// console.log(`Fetching from ${apiUrl}`);
......@@ -170,7 +171,7 @@ class CropTraitService {
url: apiUrl,
method: 'PUT',
...content,
}).then(({ data }) => data as undefined);
}).then(({ data }) => data as CropTrait);
};
/**
......@@ -179,7 +180,7 @@ class CropTraitService {
* @param data Request body
* @param xhrConfig additional xhr config
*/
public create = (data: CropTrait, xhrConfig?: AxiosRequestConfig): Promise<any> => {
public create = (data: CropTrait, xhrConfig?: AxiosRequestConfig): Promise<CropTrait> => {
const apiUrl = URL_CREATE;
// console.log(`Fetching from ${apiUrl}`);
......@@ -190,7 +191,7 @@ class CropTraitService {
url: apiUrl,
method: 'POST',
...content,
}).then(({ data }) => data as undefined);
}).then(({ data }) => data as CropTrait);
};
/**
......@@ -200,7 +201,7 @@ class CropTraitService {
* @param page undefined
* @param xhrConfig additional xhr config
*/
public list = (data: CropTraitFilter, page?: IPageRequest, xhrConfig?: AxiosRequestConfig): Promise<any> => {
public list = (data: CropTraitFilter, page?: IPageRequest, xhrConfig?: AxiosRequestConfig): Promise<TranslatedCropTrait> => {
const qs = QueryString.stringify({
p: page.page || undefined,
......@@ -217,7 +218,7 @@ class CropTraitService {
url: apiUrl,
method: 'POST',
...content,
}).then(({ data }) => data as undefined);
}).then(({ data }) => data as TranslatedCropTrait);
}
}
......
import * as React from 'react';
import Crop from '@gringlobal/client/model/gringlobal/Crop';
import { FieldRenderProps } from 'react-final-form';
import { ListItemText } from '@material-ui/core';
import { CropService } from '@gringlobal/client/service';
import Autocomplete from '@gringlobal/client/ui/common/form/Autocomplete';
interface ICropAutocomplete {
label: string;
}
class CropAutocomplete extends React.Component<ICropAutocomplete & FieldRenderProps<any>> {
public constructor(props) {
super(props);
}
private autocomplete = (text: string): Promise<Crop[]> => {
return CropService.autocomplete(text);
};
private renderOption = (crop: Crop, state: object): React.ReactNode => {
return (
<ListItemText
primary={ crop.name }
/>
);
};
private mapOptions = (crops: Crop[]): Crop[] => crops;
private getOptionLabel = (crop: Crop): string => crop.name;
public render() {
return (
<Autocomplete
{ ...this.props }
autocomplete={ this.autocomplete }
renderOption={ this.renderOption }
mapOptions={ this.mapOptions }
getOptionLabel={ this.getOptionLabel }
/>
);
}
}
export default CropAutocomplete;
......@@ -4,12 +4,14 @@ import { put, takeEvery, call, take, select } from 'redux-saga/effects';
import {
ADMIN_RECEIVE_CROP,
ADMIN_RECEIVE_CROP_TRAIT,
REMOVE_CROP_TRAIT,
SAGA_ADMIN_CREATE_CROP,
SAGA_ADMIN_RECEIVE_CROP,
SAGA_ADMIN_EDIT_CROP,
SAGA_ADMIN_CREATE_CROP_TRAIT,
SAGA_ADMIN_EDIT_CROP_TRAIT,
SAGA_ADMIN_RECEIVE_CROP_TRAIT,
SAGA_ADMIN_REMOVE_CROP_TRAIT,
} from 'crop/constants';
// Model
import Crop from '@gringlobal/client/model/gringlobal/Crop';
......@@ -18,7 +20,7 @@ import CropTrait from '@gringlobal/client/model/gringlobal/CropTrait';
import { CropService, CropTraitService } from '@gringlobal/client/service';
// Action
import { sagaNavigate } from '@gringlobal/client/action/navigation';
import { receiveCropDetailsSaga } from './public';
import { clearCropTrait, receiveCropDetailsSaga } from './public';
export const cropAdminSagas = [
......@@ -28,6 +30,7 @@ export const cropAdminSagas = [
takeEvery(SAGA_ADMIN_CREATE_CROP_TRAIT, createCropTraitSaga),
takeEvery(SAGA_ADMIN_EDIT_CROP_TRAIT, editCropTraitSaga),
takeEvery(SAGA_ADMIN_RECEIVE_CROP_TRAIT, getCropTraitSaga),
takeEvery(SAGA_ADMIN_REMOVE_CROP_TRAIT, removeCropTraitSaga),
];
export const createCropAction = (crop: Crop) => ({
......@@ -60,6 +63,11 @@ export const getCropTraitAction = (id: string | number) => ({
payload: { id },
});
export const removeCropTraitAction = (id: string | number) => ({
type: SAGA_ADMIN_REMOVE_CROP_TRAIT,
payload: { id },
});
function* createCropSaga(action) {
yield put({
type: 'API',
......@@ -133,15 +141,11 @@ function* createCropTraitSaga(action) {
return cropTrait;
},
});
yield take(ADMIN_RECEIVE_CROP);
const received = yield take(ADMIN_RECEIVE_CROP);
yield take(ADMIN_RECEIVE_CROP_TRAIT);
const received = yield take(ADMIN_RECEIVE_CROP_TRAIT);
if (received.payload.apiCall.data) {
console.log('navigate after creating');
// const id = received.payload.apiCall.data.id;
// yield call(receiveCropDetailsSaga, { payload: { id } });
// yield call(sagaNavigate, `/crop/${received.payload.apiCall.data.id}`);
// yield put({ type: ADMIN_RECEIVE_CROP, payload: received.payload })
yield call(sagaNavigate, `/crop/trait/${received.payload.apiCall.data.id}`);
}
}
......@@ -159,19 +163,9 @@ function* editCropTraitSaga(action) {
yield take(ADMIN_RECEIVE_CROP_TRAIT);
const received = yield take(ADMIN_RECEIVE_CROP_TRAIT);
// yield take(RECEIVE_CROP);
// const received = yield take(RECEIVE_CROP);
//
// if (received.payload.apiCall.data) {
// console.log('navigate after creating');
// yield call(sagaNavigate, `/crop/${received.payload.apiCall.data.id}`);
// yield put({ type: ADMIN_RECEIVE_CROP, payload: received.payload })
// }
if (received.payload.apiCall.data) {
console.log('do something');
// const id = received.payload.apiCall.data.id;
// yield call(receiveCropDetailsSaga, { payload: { id } });
// yield call(sagaNavigate, `/crop/${received.payload.apiCall.data.id}`);
yield call(clearCropTrait);
yield call(sagaNavigate, `/crop/trait/${received.payload.apiCall.data.id}`);
}
}
......@@ -180,13 +174,11 @@ function* getCropTraitSaga(action) {
const publicCropTrait = yield select((state) => state.crop.public.trait);
if (publicCropTrait && publicCropTrait.data && +id === publicCropTrait.data.id) {
console.log('already in state, prevent api call', publicCropTrait);
yield put({
type: ADMIN_RECEIVE_CROP_TRAIT,
payload: { apiCall: publicCropTrait },
})
} else {
console.log('get trait admin call');
yield put({
type: 'API',
target: ADMIN_RECEIVE_CROP_TRAIT,
......@@ -198,3 +190,18 @@ function* getCropTraitSaga(action) {
});
}
}
function* removeCropTraitSaga(action) {
yield put({
type: 'API',
target: REMOVE_CROP_TRAIT,
method: CropTraitService.remove,
params: [action.payload.id],
onSuccess: (cropTrait: CropTrait) => {
return (function* () {
yield call(sagaNavigate, `/crop/${cropTrait.crop.id}`);
return cropTrait;
})();
},
});
}
......@@ -77,8 +77,6 @@ export const getCropTraitAction = (id: string | number) => ({
payload: { id },
});
export const removeCropTraitAction = (id: string | number) => null;
function* listCropsSaga(action) {
yield put({
type: 'API',
......@@ -167,3 +165,10 @@ function* uploadCropAttachmentSaga(action) {
},
});
}
export function* clearCropTrait() {
yield put({
type: RECEIVE_CROP_TRAIT,
payload: { apiCall: null },
});
}
......@@ -27,3 +27,6 @@ export const ADMIN_RECEIVE_CROP_TRAIT = 'crop/admin/RECEIVE_CROP_TRAIT';
export const SAGA_ADMIN_CREATE_CROP_TRAIT = 'saga/crop/public/CREATE_CROP_TRAIT';
export const SAGA_ADMIN_EDIT_CROP_TRAIT = 'saga/crop/public/EDIT_CROP_TRAIT';
export const SAGA_ADMIN_REMOVE_CROP_TRAIT = 'saga/crop/admin/REMOVE_CROP_TRAIT';
export const REMOVE_CROP_TRAIT = 'crop/public/REMOVE_CROP_TRAIT';
import update from 'immutability-helper';
// Constants
import { RECEIVE_CROPS, RECEIVE_CROP_DETAILS, RECEIVE_CROP_SPECIES, RECEIVE_CROP_ATTACHMENT, RECEIVE_CROP_TRAITS, RECEIVE_CROP_TRAIT } from 'crop/constants';
import { RECEIVE_CROPS, RECEIVE_CROP_DETAILS, RECEIVE_CROP_SPECIES, RECEIVE_CROP_ATTACHMENT, RECEIVE_CROP_TRAITS, RECEIVE_CROP_TRAIT, REMOVE_CROP_TRAIT } from 'crop/constants';
// Model
import { FilteredPage } from '@gringlobal/client/model/page';
import { ApiCall } from '@gringlobal/client/model/common';
import Crop from '@gringlobal/client/model/gringlobal/Crop';
import CropDetails from '@gringlobal/client/model/gringlobal/CropDetails';
import TaxonomySpecies from '@gringlobal/client/model/gringlobal/TaxonomySpecies';
import CropTrait from '@gringlobal/client/model/gringlobal/CropTrait';
import TranslatedCropTrait from '@gringlobal/client/model/gringlobal/TranslatedCropTrait';
const initialState: {
crop: ApiCall<CropDetails>,
crops: ApiCall<FilteredPage<Crop>>,
species: ApiCall<FilteredPage<TaxonomySpecies>>,
trait: ApiCall<CropTrait>,
traits: ApiCall<FilteredPage<CropTrait>>,
trait: ApiCall<TranslatedCropTrait>,
traits: ApiCall<FilteredPage<TranslatedCropTrait>>,
} = {
crop: null,
crops: null,
......@@ -91,6 +91,31 @@ const cropPublicReducer = (state = initialState, action) => {
}
return state;
}
case REMOVE_CROP_TRAIT: {
const { apiCall: { data } } = action.payload;
if (data) {
if (state.traits && state.traits.data && state.traits.data.content) {
const updatedTraits = state.traits.data.content.filter((trait) => trait.id !== data.id);
return update(state, {
trait: { $set: null },
traits: {
data: {
content: {
$set: updatedTraits,
},
totalElements: {
$apply: (total) => total - 1,
},
},
},
});
}
return update(state, {
trait: { $set: null },
});
}
return state;
}
default:
return state;
}
......
......@@ -31,12 +31,13 @@ import CropTrait from '@gringlobal/client/model/gringlobal/CropTrait';
import AddNewButton from '@gringlobal/client/ui/common/button/AddNewButton';
import { CodeValueDisplay } from 'common/CodeValue';
import SlotLayout from '@gringlobal/client/ui/common/layout/SlotLayout';
import TranslatedCropTrait from '@gringlobal/client/model/gringlobal/TranslatedCropTrait';
interface ICropDetailsPage extends React.ClassAttributes<any>, WithTranslation {
cropCall: ApiCall<CropDetails>;
speciesCall: ApiCall<FilteredPage<TaxonomySpecies>>;
traitsCall: ApiCall<FilteredPage<CropTrait>>;
traitsCall: ApiCall<FilteredPage<TranslatedCropTrait>>;
getCropDetailsAction: (id) => void;
getCropSpeciesAction: (id) => void;
getCropTraitsAction: (id) => void;
......@@ -49,6 +50,8 @@ export const CropTraitTablDefaultConfig = {
defaultColumns: [
'id',
'codedName',
'title',
'description',
'categoryCode',
'dataTypeCode',
'isArchived',
......@@ -68,9 +71,10 @@ export const CropTraitTablDefaultConfig = {
id: { readonly: true, align: TextAlign.right },
},
columnsRenderers: {
codedName: (codedName: string, cropTrait: CropTrait): JSX.Element => <CropTraitLink cropTrait={ cropTrait } />,
codedName: (codedName: string, cropTrait: TranslatedCropTrait): JSX.Element => <CropTraitLink cropTrait={ cropTrait } />,
dataTypeCode: (dataTypeCode: string) => <CodeValueDisplay codeGroup={ CropTrait.CodeValues.dataTypeCode } value={ dataTypeCode } />,
categoryCode: (categoryCode: string) => <CodeValueDisplay codeGroup={ CropTrait.CodeValues.categoryCode } value={ categoryCode } />,
originalValueTypeCode: (originalValueTypeCode: string) => <CodeValueDisplay codeGroup={ CropTrait.CodeValues.originalValueTypeCode } value={ originalValueTypeCode } />,
},
};
......@@ -87,6 +91,15 @@ class CropDetailsPage extends React.Component<ICropDetailsPage> {
selectedTab: 'info',
};
public constructor(props) {
super(props);
const { getCropSpeciesAction, getCropTraitsAction, id } = this.props;
console.log('Constructor calling load data');
getCropSpeciesAction(id);
getCropTraitsAction(id);
}
public componentDidMount(): void {
const { id, cropCall, getCropDetailsAction, getCropSpeciesAction, getCropTraitsAction } = this.props;
if (!cropCall || !cropCall.loading && (!cropCall.error && id && +id !== cropCall.data.id)) {
......
......@@ -3,11 +3,13 @@ import { bindActionCreators, compose } from 'redux';
import { connect } from 'react-redux';
import { WithTranslation, withTranslation } from 'react-i18next';
// Action
import { getCropTraitAction, removeCropTraitAction } from 'crop/action/public';
import { getCropTraitAction } from 'crop/action/public';
import { removeCropTraitAction } from 'crop/action/admin';
import navigateTo from '@gringlobal/client/action/navigation';
// Model
import ApiCall from '@gringlobal/client/model/common/ApiCall';
import CropTrait from '@gringlobal/client/model/gringlobal/CropTrait';
import TranslatedCropTrait from '@gringlobal/client/model/gringlobal/TranslatedCropTrait';
// Ui
import ContentHeader from '@gringlobal/client/ui/common/heading/ContentHeader';
import { Button, Card, CardActions, CardContent } from '@material-ui/core';
......@@ -21,7 +23,7 @@ import { YesNoToBoolean } from '@gringlobal/client/utilities';
interface ICropTraitDetailsPage extends React.ClassAttributes<any>, WithTranslation {
cropTraitCall: ApiCall<CropTrait>;
cropTraitCall: ApiCall<TranslatedCropTrait>;
getCropTraitAction: (id) => void;
id: string;
navigateTo: (path: string, query?: object) => void;
......@@ -71,45 +73,60 @@ class CropTraitDetailsPage extends React.Component<ICropTraitDetailsPage> {
<ContentHeader title={ t('crop.public.p.traitsDetails.title') }/>
{ loading && <Loading /> }
{ cropTrait && (
<div key={ cropTrait.id }>
<div>
<Card>
<CardContent>
<Properties>
{ ['codedName', 'maxLength', 'numericFormat', 'numericMaximum', 'numericMinimum', 'ontologyUrl', 'originalValueFormat', 'originalValueTypeCode', 'note'].map((property) => (
{ ['codedName', 'title', 'description', 'maxLength', 'numericFormat', 'numericMaximum', 'numericMinimum', 'ontologyUrl', 'originalValueFormat'].map((property) => property && (
<PropertiesItem key={ property } title={ t(`client:model.CropTrait.${property}`, `client:model._.${property}`) }>
{ cropTrait[property] }
</PropertiesItem>
)) }
{ cropTrait.crop &&
<PropertiesItem title={ t('client:model.CropTrait.crop') }>
{ cropTrait.crop.name }
</PropertiesItem>
}
<PropertiesItem title={ t('client:model.CropTrait.dataTypeCode') }>
<CodeValueDisplay codeGroup={ CropTrait.CodeValues.dataTypeCode } value={ cropTrait.dataTypeCode } />
</PropertiesItem>
<PropertiesItem title={ t('client:model.CropTrait.categoryCode') }>
<CodeValueDisplay codeGroup={ CropTrait.CodeValues.categoryCode } value={ cropTrait.categoryCode } />
</PropertiesItem>
{ cropTrait.originalValueTypeCode &&
<PropertiesItem title={ t('client:model.CropTrait.originalValueTypeCode') }>
<CodeValueDisplay codeGroup={ CropTrait.CodeValues.originalValueTypeCode } value={ cropTrait.originalValueTypeCode } />
</PropertiesItem>
}
{ ['isArchived', 'isCoded', 'isPeerReviewed'].map((property) => (
<PropertiesItem key={ property } title={ t(`client:model.CropTrait.${property}`, `client:model._.${property}`) }>
{ YesNoToBoolean(cropTrait[property]) ? t('common:label.yes') : t('common:label.no') }
</PropertiesItem>
)) }
{ cropTrait.createdDate &&
<PropertiesItem title={ t('client:model._.createdDate') }>
<PrettyDate value={ cropTrait.createdDate } />
</PropertiesItem>
<PropertiesItem title={ t('client:model._.createdDate') }>
<PrettyDate value={ cropTrait.createdDate } />
</PropertiesItem>
}
{ cropTrait.modifiedDate &&
<PropertiesItem title={ t('client:model._.modifiedDate') }>
<PrettyDate value={ cropTrait.modifiedDate } />
</PropertiesItem>
<PropertiesItem title={ t('client:model._.modifiedDate') }>
<PrettyDate value={ cropTrait.modifiedDate } />
</PropertiesItem>
}
{ cropTrait.ownedDate &&
<PropertiesItem title={ t('client:model._.ownedDate') }>
<PrettyDate value={ cropTrait.ownedDate } />
</PropertiesItem>
<PropertiesItem title={ t('client:model._.ownedDate') }>
<PrettyDate value={ cropTrait.ownedDate } />
</PropertiesItem>
}
{ cropTrait.ownedBy &&
<PropertiesItem title={ t('client:model._.ownedBy') }>
<CooperatorLink cooperator={ cropTrait.ownedBy }/>
</PropertiesItem>
<PropertiesItem title={ t('client:model._.ownedBy') }>
<CooperatorLink cooperator={ cropTrait.ownedBy }/>
</PropertiesItem>
}
{ cropTrait.note &&
<PropertiesItem title={ t('client:model._.note') }>
{ cropTrait.note }
</PropertiesItem>
}
</Properties>
</CardContent>
......
......@@ -13,6 +13,7 @@ import { Card, CardContent } from '@material-ui/core';
import CropTraitForm from 'crop/ui/admin/c/CropTraitForm';
import PageTitle from '@gringlobal/client/ui/common/PageTitle';
import Loading from '@gringlobal/client/ui/common/Loading';
import Crop from '@gringlobal/client/model/gringlobal/Crop';
interface ICropEditPage extends React.ClassAttributes<any> {
......@@ -21,6 +22,7 @@ interface ICropEditPage extends React.ClassAttributes<any> {