diff --git a/packages/client/src/service/AccessionService.ts b/packages/client/src/service/AccessionService.ts index fa4352a1ee013da6f9458a3e9b0d3f9c4ab552f9..b9ce981dffc0eb80149021189d8da1d9b69c3e98 100644 --- a/packages/client/src/service/AccessionService.ts +++ b/packages/client/src/service/AccessionService.ts @@ -5,13 +5,14 @@ import { AxiosInstance, AxiosRequestConfig } from 'axios'; import Accession from '@gringlobal/client/model/gringlobal/Accession'; import AccessionFilter from '@gringlobal/client/model/gringlobal/AccessionFilter'; -import { IPageRequest, FilteredPage } from '@gringlobal/client/model/page'; +import { IPageRequest, FilteredPage, Page } from '@gringlobal/client/model/page'; import { dereferenceReferences3 } from '@gringlobal/client/utilities'; import AccessionSource from '@gringlobal/client/model/gringlobal/AccessionSource'; import AccessionInvGroup from '@gringlobal/client/model/gringlobal/AccessionInvGroup'; import AccessionDetails from '@gringlobal/client/model/gringlobal/AccessionDetails'; import AccessionAction from '@gringlobal/client/model/gringlobal/AccessionAction'; import AccessionActionFilter from '@gringlobal/client/model/gringlobal/AccessionActionFilter'; +import AuditLog from '@gringlobal/client/model/gringlobal/AuditLog'; const URL_UPDATE = '/api/v1/a'; const URL_CREATE = '/api/v1/a'; @@ -22,6 +23,7 @@ const URL_ADD_MATERIAL = '/api/v1/a/acquire'; const URL_DETAILS = UrlTemplate.parse('/api/v1/a/details/{id}'); const URL_OVERVIEW = UrlTemplate.parse('/api/v1/a/overview/{groupBy}'); const URL_LIST_ACTIONS = '/api/v1/a/action/list'; +const URL_ACCESSION_AUDIT_LOGS = UrlTemplate.parse('/api/v1/a/auditlog/{id}'); /** * Accession service @@ -246,6 +248,33 @@ class AccessionService { }); return data as FilteredPage; }); + }; + + /** + * accessionAuditLogs at /api/v1/a/auditlog/{id} + * + * @param id undefined + * @param page the page + * @param xhrConfig additional xhr config + */ + public accessionAuditLogs = (id: number, page: IPageRequest = {}, xhrConfig?: AxiosRequestConfig): Promise> => { + + 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_ACCESSION_AUDIT_LOGS.expand({ id }) + (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 Page); } } diff --git a/packages/client/src/service/CooperatorService.ts b/packages/client/src/service/CooperatorService.ts index 0325c10b36b93a548366a1c09157f5b4fb378539..1e91fcd7c554dce7ee784e43a7919fe99f3d6843 100644 --- a/packages/client/src/service/CooperatorService.ts +++ b/packages/client/src/service/CooperatorService.ts @@ -7,12 +7,14 @@ import { dereferenceReferences3 } from '@gringlobal/client/utilities'; import Cooperator from '@gringlobal/client/model/gringlobal/Cooperator'; import { FilteredPage, Page, IPageRequest } from '@gringlobal/client/model/page'; import CooperatorFilter from '@gringlobal/client/model/gringlobal/CooperatorFilter'; +import AuditLog from '@gringlobal/client/model/gringlobal/AuditLog'; const URL_UPDATE_COOPERATOR = '/api/v1/cooperator'; const URL_CREATE_COOPERATOR = '/api/v1/cooperator'; const URL_FILTER = '/api/v1/cooperator/filter'; const URL_LIST = '/api/v1/cooperator/list'; const URL_GET_COOPERATOR = UrlTemplate.parse('/api/v1/cooperator/{id}'); +const URL_COOPERATOR_AUDIT_LOGS = UrlTemplate.parse('/api/v1/cooperator/auditlog/{id}'); /** * Cooperator service @@ -29,7 +31,7 @@ class CooperatorService { /** * filter at /api/v1/cooperator/filter * - * @param f undefined + * @param filter undefined * @param page undefined * @param xhrConfig additional xhr config */ @@ -65,7 +67,7 @@ class CooperatorService { /** * list at /api/v1/cooperator/list * - * @param f undefined + * @param filter undefined * @param page undefined * @param xhrConfig additional xhr config */ @@ -155,6 +157,33 @@ class CooperatorService { ...content, }).then(({ data }) => data as Cooperator); }; + + /** + * cooperatorAuditLogs at /api/v1/cooperator/auditlog/{id} + * + * @param id undefined + * @param page the page + * @param xhrConfig additional xhr config + */ + public cooperatorAuditLogs = (id: number, page: IPageRequest = {}, xhrConfig?: AxiosRequestConfig): Promise> => { + + 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_COOPERATOR_AUDIT_LOGS.expand({ id }) + (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 Page); + } } export default CooperatorService; diff --git a/packages/ui-express/locales/en/express.json b/packages/ui-express/locales/en/express.json index 4711ef8bd7c013d6a9bed47e267ffa3ef96d3573..ff45760219dee7a3e1cac7d51e2813b0984a8eb5 100644 --- a/packages/ui-express/locales/en/express.json +++ b/packages/ui-express/locales/en/express.json @@ -141,6 +141,8 @@ } }, "details": { + "auditLogs": "Audit Logs", + "actions": "Actions", "title": "Accession", "attachments": "Attachments", "quarantine": "Accession quarantine", @@ -184,6 +186,7 @@ "title": "Cooperators" }, "details": { + "auditLogs": "Audit Logs", "title": "Cooperator", "auditingInformation": "Auditing information", "organization": "Organization", diff --git a/packages/ui-express/src/accession/translations.json b/packages/ui-express/src/accession/translations.json index 6a0cf29572583893a7c1f0db8d659d6ed0e45628..187a5fbb49055ce7a95e891c58b720bce254e5f1 100644 --- a/packages/ui-express/src/accession/translations.json +++ b/packages/ui-express/src/accession/translations.json @@ -13,6 +13,8 @@ } }, "details": { + "auditLogs": "Audit Logs", + "actions": "Actions", "title": "Accession", "attachments": "Attachments", "quarantine": "Accession quarantine", diff --git a/packages/ui-express/src/accession/ui/AccessionDetailsPage.tsx b/packages/ui-express/src/accession/ui/AccessionDetailsPage.tsx index 6bc39c3c10575542ea7d3080b2110e0f42fddfd0..bd9f2dc0961f3073ff591541116a4399e3511b69 100644 --- a/packages/ui-express/src/accession/ui/AccessionDetailsPage.tsx +++ b/packages/ui-express/src/accession/ui/AccessionDetailsPage.tsx @@ -5,10 +5,8 @@ import { WithTranslation, withTranslation } from 'react-i18next'; // Action import { ApiCall } from '@gringlobal/client/model/common'; import { getAccessionAction, getAccessionInventories, uploadAccessionAttachment } from 'accession/action/public'; - -import Tab from '@material-ui/core/Tab'; -import HeaderTabs from '@gringlobal/client/ui/common/tabs/HeaderTabs'; -import TabPanel from '@gringlobal/client/ui/common/tabs/TabPanel'; +// Service +import { AccessionService } from '@gringlobal/client/service'; // Models import AccessionDetails from '@gringlobal/client/model/gringlobal/AccessionDetails'; import Inventory from '@gringlobal/client/model/gringlobal/Inventory'; @@ -17,7 +15,7 @@ import AccessionSource from '@gringlobal/client/model/gringlobal/AccessionSource import AccessionIpr from '@gringlobal/client/model/gringlobal/AccessionIpr'; import AccessionAction from '@gringlobal/client/model/gringlobal/AccessionAction'; import Accession from '@gringlobal/client/model/gringlobal/Accession'; -import { FilteredPage } from '@gringlobal/client/model/page'; +import { FilteredPage, SortDirection, Page } from '@gringlobal/client/model/page'; // UI import Loading from '@gringlobal/client/ui/common/Loading'; import { Card, CardContent, CardHeader, CardActions, Button } from '@material-ui/core'; @@ -33,6 +31,10 @@ import PageTitle from '@gringlobal/client/ui/common/PageTitle'; import FileUploader from '@gringlobal/client/ui/common/file-uploader'; import AttachmentsDisplay from 'repository/ui/c/AttachmentsDisplay'; import AuditDataDisplay from 'common/AuditDataDisplay'; +import { AuditLogsTable } from 'common/AuditLogsTable'; +import Tab from '@material-ui/core/Tab'; +import HeaderTabs from '@gringlobal/client/ui/common/tabs/HeaderTabs'; +import TabPanel from '@gringlobal/client/ui/common/tabs/TabPanel'; const InventoryTableConfig = new TableConfiguration(TableConfiguration.merge( @@ -66,6 +68,14 @@ interface IDetailsPageProps extends React.ClassAttributes, WithTranslation uploadAccessionAttachment: (id: number, file: File) => void; } +enum AccessionDetailsTabs { + INFO = 'info', + ACTIONS = 'actions', + INVENTORIES = 'inventories', + ATTACHMENTS = 'attachments', + AUDITLOGS = 'auditlogs', +} + class AccessionDetailsPage extends React.Component { protected static needs = [ @@ -74,7 +84,8 @@ class AccessionDetailsPage extends React.Component { ]; public state = { - selectedTab: 'info', + selectedTab: AccessionDetailsTabs.INFO, + auditLogs: null, }; public constructor(props) { @@ -101,6 +112,13 @@ class AccessionDetailsPage extends React.Component { this.setState({ selectedTab: newValue, }); + + if (newValue === AccessionDetailsTabs.AUDITLOGS) { + if (!this.state.auditLogs) { + AccessionService.accessionAuditLogs(+this.props.id) + .then((auditLogs) => this.setState({ auditLogs })); + } + } } private handleUploading = (files: File[]) => { @@ -111,9 +129,20 @@ class AccessionDetailsPage extends React.Component { } }; + private onSortChange = (sortBy: string, dir: SortDirection): void => { + AccessionService.accessionAuditLogs(+this.props.id, { properties: [sortBy], direction: [dir] }) + .then((auditLogs) => this.setState({ auditLogs })); + }; + + private loadMore = (): void => { + const { auditLogs } = this.state; + AccessionService.accessionAuditLogs(+this.props.id, Page.nextPage(auditLogs)) + .then((newAuditLogs) => this.setState({ auditLogs: Page.merge(auditLogs, newAuditLogs) })); + }; + public render(): React.ReactNode { const { inventories, accessionCall, t } = this.props; - const { selectedTab } = this.state; + const { selectedTab, auditLogs } = this.state; const columns = InventoryTableConfig.getColumns(inventories && inventories.data && inventories.data.content ? inventories.data.content[0] : null); if (!accessionCall) { return null; @@ -132,16 +161,16 @@ class AccessionDetailsPage extends React.Component { scrollButtons="auto" aria-label="Inventory tabs" > - { accession.accessionNumber } : t('accession.public.p.details.title') } /> - - - - {/* */} + { accession.accessionNumber } : t('accession.public.p.details.title') } /> + + + + { loading && } { accession && <> - + <> { - + { /> - + { accession.attachments && } @@ -361,7 +390,7 @@ class AccessionDetailsPage extends React.Component { - + <> { accession.actions && accession.actions.sort(AccessionAction.completedDateSort).map((action) => ( @@ -385,8 +414,18 @@ class AccessionDetailsPage extends React.Component { - + <> + { !auditLogs && } + { auditLogs && auditLogs.content.length > 0 && + + } diff --git a/packages/ui-express/src/inventory/ui/c/InventoryAuditLogsTable.tsx b/packages/ui-express/src/common/AuditLogsTable.tsx similarity index 77% rename from packages/ui-express/src/inventory/ui/c/InventoryAuditLogsTable.tsx rename to packages/ui-express/src/common/AuditLogsTable.tsx index 0e0c364140340df8bcf6dfa70c7a8620213f3e79..47eb21282bbc4cae5dc36425a158928656bc520d 100644 --- a/packages/ui-express/src/inventory/ui/c/InventoryAuditLogsTable.tsx +++ b/packages/ui-express/src/common/AuditLogsTable.tsx @@ -20,6 +20,7 @@ const InventoryAuditLogsTableConfigProps = { previousState: { sort: null }, newState: { sort: null }, createdBy: { sort: null }, + property: { sort: null }, }, columnsRenderers: { logDate: Renderers.DATE_RENDERER, @@ -29,14 +30,14 @@ const InventoryAuditLogsTableConfigProps = { 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 + return propertyName ? t([`client:model.${className}.${propertyName}`, `client:model._.${propertyName}`]) : null }, }, }; -const InventoryAuditLogsTableConfig = new TableConfiguration(InventoryAuditLogsTableConfigProps as any); +const AuditLogsTableConfig = new TableConfiguration(InventoryAuditLogsTableConfigProps as any); -interface IInventoryAuditLogsTableProps { +interface IAuditLogsTableProps { auditLogs: AuditLog[]; total: number; sort: ISort[]; @@ -44,17 +45,17 @@ interface IInventoryAuditLogsTableProps { loadMore: () => void; } -export const InventoryAuditLogsTable = (props: IInventoryAuditLogsTableProps) => { +export const AuditLogsTable = (props: IAuditLogsTableProps) => { const { auditLogs, total, sort, onSortChange, loadMore } = props; if (auditLogs && auditLogs.length > 0) { return (
, WithTranslation { @@ -28,12 +34,22 @@ interface ICooperatorDetailsPage extends React.ClassAttributes, WithTransla navigateTo: (path: string, query?: object) => void; } +enum CooperatorDetailsTabs { + INFO = 'info', + AUDITLOGS = 'auditlogs', +} + class CooperatorDetailsPage extends React.Component { protected static needs = [ ({ params: { id } }) => getCooperatorAction(id), ]; + public state = { + selectedTab: CooperatorDetailsTabs.INFO, + auditLogs: null, + }; + public componentDidMount(): void { const { id, cooperatorCall, getCooperatorAction } = this.props; @@ -54,135 +70,189 @@ class CooperatorDetailsPage extends React.Component { navigateTo(`/cooperator/edit/${id}`); }; + private selectTab = (event, newValue) => { + this.setState({ + selectedTab: newValue, + }); + + if (newValue === CooperatorDetailsTabs.AUDITLOGS) { + if (!this.state.auditLogs) { + CooperatorService.cooperatorAuditLogs(+this.props.id) + .then((auditLogs) => this.setState({ auditLogs })); + } + } + }; + + private onSortChange = (sortBy: string, dir: SortDirection): void => { + CooperatorService.cooperatorAuditLogs(+this.props.id, { properties: [sortBy], direction: [dir] }) + .then((auditLogs) => this.setState({ auditLogs })); + }; + + private loadMore = (): void => { + const { auditLogs } = this.state; + CooperatorService.cooperatorAuditLogs(+this.props.id, Page.nextPage(auditLogs)) + .then((newAuditLogs) => this.setState({ auditLogs: Page.merge(auditLogs, newAuditLogs) })); + }; + public render() { const { cooperatorCall, t } = this.props; if (! cooperatorCall) { return null; } const { loading, data: cooperator } = cooperatorCall; + const { selectedTab, auditLogs } = this.state; + return ( <> - + + + + { loading && } { cooperator && ( -
- - - - - - - - - { (cooperator.currentCooperator && cooperator.currentCooperator !== cooperator.id) && - - See - - } - { [ 'statusCode', 'categoryCode', 'disciplineCode', 'title' ].map((property) => ( - - { cooperator[property] && } - - )) } - { [ 'firstName', 'lastName', 'job'].map((property) => ( - - { cooperator[property] } - - )) } - { [ 'email', 'secondaryEmail' ].map((property) => ( - - { cooperator[property] && } - - )) } - { [ 'primaryPhone', 'secondaryPhone', 'note' ].map((property) => ( - - { cooperator[property] } - - )) } - - - - - - - - - { [ 'organization', 'organizationAbbrev' ].map((property) => ( - - { cooperator[property] } - - )) } - - { cooperator.organizationRegionCode && } - - - - - - - - - - { [ 'addressLine1', 'addressLine2', 'addressLine3', 'city' ].map((property) => ( - - { cooperator[property] } - - )) } - { cooperator.geography && - - { cooperator.geography.adm1 }{ cooperator.geography.adm1 && ', ' }{ cooperator.geography.countryCode } - - } - - - - - { (cooperator.secondaryOrganization || cooperator.secondaryOrganizationAbbrev) && - - - - - { [ 'secondaryOrganization', 'secondaryOrganizationAbbrev' ].map((property) => ( - - { cooperator[property] } + <> + +
+ + + + + + + + + { (cooperator.currentCooperator && cooperator.currentCooperator !== cooperator.id) && + + See + + } + { [ 'statusCode', 'categoryCode', 'disciplineCode', 'title' ].map((property) => ( + + { cooperator[property] && } + + )) } + { [ 'firstName', 'lastName', 'job'].map((property) => ( + + { cooperator[property] } + + )) } + { [ 'email', 'secondaryEmail' ].map((property) => ( + + { cooperator[property] && } + + )) } + { [ 'primaryPhone', 'secondaryPhone', 'note' ].map((property) => ( + + { cooperator[property] } + + )) } + + + + + + + + + { [ 'organization', 'organizationAbbrev' ].map((property) => ( + + { cooperator[property] } + + )) } + + { cooperator.organizationRegionCode && } - )) } - - - } - - { (cooperator.secondaryAddressLine1 || cooperator.secondaryAddressLine2 || cooperator.secondaryAddressLine3 || cooperator.secondaryCity || cooperator.secondaryGeography) && - - - - - { [ 'secondaryAddressLine1', 'secondaryAddressLine2', 'secondaryAddressLine3', 'secondaryCity' ].map((property) => ( - - { cooperator[property] } + + + + + + + + + { [ 'addressLine1', 'addressLine2', 'addressLine3', 'city' ].map((property) => ( + + { cooperator[property] } + + )) } + { cooperator.geography && + + { cooperator.geography.adm1 }{ cooperator.geography.adm1 && ', ' }{ cooperator.geography.countryCode } - )) } - { cooperator.secondaryGeography && + } + + + + + { (cooperator.secondaryOrganization || cooperator.secondaryOrganizationAbbrev) && + + + + + { [ 'secondaryOrganization', 'secondaryOrganizationAbbrev' ].map((property) => ( + + { cooperator[property] } + + )) } + + + } + + { (cooperator.secondaryAddressLine1 || cooperator.secondaryAddressLine2 || cooperator.secondaryAddressLine3 || cooperator.secondaryCity || cooperator.secondaryGeography) && + + + + + { [ 'secondaryAddressLine1', 'secondaryAddressLine2', 'secondaryAddressLine3', 'secondaryCity' ].map((property) => ( + + { cooperator[property] } + + )) } + { cooperator.secondaryGeography && { cooperator.secondaryGeography.adm1 }{ cooperator.secondaryGeography.adm1 && ', ' }{ cooperator.secondaryGeography.countryCode } - } - - - } - - - - - - - { cooperator.site && <>{ cooperator.site.siteShortName } } - - - - - -
+ } +
+
+
} + + + + + + + { cooperator.site && <>{ cooperator.site.siteShortName } } + + + + + +
+ + + <> + { !auditLogs && } + { auditLogs && auditLogs.content.length > 0 && + + } + + + ) } ); diff --git a/packages/ui-express/src/inventory/ui/InventoryDetailsPage.tsx b/packages/ui-express/src/inventory/ui/InventoryDetailsPage.tsx index 0001709fc46be8f33dc56ba05fbc4c41f5cd8bfa..9d458551850afc9de4bc11f934deb592ec5f1686 100644 --- a/packages/ui-express/src/inventory/ui/InventoryDetailsPage.tsx +++ b/packages/ui-express/src/inventory/ui/InventoryDetailsPage.tsx @@ -28,7 +28,7 @@ import ButtonBar from '@gringlobal/client/ui/common/button/ButtonBar'; import { CodeValueDisplay } from 'common/CodeValue'; import PageTitle from '@gringlobal/client/ui/common/PageTitle'; import { BasicInventoryActionsTable as InventoryActionsTable } from 'inventory/ui/c/InventoryActionsTable'; -import { InventoryAuditLogsTable } from 'inventory/ui/c/InventoryAuditLogsTable'; +import { AuditLogsTable } from 'common/AuditLogsTable'; import AttachmentsDisplay from 'repository/ui/c/AttachmentsDisplay'; import FileUploader from '@gringlobal/client/ui/common/file-uploader'; import AuditDataDisplay from 'common/AuditDataDisplay'; @@ -427,7 +427,7 @@ class InventoryDetailsPage extends React.Component { { !auditLogs && } { auditLogs && auditLogs.content.length > 0 && -