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

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"
}
}
}
}
......
......@@ -12,6 +12,7 @@ import ContentHeader from '@gringlobal/client/ui/common/heading/ContentHeader';
import { Card, CardContent } from '@material-ui/core';
import CropForm from 'crop/ui/admin/c/CropForm';
import PageTitle from '@gringlobal/client/ui/common/PageTitle';
import Loading from '@gringlobal/client/ui/common/Loading';
interface ICropEditPage extends React.ClassAttributes<any> {
......@@ -58,7 +59,7 @@ class CropEditPage extends React.Component<ICropEditPage & WithTranslation> {
};
public render(): React.ReactNode {
const { t, cropCall } = this.props;
const { t, cropCall, id } = this.props;
return (
<>
......@@ -71,7 +72,9 @@ class CropEditPage extends React.Component<ICropEditPage & WithTranslation> {
: t('crop.admin.p.edit.title')
}
/>
<>
{ id && cropCall && cropCall.loading ? (
<Loading/>
) : (
<Card>
<CardContent>
<CropForm
......@@ -80,7 +83,7 @@ class CropEditPage extends React.Component<ICropEditPage & WithTranslation> {
/>
</CardContent>
</Card>
</>
) }
</>
);
}
......
......@@ -23,7 +23,7 @@ import Inventory from '@gringlobal/client/model/gringlobal/Inventory';
import PageTitle from '@gringlobal/client/ui/common/PageTitle';
import InventoryAction from '@gringlobal/client/model/gringlobal/InventoryAction';
import { BasicInventoryActionsTable as InventoryActionsTable } from 'inventory/ui/c/InventoryActionsTable';
import AttachmentsDisplay from 'accession/ui/c/AttachmentsDisplay';
import AttachmentsDisplay from 'repository/ui/c/AttachmentsDisplay';
import FileUploader from '@gringlobal/client/ui/common/file-uploader';
......
......@@ -58,6 +58,9 @@
"form": {
"name": "Folder name"
}
},
"attachmentsDisplay": {
"files": "Files"
}
}
},
......
......@@ -19,6 +19,7 @@ import RepositoryImage from '@gringlobal/client/model/repository/RepositoryImage
import Grid from '@material-ui/core/Grid/Grid';
import DownloadIcon from '@material-ui/icons/GetApp';
import ImageGallery from '@gringlobal/client/model/repository/ImageGallery';
import CropAttach from '@gringlobal/client/model/gringlobal/CropAttach';
const styles = (theme) => createStyles({
......@@ -55,8 +56,11 @@ const styles = (theme) => createStyles({
},
});
type Attachment = AccessionInvAttach | CropAttach;
interface IAttachmentsDisplay extends React.ClassAttributes<any>, WithStyles, WithTranslation {
attachments: AccessionInvAttach[];
attachments: Attachment[];
title: string;
apiUrl: string;
}
......@@ -65,7 +69,7 @@ class AttachmentsDisplay extends React.Component<IAttachmentsDisplay, any> {
super(props);
}
private getFileGroups = memoize((attachments: AccessionInvAttach[]) => {
private getFileGroups = memoize((attachments: Attachment[]) => {
const images = [];
const rest = [];
......@@ -82,7 +86,7 @@ class AttachmentsDisplay extends React.Component<IAttachmentsDisplay, any> {
});
public render() {
const { attachments, classes, apiUrl, t } = this.props;
const { attachments, classes, apiUrl, t, title } = this.props;
const [images, rest] = this.getFileGroups(attachments);
return (
......@@ -101,7 +105,7 @@ class AttachmentsDisplay extends React.Component<IAttachmentsDisplay, any> {
{ rest && rest.length > 0 && (
<Grid item xs={ 12 }>
<Card className={ classes.root }>
<CardHeader title={ t('accession.public.c.attachmentsDisplay.files') }/>
<CardHeader title={ t(title || 'repository.public.c.attachmentsDisplay.files') }/>
<CardContent>
<List>
{ rest && rest.length > 0 && rest.map((file, i) => (
......
......@@ -2,6 +2,7 @@
"navigation": {
"accessionAction": "Accession actions",
"admin": "Admin tools",
"crops": "Crops",
"home": "Home",
"cooperators": "Cooperators",
"users": "Users",
......
......@@ -71,7 +71,11 @@ function PublicMenu(): JSX.Element {
</>
</Authorize>
<Divider />
<Link to="/crop">
<ListItem button>
<ListItemText primary={ t('navigation.crops') } />
</ListItem>
</Link>
<Link to="/t/species">
<ListItem button>
<ListItemText primary={ t('navigation.taxonomySpecies') } />
......
......@@ -175,7 +175,16 @@ class WelcomePage extends React.Component<WithTranslation & WithStyles> {
</Authorize>
<SectionHeader title={ t('welcome.p.sections.taxonomy') } className="m-halfrem pt-5 pb-5"/>
<Grid item xs={ 12 } sm={ 6 } md={ 4 } lg={ 3 }>
<Link to="/crop">
<Card className={ classes.card }>
<CardHeader title={ t('welcome.p.cards.crop.title') }/>
<CardContent>
<div>{ t('welcome.p.cards.crop.description') }</div>
</CardContent>
</Card>
</Link>
</Grid>
<Grid item xs={ 12 } sm={ 6 } md={ 4 } lg={ 3 }>
<Link to="/t/species">
<Card className={ classes.card }>
......@@ -251,17 +260,6 @@ class WelcomePage extends React.Component<WithTranslation & WithStyles> {
</Grid>
</Authorize>
<Grid item xs={ 12 } sm={ 6 } md={ 4 } lg={ 3 }>
<Link to="/crop">
<Card className={ classes.card }>
<CardHeader title={ t('welcome.p.cards.crop.title') }/>
<CardContent>
<div>{ t('welcome.p.cards.crop.description') }</div>
</CardContent>
</Card>
</Link>
</Grid>
</Grid>
</div>
</>
......
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