Commit a4660921 authored by Matija Obreza's avatar Matija Obreza

Merge branch '126-basic-inventory-form' into 'master'

Created Inventory Edit page with Inventory form.

Closes #126

See merge request !105
parents 00dc5b43 056c7435
......@@ -8,6 +8,7 @@ import InventoryMaintenancePolicy from '@gringlobal/client/model/gringlobal/Inve
import InventoryQualityStatus from '@gringlobal/client/model/gringlobal/InventoryQualityStatus';
import InventoryViability from '@gringlobal/client/model/gringlobal/InventoryViability';
import Method from '@gringlobal/client/model/gringlobal/Method';
import Site from '@gringlobal/client/model/gringlobal/Site';
/**
* InventoryDetails
......@@ -15,13 +16,14 @@ import Method from '@gringlobal/client/model/gringlobal/Method';
* GRIN-Global CE API
*/
class InventoryDetails {
public createdBy: number;
public createdBy: Cooperator;
public createdDate: Date;
public modifiedBy: number;
public modifiedBy: Cooperator;
public modifiedDate: Date;
public ownedBy: Cooperator;
public ownedDate: Date;
public accession: Accession;
public site: Site;
public availabilityEndDate: Date;
public availabilityStartDate: Date;
public availabilityStatusCode: string;
......@@ -61,6 +63,7 @@ class InventoryDetails {
public storageLocationPart3: string;
public storageLocationPart4: string;
public webAvailabilityNote: string;
public generation: number;
public inventoryMaintPolicy: InventoryMaintenancePolicy;
public systemInventory: boolean;
public inventoryNumber: string;
......
......@@ -474,7 +474,13 @@ class InventoryService {
url: apiUrl,
method: 'GET',
...content,
}).then(({ data }) => data as InventoryDetails);
}).then(({ data }) => {
dereferenceReferences3([data], {
coo: { id: [ 'ownedBy', 'modifiedBy', 'createdBy' ] },
sit: { id: [ 'site', 'ownedBy.site' ] },
});
return data as InventoryDetails
});
}
......
......@@ -246,7 +246,18 @@
"auditLogs": "Audit Logs",
"quality": "Quality",
"viability": "Viability",
"names": "Inventory names"
"names": "Inventory names",
"delete": "Do you really want to delete {{name}} inventory?"
},
"edit": {
"title": "New inventory",
"inventory": "Inventory",
"switches": "Switches",
"availability": "Availability",
"site": "Site",
"error": "Error",
"storageLocation": "Storage location",
"otherFields": "Other fields"
},
"summary": {
"title": "Inventory summary",
......
import * as React from 'react';
import { AccessionService } from '@gringlobal/client/service';
import Accession from '@gringlobal/client/model/gringlobal/Accession';
import { FieldRenderProps } from 'react-final-form';
import { FilteredPage } from '@gringlobal/client/model/page';
import Autocomplete from '@gringlobal/client/ui/common/form/Autocomplete';
interface IAccessionField {
label: string;
}
export default class AccessionField extends React.Component<IAccessionField & FieldRenderProps<any>> {
public constructor(props) {
super(props);
}
private autocomplete = (text: string) => {
return AccessionService.filter({ _text: `${text}*` }, { page: 0, size: 20 });
};
private renderOption = (accession: Accession, state?: object) => <>{ accession.accessionNumber } { accession.note }</>
private optionLabel = (accession: Accession): string => accession.accessionNumber;
private mapOptions = (page: FilteredPage<Accession>) => page.content;
public render() {
return (
<Autocomplete
{ ...this.props }
autocomplete={ this.autocomplete }
renderOption={ this.renderOption }
mapOptions={ this.mapOptions }
getOptionLabel={ this.optionLabel }
/>
);
}
}
......@@ -4,6 +4,7 @@ import { put, takeEvery, take, call } from 'redux-saga/effects';
import {
RECEIVE_INVENTORY,
RECEIVE_INVENTORY_LIST,
RECEIVE_INVENTORY_ITEM,
SAGA_RECEIVE_INVENTORY,
SAGA_LIST_INVENTORY,
RECEIVE_INVENTORY_ACTIONS_LIST,
......@@ -11,6 +12,9 @@ import {
RECEIVE_INVENTORY_ATTACHMENT,
UPLOAD_ATTACHMENT_SUCCESS,
SAGA_UPLOAD_INVENTORY_ATTACHMENT,
REMOVE_INVENTORY,
SAGA_REMOVE_INVENTORY,
SAGA_RECEIVE_INVENTORY_SUCCESS,
} from 'inventory/constants';
// Model
import Inventory from '@gringlobal/client/model/gringlobal/Inventory';
......@@ -21,6 +25,8 @@ import { IPageRequest, FilteredPage, Page } from '@gringlobal/client/model/page'
// Service
import { InventoryService } from '@gringlobal/client/service';
import AccessionInvAttach from '@gringlobal/client/model/gringlobal/AccessionInvAttach';
import { sagaNavigate } from '@gringlobal/client/action/navigation';
import { ApiCall } from '@gringlobal/client/model/common';
......@@ -29,6 +35,8 @@ export const inventoryPublicSagas = [
takeEvery(SAGA_RECEIVE_INVENTORY, getInventorySaga),
takeEvery(SAGA_LIST_INVENTORY_ACTIONS, listInventoryActionsSaga),
takeEvery(SAGA_UPLOAD_INVENTORY_ATTACHMENT, uploadInventoryAttachmentSaga),
takeEvery(SAGA_REMOVE_INVENTORY, removeInventorySaga),
takeEvery(SAGA_RECEIVE_INVENTORY_SUCCESS, receiveInventorySaga),
];
export const getInventoryAction = (id: number | string) => ({
......@@ -72,6 +80,39 @@ function * listInventorySaga(action) {
});
}
function* removeInventorySaga(action) {
yield put({
type: 'API',
target: REMOVE_INVENTORY,
method: InventoryService.removeInventory,
params: [action.payload.id],
onSuccess: (inventory: Inventory) => {
return (function* () {
yield call(sagaNavigate, '/inventory');
return inventory;
})();
},
});
}
function* receiveInventorySaga(action) {
yield put({
type: RECEIVE_INVENTORY_ITEM,
payload: { apiCall: ApiCall.success(action.payload.inventory) },
});
}
export const removeInventoryAction = (id: number) => ({
type: SAGA_REMOVE_INVENTORY,
payload: { id },
});
export const receiveInventorySuccessAction = (inventory: Inventory) => ({
type: SAGA_RECEIVE_INVENTORY_SUCCESS,
payload: { inventory },
});
export const listInventoryActionsAction = (filter: InventoryActionFilter = {}, pageR: IPageRequest = { page: 0, size: 100 }) => ({
type: SAGA_LIST_INVENTORY_ACTIONS,
payload: {
......
export const RECEIVE_INVENTORY_LIST = 'success/inventory/public/RECEIVE_INVENTORY_LIST';
export const RECEIVE_INVENTORY = 'success/inventory/public/RECEIVE_INVENTORY';
export const RECEIVE_INVENTORY_ITEM = 'success/inventory/public/RECEIVE_INVENTORY_ITEM';
export const SAGA_LIST_INVENTORY = 'saga/inventory/public/SAGA_LIST_INVENTORY';
export const SAGA_RECEIVE_INVENTORY = 'saga/inventory/public/RECEIVE_INVENTORY';
export const SAGA_RECEIVE_INVENTORY_SUCCESS = 'saga/INVENTORY_GROUP/public/RECEIVE_INVENTORY_SUCCESS';
export const RECEIVE_INVENTORY_ACTIONS_LIST = 'success/inventory/public/RECEIVE_INVENTORY_ACTIONS_LIST';
export const SAGA_LIST_INVENTORY_ACTIONS = 'saga/inventory/public/RECEIVE_INVENTORY_ACTIONS_LIST';
......@@ -10,3 +12,7 @@ export const SAGA_LIST_INVENTORY_ACTIONS = 'saga/inventory/public/RECEIVE_INVENT
export const RECEIVE_INVENTORY_ATTACHMENT = 'success/inventory/public/RECEIVE_INVENTORY_ATTACHMENT';
export const SAGA_UPLOAD_INVENTORY_ATTACHMENT = 'saga/inventory/public/UPLOAD_INVENTORY_ATTACHMENT';
export const UPLOAD_ATTACHMENT_SUCCESS = 'saga/inventory/public/UPLOAD_ATTACHMENT_SUCCESS';
export const REMOVE_INVENTORY = 'success/inventory/public/REMOVE_INVENTORY';
export const SAGA_REMOVE_INVENTORY = 'saga/inventory/public/REMOVE_INVENTORY';
......@@ -5,6 +5,8 @@ import {
RECEIVE_INVENTORY,
RECEIVE_INVENTORY_ACTIONS_LIST,
RECEIVE_INVENTORY_ATTACHMENT,
REMOVE_INVENTORY,
RECEIVE_INVENTORY_ITEM,
} from 'inventory/constants';
// Model
import Inventory from '@gringlobal/client/model/gringlobal/Inventory';
......@@ -57,11 +59,46 @@ const inventoryPublicReducer = (state = initialState, action) => {
});
}
}
return update(state, {
inventory: { $set: apiCall },
});
}
case RECEIVE_INVENTORY_ITEM: {
const { apiCall } = action.payload;
if (apiCall.data && state.inventoryList) {
const { data: inventoryList } = state.inventoryList;
const inventory = apiCall.data;
const updatedIndex = inventoryList && inventoryList.content && inventoryList.content.findIndex((stateInventory) => +stateInventory.id === +inventory.id);
if (updatedIndex !== undefined && updatedIndex !== -1) {
return update(state, {
inventory: { $set: null },
inventoryList: {
data: {
content: {
[updatedIndex]: { $set: inventory },
},
},
},
});
} else {
return update(state, {
inventory: { $set: null },
inventoryList: {
data: {
content: {
$set: [...inventoryList.content, inventory],
},
},
},
});
}
}
return update(state, {
inventory: { $set: null },
});
}
case RECEIVE_INVENTORY_LIST: {
const { apiCall: { loading, error, timestamp, data } } = action.payload;
return update(state, {
......@@ -100,6 +137,31 @@ const inventoryPublicReducer = (state = initialState, action) => {
},
});
}
case REMOVE_INVENTORY: {
const { apiCall: { data } } = action.payload;
if (data) {
if (state.inventoryList && state.inventoryList.data && state.inventoryList.data.content) {
const updatedInventory = state.inventoryList.data.content.filter((inventory) => inventory.id !== data.id);
return update(state, {
inventory: { $set: null },
inventoryList: {
data: {
content: {
$set: updatedInventory,
},
totalElements: {
$apply: (total) => total - 1,
},
},
},
});
}
return update(state, {
inventory: { $set: null },
});
}
return state;
}
default:
return state;
}
......
......@@ -18,6 +18,20 @@ const publicRoutes: IRoute[] = [
}),
path: '/inventory/:id(\\d+)',
},
{
exact: true,
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "inventory" */'inventory/ui/InventoryEditPage'),
}),
path: '/inventory/edit/:id(\\d+)',
},
{
exact: true,
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "inventory" */'inventory/ui/InventoryEditPage'),
}),
path: '/inventory/edit',
},
{
exact: true,
component: Loadable({
......
......@@ -19,7 +19,18 @@
"auditLogs": "Audit Logs",
"quality": "Quality",
"viability": "Viability",
"names": "Inventory names"
"names": "Inventory names",
"delete": "Do you really want to delete {{name}} inventory?"
},
"edit": {
"title": "New inventory",
"inventory": "Inventory",
"switches": "Switches",
"availability": "Availability",
"site": "Site",
"error": "Error",
"storageLocation": "Storage location",
"otherFields": "Other fields"
},
"summary": {
"title": "Inventory summary",
......
......@@ -5,6 +5,7 @@ import { WithTranslation, withTranslation } from 'react-i18next';
// Action
import { listInventoryAction, loadMoreInventoryAction } from 'inventory/action/public';
import navigateTo from '@gringlobal/client/action/navigation';
// Model
import Accession from '@gringlobal/client/model/gringlobal/Accession';
import Inventory from '@gringlobal/client/model/gringlobal/Inventory';
......@@ -30,6 +31,7 @@ interface IBrowsePageProps extends React.ClassAttributes<any>, WithTranslation,
applyFilter: (filter: InventoryFilter) => void;
loadMore: () => void;
data: FilteredPage<Inventory>;
navigateTo: (path: string, query?: object) => void;
}
export const InventoryTableDefaultConfig = {
......@@ -106,6 +108,11 @@ class BrowsePage extends React.Component<IBrowsePageProps> {
console.log(`Column ${toggledColumn} was toggled. Have ${selectedColumns}`);
};
private handleEdit = () => {
const { navigateTo } = this.props;
navigateTo('/inventory/edit');
};
public render() {
const { data, t, onSortChange, applyFilter, loadMore } = this.props;
const columns = InventoryTableConfig.getColumns(data && data.content ? data.content[0] : null);
......@@ -132,7 +139,7 @@ class BrowsePage extends React.Component<IBrowsePageProps> {
sort={ data && data.sort }
onSortChange={ onSortChange }
/>
<AddNewButton/>
<AddNewButton action={ this.handleEdit }/>
</>
);
}
......@@ -147,6 +154,7 @@ const mapStateToProps = (state) => ({
const mapDispatchToProps = (dispatch) => bindActionCreators({
listAction: listInventoryAction,
loadMoreData: loadMoreInventoryAction,
navigateTo,
}, dispatch);
......
......@@ -11,7 +11,8 @@ import Page from '@gringlobal/client/model/page/Page';
import { SortDirection } from '@gringlobal/client/model/page';
// Action
import { ApiCall } from '@gringlobal/client/model/common';
import { getInventoryAction, uploadInventoryAttachment } from 'inventory/action/public';
import { getInventoryAction, uploadInventoryAttachment, removeInventoryAction } from 'inventory/action/public';
import navigateTo from '@gringlobal/client/action/navigation';
// Service
import { InventoryService } from '@gringlobal/client/service';
// UI
......@@ -31,8 +32,10 @@ import { BasicInventoryActionsTable as InventoryActionsTable } from 'inventory/
import { InventoryAuditLogsTable } from 'inventory/ui/c/InventoryAuditLogsTable';
import AttachmentsDisplay from 'repository/ui/c/AttachmentsDisplay';
import FileUploader from '@gringlobal/client/ui/common/file-uploader';
import confirm from '@gringlobal/client/utilities/confirmAlert';
import AuditDataDisplay from 'common/AuditDataDisplay';
import { GridContainer, GridItem } from '@gringlobal/client/ui/common/grid';
import { showSnackbar } from '@gringlobal/client/action/snackbar';
interface IDetailsPageProps extends React.ClassAttributes<any>, WithTranslation {
......@@ -40,6 +43,9 @@ interface IDetailsPageProps extends React.ClassAttributes<any>, WithTranslation
getInventoryAction: (id: string | number) => void;
inventoryCall: ApiCall<InventoryDetails>;
uploadInventoryAttachment: (id: number, file: File) => void;
removeInventoryAction: (id: number) => void;
navigateTo: (path: string, query?: object) => void;
showSnackbar: (error: string) => void;
}
const INFO_TAB: string = 'info';
......@@ -107,6 +113,24 @@ class InventoryDetailsPage extends React.Component<IDetailsPageProps> {
uploadInventoryAttachment(inventoryCall.data.id, files[0]);
};
private handleEdit = () => {
const { navigateTo, id } = this.props;
navigateTo(`/inventory/edit/${id}`);
};
private handleRemove = () => {
const { removeInventoryAction, t, id, inventoryCall, showSnackbar } = this.props;
confirm(t('inventory.public.p.details.delete', { name: inventoryCall.data.inventoryNumber }), {
confirmLabel: t('common:label.yes'),
abortLabel: t('common:label.no'),
}).then(() => {
removeInventoryAction(+id);
}).catch((e) => {
showSnackbar(e.data && e.data.error || e.toString());
});
};
public render(): React.ReactNode {
const { inventoryCall, t } = this.props;
if (!inventoryCall) {
......@@ -147,10 +171,10 @@ class InventoryDetailsPage extends React.Component<IDetailsPageProps> {
<CardActions>
<ButtonBar>
<Button variant="contained" color="primary">Test</Button>
<Button variant="contained" color="secondary">Edit</Button>
<Button variant="outlined" color="secondary">Remove</Button>
<Button variant="text" color="secondary">Do something</Button>
{/* <Button variant="contained" color="primary">Test</Button> */}
<Button onClick={ this.handleEdit } variant="contained" color="primary">Edit</Button>
<Button onClick={ this.handleRemove } variant="text" color="secondary">Remove</Button>
{/* <Button variant="text" color="secondary">Do something</Button> */}
</ButtonBar>
</CardActions>
......@@ -456,6 +480,9 @@ const mapStateToProps = (state, ownProps) => ({
const mapDispatchToProps = (dispatch) => bindActionCreators({
getInventoryAction,
uploadInventoryAttachment,
removeInventoryAction,
navigateTo,
showSnackbar,
}, dispatch);
......
import * as React from 'react';
import { connect } from 'react-redux';
import { withTranslation, WithTranslation } from 'react-i18next';
import { ApiCall } from '@gringlobal/client/model/common';
import { getInventoryAction, receiveInventorySuccessAction } from 'inventory/action/public';
import PageTitle from '@gringlobal/client/ui/common/PageTitle';
import Loading from '@gringlobal/client/ui/common/Loading';
import { bindActionCreators, compose } from 'redux';
import ContentHeader from '@gringlobal/client/ui/common/heading/ContentHeader';
import Inventory from '@gringlobal/client/model/gringlobal/Inventory';
import InventoryForm from 'inventory/ui/c/InventoryForm';
import { InventoryService } from '@gringlobal/client/service';
import navigateTo from '@gringlobal/client/action/navigation';
interface IEditPageProps extends React.ClassAttributes<any>, WithTranslation {
id: string;
receiveInventorySuccessAction: (inventory: Inventory) => void;
createInventoryAction: (inventory: Inventory) => void;
editInventoryAction: (inventory: Inventory) => void;
getInventoryAction: (id) => void;
navigateTo: (path: string, query?: object) => void;
inventoryCall: ApiCall<Inventory>;
}
class InventoryEditPage extends React.Component<IEditPageProps> {
public state = {
apiError: null,
};
protected static needs = [
({ params: { id } }) => getInventoryAction(id),
];
public constructor(props) {
super(props);
}
public componentDidMount(): void {
const { getInventoryAction, id, inventoryCall } = this.props;
if (!id) { return }
if (!inventoryCall || !inventoryCall.data || inventoryCall.data.id !== +id) {
getInventoryAction(id);
}
}
public componentDidUpdate() {
const { inventoryCall, id, getInventoryAction } = this.props;
if (!id) { return }
if (!inventoryCall || !inventoryCall.loading && (!inventoryCall.error && id && +id !== inventoryCall.data.id)) {
getInventoryAction(id);
}
}
private handleSubmit = (inventory: Inventory) => {
const { receiveInventorySuccessAction, id, navigateTo } = this.props;
let inventoryPromise: Promise<Inventory>;
this.setState({ apiError: null });
if (id) {
inventoryPromise = InventoryService.updateInventory(inventory)
} else {
inventoryPromise = InventoryService.createInventory(inventory)
}
inventoryPromise.then((inventory) => {
navigateTo(`/inventory/${inventory.id}`);
receiveInventorySuccessAction(inventory);
}).catch((e) => {
this.setState({ apiError: e.data && e.data.error || e.toString() });
});
};
private getTitle = (inventoryCall, id, t) => {
if (inventoryCall && inventoryCall.loading) {
return t('common:label.loadingData')
}
if (id && inventoryCall && inventoryCall.data) {
return inventoryCall.data.inventoryNumber
}
return t('inventory.public.p.edit.title')
}
public render(): React.ReactNode {
const { inventoryCall,id, t } = this.props;
const { apiError } = this.state;
return (
<>
<PageTitle title={ this.getTitle(inventoryCall, id, t) }/>
<ContentHeader
title={ this.getTitle(inventoryCall, id, t) }
/>
{ id && inventoryCall && inventoryCall.loading ? (
<Loading/>
) : (
<InventoryForm
initialValues={ id && inventoryCall ? inventoryCall.data : {} }
onSubmit={ this.handleSubmit }
error={ apiError }
/>
) }
</>
);
}
}
const mapStateToProps = (state, ownProps) => ({
id: ownProps.match.params.id,
inventoryCall: state.inventory.public.inventory,
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
getInventoryAction,
receiveInventorySuccessAction,
navigateTo,
}, dispatch);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withTranslation(),
)(InventoryEditPage);
This diff is collapsed.
......@@ -27,6 +27,7 @@ import PageTitle from '@gringlobal/client/ui/common/PageTitle';
import AuditDataDisplay from 'common/AuditDataDisplay';
import EditGroupDialog from 'inventorygroup/ui/c/EditGroupDialog';
import confirm from '@gringlobal/client/utilities/confirmAlert';
import Narrative from '@gringlobal/client/ui/common/Narrative';
interface IDetailsPageProps extends React.ClassAttributes<any>, WithTranslation {
......@@ -223,7 +224,7 @@ class InventoryDetailsPage extends React.Component<IDetailsPageProps> {
/>
<CardContent>
{ inventoryGroup.note && <p>{ inventoryGroup.note }</p> }
{ inventoryGroup.note && <Narrative>{ inventoryGroup.note }</Narrative> }
<Properties>
<PropertiesItem title={ t(['client:model.AccessionInvGroup.isWebVisible', 'client:model._.isWebVisible']) }>
......
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