Commit 3371c805 authored by Oleksii Savran's avatar Oleksii Savran
Browse files

Crop: species, attachments

parent 5fef85a5
......@@ -8,6 +8,8 @@ import Crop from '@gringlobal/client/model/gringlobal/Crop';
import CropFilter from '@gringlobal/client/model/gringlobal/CropFilter';
import TaxonomySpeciesFilter from '@gringlobal/client/model/gringlobal/TaxonomySpeciesFilter';
import CropDetails from '@gringlobal/client/model/gringlobal/CropDetails';
import CropAttachmentRequest from '@gringlobal/client/model/gringlobal/CropAttachmentRequest';
import CropAttach from '@gringlobal/client/model/gringlobal/CropAttach';
const URL_REMOVE_FILE = UrlTemplate.parse('/api/v1/crop/attach/{cropId}/{attachmentId}');
const URL_UPLOAD_FILE = UrlTemplate.parse('/api/v1/crop/attach/{cropId}');
......@@ -71,28 +73,39 @@ class CropService {
url: apiUrl,
method: 'DELETE',
...content,
}).then(({ data }) => data as undefined); // todo: types
}).then(({ data }) => data as CropAttach);
}
/**
* uploadFile at /api/v1/crop/attach/{cropId}
*
* @param data Request body
* @param cropId undefined
* @param file File
* @param metadata metadata
* @param cropId crop ID
* @param xhrConfig additional xhr config
*/
public uploadFile = (data: object, cropId: number, xhrConfig?: AxiosRequestConfig): Promise<any>=> {
public uploadFile = (file: File, metadata: CropAttachmentRequest, cropId: number, xhrConfig?: AxiosRequestConfig): Promise<any>=> {
const apiUrl = URL_UPLOAD_FILE.expand({ cropId });
// console.log(any`Fetching from ${apiUrl}`);
const content = { data }; // todo: change to formData
const data = new FormData();
data.append('file', file);
if (metadata) {
data.append('metadata', new Blob([ JSON.stringify(metadata) ], { type : 'application/json' }));
}
const content = { data };
const oldHeaders = xhrConfig && xhrConfig.headers || {};
return this._axios.request({
...xhrConfig,
url: apiUrl,
method: 'POST',
headers: { ...oldHeaders, 'Content-Type': 'multipart/form-data' },
...content,
}).then(({ data }) => data as undefined);
}).then(({ data }) => {
return data as CropAttach;
});
};
/**
......
......@@ -2,6 +2,7 @@
"navigation": {
"accessionAction": "Accession actions",
"admin": "Admin tools",
"crops": "Crops",
"home": "Home",
"cooperators": "Cooperators",
"users": "Users",
......@@ -156,9 +157,6 @@
"c": {
"filters": {
"text": "Full-text search"
},
"attachmentsDisplay": {
"files": "Files"
}
}
}
......@@ -190,7 +188,12 @@
"title": "List of crops"
},
"details": {
"title": "Crop details"
"title": "Crop details",
"attachments": "Attachments",
"tabs": {
"traits": "Traits",
"species": "Species"
}
}
}
}
......@@ -469,6 +472,9 @@
"form": {
"name": "Folder name"
}
},
"attachmentsDisplay": {
"files": "Files"
}
}
},
......
......@@ -30,9 +30,6 @@
"c": {
"filters": {
"text": "Full-text search"
},
"attachmentsDisplay": {
"files": "Files"
}
}
}
......
......@@ -32,7 +32,7 @@ import { CodeValueDisplay } from 'common/CodeValue';
import ButtonBar from '@gringlobal/client/ui/common/button/ButtonBar';
import PageTitle from '@gringlobal/client/ui/common/PageTitle';
import FileUploader from '@gringlobal/client/ui/common/file-uploader';
import AttachmentsDisplay from 'accession/ui/c/AttachmentsDisplay';
import AttachmentsDisplay from 'repository/ui/c/AttachmentsDisplay';
const InventoryTableConfig = new TableConfiguration(TableConfiguration.merge(
......
......@@ -2,7 +2,6 @@ import { put, takeEvery, call, take } from 'redux-saga/effects';
// Constants
import {
RECEIVE_CROP,
ADMIN_RECEIVE_CROP,
SAGA_ADMIN_CREATE_CROP,
SAGA_ADMIN_RECEIVE_CROP,
......@@ -13,6 +12,7 @@ import Crop from '@gringlobal/client/model/gringlobal/Crop';
// Service
import { CropService } from '@gringlobal/client/service';
import { sagaNavigate } from '@gringlobal/client/action/navigation';
import { receiveCropDetailsSaga } from './public';
export const cropAdminSagas = [
......@@ -39,27 +39,29 @@ export const getCropActionAdmin = (id: string | number) => ({
function* createCropSaga(action) {
yield put({
type: 'API',
target: RECEIVE_CROP,
target: ADMIN_RECEIVE_CROP,
method: CropService.createCrop,
params: [action.payload.crop],
onSuccess: (crop: Crop) => {
return crop;
},
});
yield take(RECEIVE_CROP);
const received = yield take(RECEIVE_CROP);
yield take(ADMIN_RECEIVE_CROP);
const received = yield take(ADMIN_RECEIVE_CROP);
if (received.payload.apiCall.data) {
console.log('navigate after creating');
// 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 put({ type: ADMIN_RECEIVE_CROP, payload: received.payload })
}
}
function* editCropSaga(action) {
yield put({
type: 'API',
target: RECEIVE_CROP,
target: ADMIN_RECEIVE_CROP,
method: CropService.updateCrop,
params: [action.payload.crop],
onSuccess: (crop: Crop) => {
......@@ -67,13 +69,21 @@ function* editCropSaga(action) {
},
});
yield take(RECEIVE_CROP);
const received = yield take(RECEIVE_CROP);
yield take(ADMIN_RECEIVE_CROP);
const received = yield take(ADMIN_RECEIVE_CROP);
// 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('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 })
}
}
......
......@@ -3,22 +3,30 @@ import { put, takeEvery } from 'redux-saga/effects';
// Constants
import {
RECEIVE_CROPS,
RECEIVE_CROP,
RECEIVE_CROP_SPECIES,
RECEIVE_CROP_DETAILS,
RECEIVE_CROP_ATTACHMENT,
SAGA_RECEIVE_CROPS,
SAGA_RECEIVE_CROP,
SAGA_RECEIVE_CROP_DETAILS,
SAGA_RECEIVE_CROP_SPECIES,
SAGA_UPLOAD_CROP_ATTACHMENT,
} from 'crop/constants';
// Model
import Crop from '@gringlobal/client/model/gringlobal/Crop';
import { IPageRequest, FilteredPage, Page } from '@gringlobal/client/model/page';
import CropFilter from '@gringlobal/client/model/gringlobal/CropFilter';
import CropDetails from '@gringlobal/client/model/gringlobal/CropDetails';
// Service
import { CropService } from '@gringlobal/client/service';
import { dereferenceReferences3 } from '@gringlobal/client/utilities';
import CropFilter from '@gringlobal/client/model/gringlobal/CropFilter';
import CropAttach from '@gringlobal/client/model/gringlobal/CropAttach';
export const cropPublicSagas = [
takeEvery(SAGA_RECEIVE_CROPS, listCropsSaga),
takeEvery(SAGA_RECEIVE_CROP, receiveCropSaga),
takeEvery(SAGA_RECEIVE_CROP_DETAILS, receiveCropDetailsSaga),
takeEvery(SAGA_RECEIVE_CROP_SPECIES, receiveCropSpeciesSaga),
takeEvery(SAGA_UPLOAD_CROP_ATTACHMENT, uploadCropAttachmentSaga),
];
export const listCropsAction = (filter: Partial<CropFilter> = {}, pageR: IPageRequest = { page: 0, size: 100 }) => ({
......@@ -37,11 +45,21 @@ export const loadMoreCropsAction = (crops: FilteredPage<Crop>) => ({
},
});
export const getCropAction = (id: string | number) => ({
type: SAGA_RECEIVE_CROP,
export const getCropDetailsAction = (id: string | number) => ({
type: SAGA_RECEIVE_CROP_DETAILS,
payload: { id },
});
export const getCropSpeciesAction = (id: string| number) => ({
type: SAGA_RECEIVE_CROP_SPECIES,
payload: { id },
});
export const uploadCropAttachment = (id: number, file: File) => ({
type: SAGA_UPLOAD_CROP_ATTACHMENT,
payload: { id, file },
});
function* listCropsSaga(action) {
yield put({
type: 'API',
......@@ -50,7 +68,6 @@ function* listCropsSaga(action) {
params: [action.payload.filter, action.payload.pageR],
onSuccess: (crops: FilteredPage<Crop>) => {
dereferenceReferences3(crops.content, {
// _self: { id: [ '_self', 'ownedBy.crop' ] },
coo: { id: [ 'ownedBy' ] },
});
return crops;
......@@ -58,14 +75,48 @@ function* listCropsSaga(action) {
});
}
function* receiveCropSaga(action) {
export function* receiveCropDetailsSaga(action) {
yield put({
type: 'API',
target: RECEIVE_CROP,
method: CropService.get,
target: RECEIVE_CROP_DETAILS,
method: CropService.cropDetails,
params: [action.payload.id],
onSuccess: (crop: Crop) => {
onSuccess: (crop: CropDetails) => {
return crop;
},
});
}
function* receiveCropSpeciesSaga(action) {
yield put({
type: 'API',
target: RECEIVE_CROP_SPECIES,
method: CropService.listCropSpecies,
params: [{}, action.payload.id],
onSuccess: (data: any) => {
return data;
},
});
}
function* uploadCropAttachmentSaga(action) {
const { id, file } = action.payload;
const attachMetadata = new CropAttach();
attachMetadata.isWebVisible = 'Y';
attachMetadata.categoryCode = file.type.startsWith('image') ? 'IMAGE' : 'DOCUMENT'; // 'LINK'
const metadata = {
attachMetadata,
};
console.log('id, file:', id, file);
yield put({
type: 'API',
target: RECEIVE_CROP_ATTACHMENT,
method: CropService.uploadFile,
params: [file, metadata, id],
onSuccess: (attachment: CropAttach) => {
return attachment;
},
});
}
......@@ -4,8 +4,14 @@ export const SAGA_RECEIVE_CROPS = 'saga/crop/public/RECEIVE_CROPS';
export const SAGA_ADMIN_CREATE_CROP = 'saga/crop/public/CREATE_CROP';
export const SAGA_ADMIN_EDIT_CROP = 'saga/crop/public/EDIT_CROP';
export const SAGA_RECEIVE_CROP = 'saga/crop/public/RECEIVE_CROP';
export const RECEIVE_CROP = 'crop/public/RECEIVE_CROP';
export const SAGA_RECEIVE_CROP_DETAILS = 'saga/crop/public/RECEIVE_CROP_DETAILS';
export const RECEIVE_CROP_DETAILS = 'crop/public/RECEIVE_CROP_DETAILS';
export const SAGA_ADMIN_RECEIVE_CROP = 'saga/crop/admin/RECEIVE_CROP';
export const ADMIN_RECEIVE_CROP = 'crop/admin/RECEIVE_CROP';
export const SAGA_RECEIVE_CROP_SPECIES = 'saga/crop/public/RECEIVE_CROP_SPECIES';
export const RECEIVE_CROP_SPECIES = 'crop/public/RECEIVE_CROP_SPECIES';
export const SAGA_UPLOAD_CROP_ATTACHMENT = 'saga/crop/public/UPLOAD_CROP_ATTACHMENT';
export const RECEIVE_CROP_ATTACHMENT = 'crop/public/RECEIVE_CROP_ATTACHMENT';
import update from 'immutability-helper';
// Constants
import { RECEIVE_CROPS, RECEIVE_CROP } from 'crop/constants';
import { RECEIVE_CROPS, RECEIVE_CROP_DETAILS, RECEIVE_CROP_SPECIES, RECEIVE_CROP_ATTACHMENT } 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';
const initialState: {
crop: ApiCall<Crop>,
crop: ApiCall<CropDetails>,
crops: ApiCall<FilteredPage<Crop>>,
species: ApiCall<FilteredPage<TaxonomySpecies>>,
} = {
crop: null,
crops: null,
species: null,
};
const cropPublicReducer = (state = initialState, action) => {
......@@ -29,43 +33,40 @@ const cropPublicReducer = (state = initialState, action) => {
},
});
}
case RECEIVE_CROP: {
case RECEIVE_CROP_DETAILS: {
const { apiCall } = action.payload;
if (apiCall.data && state.crops) {
const { data: crops } = state.crops;
const crop = apiCall.data;
const updatedIndex = crops && crops.content && crops.content.findIndex((stateCrop) => +stateCrop.id === +crop.id);
if (updatedIndex !== undefined && updatedIndex !== -1) {
return update(state, {
crop: { $set: apiCall },
crops: {
data: {
content: {
[updatedIndex]: { $set: crop },
},
},
},
});
} else {
return update(state, {
crop: { $set: apiCall },
crops: {
data: {
content: {
$set: [...crops.content, crop],
},
},
},
});
}
}
return update(state, {
crop: { $set: apiCall },
});
}
case RECEIVE_CROP_SPECIES: {
const { apiCall: { loading, error, timestamp, data } } = action.payload;
return update(state, {
species: {
$set: {
loading,
error,
timestamp,
data: FilteredPage.merge(state.species && state.species.data, data),
},
},
});
}
case RECEIVE_CROP_ATTACHMENT: {
const { apiCall } = action.payload;
if (apiCall.data) {
return update(state, {
crop: {
data: {
attachments: {
$push: [apiCall.data],
},
},
},
});
}
return state;
}
default:
return state;
}
......
......@@ -5,7 +5,12 @@
"title": "List of crops"
},
"details": {
"title": "Crop details"
"title": "Crop details",
"attachments": "Attachments",
"tabs": {
"traits": "Traits",
"species": "Species"
}
}
}
}
......
......@@ -3,48 +3,69 @@ import { bindActionCreators, compose } from 'redux';
import { connect } from 'react-redux';
import { WithTranslation, withTranslation } from 'react-i18next';
// Action
import { getCropAction } from 'crop/action/public';
import { getCropDetailsAction, getCropSpeciesAction, uploadCropAttachment } from 'crop/action/public';
import navigateTo from '@gringlobal/client/action/navigation';
// Model
import ApiCall from '@gringlobal/client/model/common/ApiCall';
import Crop from '@gringlobal/client/model/gringlobal/Crop';
import CropDetails from '@gringlobal/client/model/gringlobal/CropDetails';
import { UserRole } from '@gringlobal/client/model/gringlobal/SysUser';
import { FilteredPage } from '@gringlobal/client/model/page';
import TaxonomySpecies from '@gringlobal/client/model/gringlobal/TaxonomySpecies';
// Ui
import ContentHeader from '@gringlobal/client/ui/common/heading/ContentHeader';
import { Button, Card, CardActions, CardContent } from '@material-ui/core';
import { Button, Card, CardActions, CardContent, CardHeader } from '@material-ui/core';
import { Properties, PropertiesItem } from '@gringlobal/client/ui/common/Properties';
import Loading from '@gringlobal/client/ui/common/Loading';
import { CooperatorLink } from 'ui/common/Links';
import PrettyDate from '@gringlobal/client/ui/common/time/PrettyDate';
import PageTitle from '@gringlobal/client/ui/common/PageTitle';
import Authorize from '@gringlobal/client/ui/common/authorized/Authorize';
import Tab from '@material-ui/core/Tab/Tab';
import HeaderTabs from '@gringlobal/client/ui/common/tabs/HeaderTabs';
import { CooperatorOwnedTableConfiguration as TableConfiguration } from '@gringlobal/client/ui/common/table/TableConfiguration';
import { TaxonomySpeciesTableDefaultConfig } from 'taxonomy/ui/TaxonomySpeciesBrowsePage';
import TabPanel from '@gringlobal/client/ui/common/tabs/TabPanel';
import Table from '@gringlobal/client/ui/common/table/Table';
import AttachmentsDisplay from 'repository/ui/c/AttachmentsDisplay';
import FileUploader from '@gringlobal/client/ui/common/file-uploader';
interface ICropDetailsPage extends React.ClassAttributes<any>, WithTranslation {
cropCall: ApiCall<Crop>;
getCropAction: (id) => void;
cropCall: ApiCall<CropDetails>;
speciesCall: ApiCall<FilteredPage<TaxonomySpecies>>;
getCropDetailsAction: (id) => void;
getCropSpeciesAction: (id) => void;
id: string;
navigateTo: (path: string, query?: object) => void;
uploadCropAttachment: (id: number, file: File) => void;
}
const TaxonomySpeciesTableConfig = new TableConfiguration(TaxonomySpeciesTableDefaultConfig);
class CropDetailsPage extends React.Component<ICropDetailsPage> {
protected static needs = [
({ params: { id } }) => getCropAction(id),
({ params: { id } }) => getCropDetailsAction(id),
];
public state = {
selectedTab: 'info',
};
public componentDidMount(): void {
const { id, cropCall, getCropAction } = this.props;
const { id, cropCall, getCropDetailsAction, getCropSpeciesAction } = this.props;
if (!cropCall || !cropCall.loading && (!cropCall.error && id && +id !== cropCall.data.id)) {
getCropAction(id);
getCropDetailsAction(id);
getCropSpeciesAction(id);
}
}
public componentDidUpdate() {
const { cropCall, id, getCropAction } = this.props;
const { cropCall, id, getCropDetailsAction, getCropSpeciesAction } = this.props;
if (!cropCall || !cropCall.loading && (!cropCall.error && id && +id !== cropCall.data.id)) {
// console.log('load on didUpdate');
getCropAction(id);
getCropDetailsAction(id);
getCropSpeciesAction(id);
}
}
......@@ -53,8 +74,22 @@ class CropDetailsPage extends React.Component<ICropDetailsPage> {
navigateTo(`/admin/crop/edit/${id}`);
};
private selectTab = (event, newValue) => {
this.setState({
selectedTab: newValue,
});
};
private handleUploading = (files: File[]) => {
const { cropCall, uploadCropAttachment } = this.props;
uploadCropAttachment(cropCall.data.id, files[0]);
};
public render() {
const { cropCall, t } = this.props;
const { cropCall, t, speciesCall } = this.props;
const { selectedTab } = this.state;
const columns = TaxonomySpeciesTableConfig.getColumns(speciesCall && speciesCall.data && speciesCall.data.content ? speciesCall.data.content[0] : null)
if (!cropCall) {
return null;
}
......@@ -62,49 +97,92 @@ class CropDetailsPage extends React.Component<ICropDetailsPage> {
return (
<>
<PageTitle title={ t('crop.public.p.details.title') }/>
<ContentHeader title={ t('crop.public.p.details.title') }/>
<HeaderTabs
value={ selectedTab }
textColor="primary"
onChange={ this.selectTab }
variant="scrollable"
scrollButtons="auto"
aria-label="Inventory tabs"
>
<Tab value="info" label={ t('crop.public.p.details.title') } />
<Tab value="attachments" label={ t('crop.public.p.details.attachments') } />
<Tab value="traits" label={ t('crop.public.p.details.tabs.traits') } />
<Tab value="species" label={ t('crop.public.p.details.tabs.species') } />
</HeaderTabs>
{ loading && <Loading /> }
{ crop && (
<div key={ crop.id }>
<Card>
<CardContent>
<Properties>
{ crop.name &&
<PropertiesItem title={ t('client:model.Crop.name') }>
{ crop.name }
</PropertiesItem>
}
{ crop.createdDate &&
<PropertiesItem title={ t('client:model._.createdDate') }>
<PrettyDate value={ crop.createdDate } />
</PropertiesItem>
}
{ crop.modifiedDate &&
<PropertiesItem title={ t('client:model._.modifiedDate') }>
<PrettyDate value={ crop.modifiedDate } />
</PropertiesItem>
}
{ crop.ownedDate &&
<PropertiesItem title={ t('client:model._.ownedDate') }>
<PrettyDate value={ crop.ownedDate } />
</PropertiesItem>
}
{ crop.ownedBy &&
<PropertiesItem title={ t('client:model._.ownedBy') }>
<CooperatorLink cooperator={ crop.ownedBy }/>