Commit 1f71e927 authored by Matija Obreza's avatar Matija Obreza

Merge branch '128-audit-history-inventory' into 'master'

Resolve "Audit history: Inventory"

Closes #128

See merge request !100
parents bc3a8083 bd254285
......@@ -80,6 +80,14 @@
"code": "Code",
"group": "Group"
},
"AuditLog": {
"logDate": "Date",
"action": "Action",
"propertyName": "Property Name",
"property": "Property",
"previousState": "Previous State",
"newState": "New State"
},
"Geography": {
"geography": "Geography",
"currentGeography": "Current Valid Geography",
......
/*
* Defined in Swagger as '#/definitions/AuditLog'
*/
import ClassPK from '@gringlobal/client/model/gringlobal/ClassPK';
class AuditLog {
public action: string;
public classPk: ClassPK;
public createdBy: string;
public entityId: number;
public id: number;
public logDate: Date;
public newEntity: any;
public newState: string;
public previousEntity: any;
public previousState: string;
public propertyName: string;
public referencedEntity: ClassPK;
}
export default AuditLog;
/*
* Defined in Swagger as '#/definitions/ClassPK'
*/
class ClassPK {
public classname: string;
public id: number;
public shortName: string;
}
export default ClassPK;
......@@ -69,6 +69,14 @@
"code": "Code",
"group": "Group"
},
"AuditLog": {
"logDate": "Date",
"action": "Action",
"propertyName": "Property Name",
"property": "Property",
"previousState": "Previous State",
"newState": "New State"
},
"Geography": {
"geography": "Geography",
"currentGeography": "Current Valid Geography",
......
......@@ -4,6 +4,7 @@ import * as QueryString from 'query-string';
import { AxiosInstance, AxiosRequestConfig } from 'axios';
import { IPageRequest, FilteredPage, Page } from '@gringlobal/client/model/page';
import AuditLog from '@gringlobal/client/model/gringlobal/AuditLog';
import Inventory from '@gringlobal/client/model/gringlobal/Inventory';
import InventoryDetails from '@gringlobal/client/model/gringlobal/InventoryDetails';
import InventoryFilter from '@gringlobal/client/model/gringlobal/InventoryFilter';
......@@ -30,6 +31,7 @@ const URL_GET_3 = UrlTemplate.parse('/api/v1/i/maintenance/{id}');
const URL_REMOVE_3 = UrlTemplate.parse('/api/v1/i/maintenance/{id}');
const URL_UPDATE_3 = '/api/v1/i/maintenance';
const URL_CREATE_3 = '/api/v1/i/maintenance';
const URL_INVENTORY_AUDIT_LOGS = UrlTemplate.parse('/api/v1/i/auditlog/{id}');
const URL_LIST_3 = '/api/v1/i/maintenance/list';
const URL_LIST_ACTIONS = '/api/v1/i/action/list';
const URL_START_ACTIONS = '/api/v1/i/action/start';
......@@ -94,6 +96,30 @@ class InventoryService {
}).then(({ data }) => data as Inventory);
}
/**
* inventoryAuditLogs at /api/v1/i/auditlog/{id}
*
* @param id the id of inventory
* @param page the page
* @param xhrConfig additional xhr config
*/
public inventoryAuditLogs = (id: number, page: IPageRequest = {}, xhrConfig?: AxiosRequestConfig): Promise<Page<AuditLog>> => {
const qs = QueryString.stringify({
p: page.page || undefined,
l: page.size || undefined,
d: page.direction ? page.direction : undefined,
s: page.properties || undefined,
}, {});
const apiUrl = URL_INVENTORY_AUDIT_LOGS.expand({ id }) + (qs ? `?${qs}` : '');
return this._axios.request({
...xhrConfig,
url: apiUrl,
method: 'GET',
}).then(({ data }) => data as Page<AuditLog>);
}
/**
* create_2 at /api/v1/i
*
......
......@@ -751,12 +751,13 @@ interface ITableRowProps {
toggleRowSelect;
columnClasses: any;
t: any;
}
class TableRow extends React.PureComponent<ITableRowProps & WithStyles> {
class TableRowInternal extends React.PureComponent<ITableRowProps & WithStyles & WithTranslation> {
public render() {
const { index, data, selected, columnsToDisplay, toggleRowSelect, classes, columnClasses } = this.props;
const { index, data, selected, columnsToDisplay, toggleRowSelect, classes, columnClasses, t } = this.props;
// console.log(`Rendering row ${index}`);
return (
<tr className={ selected ? '' : classes.selectedRow }>
......@@ -765,17 +766,17 @@ class TableRow extends React.PureComponent<ITableRowProps & WithStyles> {
</td>
{ columnsToDisplay.map((c) => (
<td key={ c } className={ columnClasses[c] }>
{ this.write(c, data[c], data) }
{ this.write(c, data[c], data, t) }
</td>
)) }
</tr>
)
}
private write(columnKey: string, d: any, row: any[]): React.ReactNode {
private write(columnKey: string, d: any, row: any[], t): React.ReactNode {
// Call render even if cell data is null
if (this.props.columnRenderers[columnKey]) {
return this.props.columnRenderers[columnKey](d, row);
return this.props.columnRenderers[columnKey](d, row, t);
}
// Use custom renderer if provided
if (this.props.defaultRenderer) {
......@@ -796,4 +797,6 @@ class TableRow extends React.PureComponent<ITableRowProps & WithStyles> {
}
}
const TableRow = withTranslation()(TableRowInternal);
export default withUserSettings(withTranslation()(withStyles(styles)(withTheme(Table))));
......@@ -10,7 +10,7 @@ export interface IBasicConfigProps {
columnsRenderers: {
[key: string]: (value: any, rowData: object) => JSX.Element,
};
defaultRenderer?: (value: any, rowData: object) => JSX.Element;
defaultRenderer?: (value: any, rowData: object, t?: any) => JSX.Element;
defaultColumnSettings: {
[key: string]: IColumnConfiguration,
};
......
......@@ -229,7 +229,9 @@
},
"details": {
"title": "Inventory",
"actions": "Actions",
"attachments": "Attachments",
"auditLogs": "Audit Logs",
"quality": "Quality",
"viability": "Viability",
"names": "Inventory names"
......
......@@ -78,7 +78,7 @@ export const listInventoryActionsAction = (filter: InventoryActionFilter = {}, p
filter,
pageR,
},
});
})
export const loadMoreInventoryActionsAction = (actions: FilteredPage<InventoryAction>) => {
return {
......
......@@ -14,7 +14,9 @@
},
"details": {
"title": "Inventory",
"actions": "Actions",
"attachments": "Attachments",
"auditLogs": "Audit Logs",
"quality": "Quality",
"viability": "Viability",
"names": "Inventory names"
......
......@@ -2,10 +2,19 @@ import * as React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators, compose } from 'redux';
import { WithTranslation, withTranslation } from 'react-i18next';
// Model
import AccessionInvName from '@gringlobal/client/model/gringlobal/AccessionInvName';
import Inventory from '@gringlobal/client/model/gringlobal/Inventory';
import InventoryAction from '@gringlobal/client/model/gringlobal/InventoryAction';
import InventoryDetails from '@gringlobal/client/model/gringlobal/InventoryDetails';
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 ContentHeader from '@gringlobal/client/ui/common/heading/ContentHeader';
// Service
import { InventoryService } from '@gringlobal/client/service';
// UI
import Loading from '@gringlobal/client/ui/common/Loading';
import { Card, CardContent, CardHeader, CardActions, Button } from '@material-ui/core';
import Tab from '@material-ui/core/Tab';
......@@ -13,16 +22,13 @@ import HeaderTabs from '@gringlobal/client/ui/common/tabs/HeaderTabs';
import TabPanel from '@gringlobal/client/ui/common/tabs/TabPanel';
import { Properties, PropertiesItem } from '@gringlobal/client/ui/common/Properties';
import PrettyDate from '@gringlobal/client/ui/common/time/PrettyDate';
import InventoryDetails from '@gringlobal/client/model/gringlobal/InventoryDetails';
import { PrintSpecies } from 'common/Taxonomy';
import { AccessionLink, CooperatorLink, InventoryLink } from 'ui/common/Links';
import ButtonBar from '@gringlobal/client/ui/common/button/ButtonBar';
import { CodeValueDisplay } from 'common/CodeValue';
import AccessionInvName from '@gringlobal/client/model/gringlobal/AccessionInvName';
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 { InventoryAuditLogsTable } from 'inventory/ui/c/InventoryAuditLogsTable';
import AttachmentsDisplay from 'repository/ui/c/AttachmentsDisplay';
import FileUploader from '@gringlobal/client/ui/common/file-uploader';
......@@ -34,6 +40,13 @@ interface IDetailsPageProps extends React.ClassAttributes<any>, WithTranslation
uploadInventoryAttachment: (id: number, file: File) => void;
}
const INFO_TAB: string = 'info';
const ACTIONS_TAB: string = 'actions';
const AUDITLOGS_TAB: string = 'auditlogs';
const ATTACHMENTS_TAB: string = 'attachments';
const VIABILITY_TAB: string = 'viabiltiy';
const QUALITY_TAB: string = 'quality';
class InventoryDetailsPage extends React.Component<IDetailsPageProps> {
protected static needs = [
......@@ -41,7 +54,8 @@ class InventoryDetailsPage extends React.Component<IDetailsPageProps> {
];
public state = {
selectedTab: 'info',
selectedTab: INFO_TAB,
auditLogs: null,
};
public constructor(props) {
......@@ -66,8 +80,26 @@ class InventoryDetailsPage extends React.Component<IDetailsPageProps> {
this.setState({
selectedTab: newValue,
});
if (newValue === AUDITLOGS_TAB) {
if (!this.state.auditLogs) {
InventoryService.inventoryAuditLogs(+this.props.id)
.then((auditLogs) => this.setState({ auditLogs }));
}
}
}
private onSortChange = (sortBy: string, dir: SortDirection): void => {
InventoryService.inventoryAuditLogs(+this.props.id, { properties: [sortBy], direction: [dir] })
.then((auditLogs) => this.setState({ auditLogs }));
};
private loadMore = (): void => {
const { auditLogs } = this.state;
InventoryService.inventoryAuditLogs(+this.props.id, Page.nextPage(auditLogs))
.then((newAuditLogs) => this.setState({ auditLogs: Page.merge(auditLogs, newAuditLogs) }));
};
private handleUploading = (files: File[]) => {
const { inventoryCall, uploadInventoryAttachment } = this.props;
uploadInventoryAttachment(inventoryCall.data.id, files[0]);
......@@ -80,7 +112,7 @@ class InventoryDetailsPage extends React.Component<IDetailsPageProps> {
}
const { loading, data: inventory } = inventoryCall;
const { selectedTab } = this.state;
const { selectedTab, auditLogs } = this.state;
return (
<>
......@@ -93,16 +125,17 @@ class InventoryDetailsPage extends React.Component<IDetailsPageProps> {
scrollButtons="auto"
aria-label="Inventory tabs"
>
<Tab value="info" label={ t('inventory.public.p.details.title') } />
<Tab value="actions" label="Actions" />
<Tab value="viabiltiy" label="Viability" />
<Tab value="quality" label="Quality" />
<Tab value="attachments" label="Attachments" />
<Tab value={ INFO_TAB } label={ t('inventory.public.p.details.title') } />
<Tab value={ ACTIONS_TAB } label={ t('inventory.public.p.details.actions') } />
<Tab value={ VIABILITY_TAB } label={ t('inventory.public.p.details.viability') } />
<Tab value={ QUALITY_TAB } label={ t('inventory.public.p.details.quality') } />
<Tab value={ ATTACHMENTS_TAB } label={ t('inventory.public.p.details.attachments') } />
<Tab value={ AUDITLOGS_TAB } label={ t('inventory.public.p.details.auditLogs') } />
</HeaderTabs>
{ loading && <Loading/> }
{ inventory &&
<>
<TabPanel value={ selectedTab } index="info">
<TabPanel value={ selectedTab } index={ INFO_TAB }>
<>
<Card>
<CardHeader
......@@ -381,7 +414,7 @@ class InventoryDetailsPage extends React.Component<IDetailsPageProps> {
</>
</TabPanel>
<TabPanel value={ selectedTab } index="attachments">
<TabPanel value={ selectedTab } index={ ATTACHMENTS_TAB }>
<Card>
<CardHeader title={ t('inventory.public.p.details.attachments') } />
{ inventory.attachments && <AttachmentsDisplay attachments={ inventory.attachments }/> }
......@@ -390,13 +423,13 @@ class InventoryDetailsPage extends React.Component<IDetailsPageProps> {
</div>
</Card>
</TabPanel>
<TabPanel value={ selectedTab } index="actions">
<TabPanel value={ selectedTab } index={ ACTIONS_TAB }>
<>
<InventoryActionsTable actions={ inventory.actions.sort(InventoryAction.completedDateSort) } />
</>
</TabPanel>
<TabPanel value={ selectedTab } index="viability">
<TabPanel value={ selectedTab } index={ VIABILITY_TAB }>
<Card>
<CardHeader title={ t('inventory.public.p.details.viability') } />
{ inventory.viability && inventory.viability.map((inventoryViability) => (
......@@ -407,7 +440,7 @@ class InventoryDetailsPage extends React.Component<IDetailsPageProps> {
</Card>
</TabPanel>
<TabPanel value={ selectedTab } index="quality">
<TabPanel value={ selectedTab } index={ QUALITY_TAB }>
<Card>
<CardHeader title={ t('inventory.public.p.details.quality') } />
{ inventory.qualityStatus && inventory.qualityStatus.map((qualityStatus) => (
......@@ -417,6 +450,22 @@ class InventoryDetailsPage extends React.Component<IDetailsPageProps> {
) ) }
</Card>
</TabPanel>
<TabPanel value={ selectedTab } index={ AUDITLOGS_TAB }>
<>
{ !auditLogs && <Loading/> }
{
auditLogs && auditLogs.content.length > 0 &&
<InventoryAuditLogsTable
auditLogs={ auditLogs.content }
sort={ auditLogs.sort }
total={ auditLogs.totalElements }
onSortChange={ this.onSortChange }
loadMore={ this.loadMore }
/>
}
</>
</TabPanel>
</>
}
</>
......
import * as React from 'react';
// Model
import AuditLog from '@gringlobal/client/model/gringlobal/AuditLog';
import { ISort, SortDirection } from '@gringlobal/client/model/page';
// UI
import { BasicTableConfiguration as TableConfiguration } from '@gringlobal/client/ui/common/table/TableConfiguration';
import Table, { Renderers } from '@gringlobal/client/ui/common/table/Table';
const InventoryAuditLogsTableConfigProps = {
defaultColumns: [
'logDate',
'action',
'propertyName',
'property',
'previousState',
'newState',
'createdBy',
],
defaultColumnSettings: {
previousState: { sort: null },
newState: { sort: null },
createdBy: { sort: null },
},
columnsRenderers: {
logDate: Renderers.DATE_RENDERER,
property: (value, row, t): JSX.Element => {
if (!row || typeof row !== 'object') { return null }
const className = row.classPk.classname.split('.').pop(); // split using dots and get the last element of array
const key = 'propertyName';
const propertyName = row[key];
return propertyName ? t(`client:model.${className}.${propertyName}`) : null
},
},
};
const InventoryAuditLogsTableConfig = new TableConfiguration(InventoryAuditLogsTableConfigProps as any);
interface IInventoryAuditLogsTableProps {
auditLogs: AuditLog[];
total: number;
sort: ISort[];
onSortChange: (sortBy: string, dir: SortDirection | null) => void;
loadMore: () => void;
}
export const InventoryAuditLogsTable = (props: IInventoryAuditLogsTableProps) => {
const { auditLogs, total, sort, onSortChange, loadMore } = props;
if (auditLogs && auditLogs.length > 0) {
return (
<Table
noWrap
tableKey="inventory-auditlogs-table"
type={ 'AuditLog' }
columns={ InventoryAuditLogsTableConfig.defaultColumns }
data={ auditLogs }
tableConfig={ InventoryAuditLogsTableConfig }
sort={ sort }
total={ total }
onSortChange={ onSortChange }
loadMore={ loadMore }
/>
)
} else {
return null;
}
};
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